n8n 스케줄 트리거 시간 오차 해결 방법
n8n 스케줄 트리거가 의도한 시간과 다르게 실행되는 문제를 해결합니다. 컨테이너 TZ 변수, GENERIC_TIMEZONE 환경 변수, 그리고 워크플로우별 타임존 설정을 동기화하여 정확한 스케줄링을 구현하는 방법을 단계별로 설명합니다.
n8n 스케줄 트리거가 잘못된 시간에 실행되는 이유
n8n 스케줄 트리거가 잘못된 시간에 실행되는 이유는 n8n이 세 곳에서 각각 타임존을 읽어오기 때문이며, 그중 하나만 수정해서는 문제가 완전히 해결되지 않습니다. 이 세 곳은 컨테이너 자체의 TZ 변수, 인스턴스 기본값인 GENERIC_TIMEZONE, 그리고 개별 워크플로우 내부에 설정된 타임존입니다. 이 세 가지를 모두 한 번에 설정하면 이후 작성하는 모든 스케줄이 의도한 시간에 실행됩니다.
먼저 잘못된 추측을 바로잡아야 합니다. 새로 설치한 자가 호스팅 n8n은 UTC(협정 세계시) 기준으로 스케줄을 실행하지 않습니다. 공식 이미지에는 TZ가 설정되어 있지 않으므로 컨테이너 시계는 UTC를 따릅니다. 스케줄링은 별도의 계층이며, 2026년 8월 기준 n8n 문서에 명시된 GENERIC_TIMEZONE의 기본값은 America/New_York입니다. 따라서 설정을 변경하지 않은 인스턴스는 뉴욕 시간을 기준으로 스케줄 트리거를 실행합니다. 이것이 사용자들이 보고하는 시간 오차가 UTC와의 실제 시차와 일치하지 않는 이유입니다. 베를린에 있는 사용자가 06:00로 설정하면 현지 시간 12:00에 실행되며, 미국은 일광 절약 시간제(서머타임)가 적용되었으나 유럽은 아직 적용되지 않은 3월의 특정 주에는 11:00에 실행됩니다.
세 가지 시간대 계층과 우선순위
TZ는 컨테이너 내부 운영체제의 시간대입니다. n8n 문서에서는 이를 시스템 시간대를 설정하여 date과 같은 스크립트나 명령어가 반환하는 값을 제어하는 변수로 설명합니다. 이 설정은 컨테이너 내부에서 date가 출력하는 내용, 컨테이너 로그 줄에 기록되는 타임스탬프, Code 노드에서 new Date()가 반환하는 값, 그리고 컨테이너 내에서 실행하는 모든 셸 스크립트가 인식하는 시간을 결정합니다. Schedule Trigger가 실행되는 시점에는 아무런 영향을 주지 않습니다.
GENERIC_TIMEZONE은 n8n 인스턴스 시간대입니다. 문서는 이를 n8n 인스턴스 시간대라고 부르며, Cron과 같은 스케줄 노드에 중요하다고 명시합니다. 여기서 Cron은 표준 시간 기반 스케줄링 구문을 의미하며, n8n은 이를 Schedule Trigger의 Custom (Cron) 옵션으로 제공합니다.
워크플로 시간대는 워크플로별로 설정합니다. 캔버스에서 워크플로를 열고 오른쪽 상단의 점 세 개를 선택한 뒤 Settings를 선택하여 Timezone 값을 변경하십시오. 이 설정은 해당 워크플로에 한해 GENERIC_TIMEZONE을 재정의합니다.
Schedule Trigger의 경우 우선순위는 고정되어 있습니다. n8n은 워크플로에 설정된 시간대가 있으면 이를 사용하고, 없으면 GENERIC_TIMEZONE의 인스턴스 시간대를 사용하며, 이마저도 없으면 내장 기본값인 America/New_York를 사용합니다. TZ은 이 결정 과정의 어떤 단계에서도 참조되지 않습니다.
노드 내부의 날짜 처리는 코드가 어떤 시계를 참조하느냐에 따라 결과가 달라집니다. n8n 표현식의 기반이 되는 날짜 라이브러리인 Luxon은 n8n 시간대를 사용하므로, $now과 $today는 트리거와 동일하게 워크플로 우선, 그다음 인스턴스 순서로 적용됩니다. Code 노드에서 사용하는 일반 JavaScript의 new Date()은 운영체제에 시간을 묻기 때문에 TZ를 따릅니다. 이러한 차이가 혼란의 주된 원인입니다. 트리거는 정확하게 작동하더라도 워크플로가 기록하는 모든 타임스탬프는 몇 시간씩 차이가 날 수 있습니다.
Compose 파일에 세 가지 모두 설정하기
파일 내에서 TZ와 GENERIC_TIMEZONE을 서로 인접하게 배치하여, 하나만 설정하고 다른 하나를 누락하는 일이 없도록 합니다. 아래 조각은 정상 작동하는 서비스에서 시간대와 관련된 부분입니다. 파일의 나머지 부분인 리버스 프록시와 인증서 설정은 HTTPS 뒤에서 호스팅하는 n8n 가이드를 참조하십시오.
services:
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- GENERIC_TIMEZONE=Europe/Berlin
- TZ=Europe/Berlin
- N8N_RUNNERS_ENABLED=true
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:docker compose restart이 아닌 docker compose up -d을 사용하여 적용하십시오. 재시작(restart)은 기존 컨테이너를 생성 당시의 환경 변수 그대로 다시 시작하므로, 파일 내용을 변경해도 실행 중인 프로세스에는 반영되지 않습니다. up -d는 변경된 환경을 감지하고 컨테이너를 다시 생성합니다. 이 값들을 인라인이 아닌 env 파일에 보관하더라도 동일한 재생성 규칙이 적용되며, Compose env 파일 및 비밀값 처리 가이드에서 해당 파일이 어디에서 읽히는지 확인할 수 있습니다.
Region/City 형식으로 Europe/Berlin 또는 America/Sao_Paulo와 같은 IANA(Internet Assigned Numbers Authority) 시간대 이름을 사용하십시오. 이러한 이름에는 해당 지역의 일광 절약 시간제 규칙이 포함되어 있어, 현지 시계가 변경될 때 오프셋도 함께 바뀝니다. Etc/GMT+5과 같은 고정 오프셋 이름은 계절에 따라 변경되지 않으며, 부호도 예상과 반대로 표기됩니다. LC_ALL=C TZ=Etc/GMT+5 date +%z를 실행하면 -0500가 출력됩니다. 이러한 이름은 사용하지 마십시오.
하나만 설정하면 절반만 해결되는 이유
GENERIC_TIMEZONE만 설정하면 Schedule Trigger는 원하는 시간에 작동하지만, 운영 체제를 참조하는 모든 항목은 UTC로 유지됩니다. new Date().toString()을 호출하는 Code 노드는 UTC 문자열을 반환하고, 컨테이너 로그 라인에는 UTC 시간이 찍히며, 시스템 시계를 기반으로 생성된 파일 이름은 잘못된 자정에 갱신됩니다.
TZ만 설정하면 정반대의 상황이 발생합니다. docker compose exec n8n date는 로컬 시간을 출력하므로 성공한 것처럼 보이지만, Schedule Trigger는 여전히 America/New_York을 기준으로 작동하여 의도한 시간과 6시간 차이가 나게 됩니다. 이 경우가 가장 많은 시간을 낭비하게 만드는데, 대부분의 사용자가 가장 먼저 수행하는 확인 작업이 이 설정에서는 통과되기 때문입니다.
워크플로 시간대를 설정한 후 나중에 GENERIC_TIMEZONE을 변경해도 해당 워크플로는 변경 사항을 무시합니다. 워크플로에 저장된 값이 우선하며, 누군가 해당 워크플로의 설정을 열기 전까지는 계속해서 그 값이 적용됩니다. 다른 워크플로는 정상인데 특정 워크플로만 이상한 시간에 실행된다면 거의 대부분 이 문제입니다.
추측하지 말고 시계부터 확인하십시오
호스트와 컨테이너의 시간을 직접 비교하십시오.
date
docker compose exec n8n date
docker compose exec n8n printenv TZ GENERIC_TIMEZONETZ가 설정되면 처음 두 명령은 동일한 벽시계 시간을 출력해야 합니다. printenv은 존재하는 변수마다 한 줄씩 출력하므로, 두 줄이 출력되면 둘 다 설정된 것이고 한 줄만 출력되면 절반만 수정된 상태를 보고 있는 것입니다.
이제 n8n 워크플로 내부에서 직접 확인하십시오. 컨테이너 셸에서는 워크플로 수준의 시간대가 무엇인지 알 수 없기 때문입니다. 문제가 발생하는 워크플로에 Code 노드를 추가하고 Execute Workflow로 한 번 실행하십시오.
return [
{
json: {
n8n_time: $now.toISO(),
n8n_zone: $now.zoneName,
system_time: new Date().toString(),
},
},
];n8n_zone는 해당 워크플로의 Schedule Trigger가 사용할 시간대이며, 워크플로에서 인스턴스로 이어지는 우선순위에 따라 이미 결정된 값이므로 질문에 대한 직접적인 답이 됩니다. system_time는 TZ에서 가져온 컨테이너 자체의 시간대를 담고 있습니다. 워크플로 수준의 설정은 워크플로와 함께 이동하므로, 새로 만든 워크플로가 아니라 문제가 발생하는 워크플로에서 실행하십시오. 이 두 값이 일치하지 않는다면, 설정 파일을 하나도 열어보지 않고도 문제를 찾아낸 것입니다.
Schedule Trigger 노드의 Cron 표현식
Schedule Trigger는 초 단위부터 월 단위까지 고정된 간격을 제공하며, 이를 지원하지 않는 경우를 위해 Custom (Cron) 기능을 제공합니다. Cron 표현식은 워크플로우에 설정된 시간대를 기준으로 해석되므로, 0 6 * * *은 UTC 기준 06:00이 아니라 해당 시간대의 06:00을 의미합니다. crontab guru에서 사용하는 5개 필드 형식의 표현식을 그대로 붙여넣을 수 있습니다. n8n은 선택적으로 초(second) 필드를 추가로 지원하며, 문서의 필드 표에는 초, 분, 시, 일, 월, 요일 순서로 명시되어 있습니다.
오프셋을 수동으로 계산하여 입력하지 마십시오. 베를린 시간 06:00을 맞추기 위해 UTC 인스턴스에서 0 4 * * *을 작성하는 것은 겨울에는 정확할지 몰라도 여름에는 1시간의 오차가 발생합니다. 베를린은 겨울에 UTC+1, 여름에 UTC+2를 사용하기 때문입니다. 반드시 시간대를 설정하고 의도하는 현지 시간을 직접 입력하십시오.
02:30으로 설정된 작업에 서머타임이 미치는 영향
현지 벽시계 시간은 특정 시점을 보장하지 않습니다. 1년에 두 번, 한 시간이 사라지거나 한 시간이 반복되며, 해당 시간대에 예약된 모든 작업이 영향을 받습니다. n8n을 사용하지 않더라도 모든 Linux 시스템에서 date를 사용하여 이 현상을 직접 확인할 수 있습니다.
LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30'date: invalid date '2027-03-28 02:30'위 명령어의 오타가 아닙니다. 2027-03-28에 베를린 시계는 02:00에서 03:00으로 바로 건너뛰므로, 해당 날짜에는 02:30이라는 현지 시간이 존재하지 않으며 date은 이를 특정 시점으로 변환하는 것을 거부합니다. 02:30 현지 시간에 고정된 작업은 실행될 시점을 찾지 못합니다. 인접한 시간대는 정상적으로 작동합니다. date -d '2027-03-28 01:30'은 CET로 해석되고 date -d '2027-03-28 03:30'는 CEST로 해석됩니다.
가을의 시간 전환은 정반대입니다. 2027-10-31에 베를린 시계는 03:00에서 02:00으로 되돌아가므로, 02:30이 두 번 발생합니다.
LC_ALL=C TZ=Europe/Berlin date -d '2027-10-31 02:30 CEST' '+%s'
LC_ALL=C TZ=Europe/Berlin date -d '2027-10-31 02:30 CET' '+%s'1824942600
1824946200둘 다 02:30 현지 시간으로 불리지만 3600초의 간격이 있는 서로 다른 두 시점입니다. 이 시간에 고정된 작업은 두 번 실행되거나 아무도 의도하지 않은 시간에 한 번 실행되는데, 결제 처리나 백업 로테이션에는 어느 쪽도 적합하지 않습니다. 해당 시간대 밖으로 일정을 옮기십시오. 대부분의 유럽 및 북미 지역에서 위험한 시간대는 현지 시간 기준 00:00에서 03:00 사이입니다.
인프라 작업은 UTC로 예약하고 사용자에게는 현지 시간을 표시하십시오
표준적인 해결책은 시간대가 수행하는 두 가지 역할을 분리하는 것입니다. 기계에는 안정적인 간격이 필요하고, 사람에게는 읽기 쉬운 시간이 필요합니다.
- 아무도 지켜보지 않는 작업은 워크플로 시간대를 UTC로 설정하십시오. 백업, 캐시 워밍, 로그 전송, 보고서 생성 등이 여기에 해당합니다. UTC는 일광 절약 시간이 없으므로, 연중 어느 날이든 두 실행 사이의 간격이 작성한 시간과 정확히 일치합니다.
- 사람이 확인해야 하는 작업은 일정을 UTC로 유지하고 표시하는 시점에 변환하십시오.
{{ $now.setZone('Europe/Berlin').toFormat('yyyy-MM-dd HH:mm') }}을 사용하면 트리거는 안정적으로 유지하면서 메시지 본문에 현지 시간을 포함할 수 있습니다.
이러한 분리는 n8n 외부에서도 동일하게 적용됩니다. 자동화의 일부가 VPS의 systemd 서비스 및 타이머로 실행될 때, 해당 OnCalendar 라인은 시스템 시간대를 기준으로 읽히며, 이는 고유한 설정을 가진 네 번째 시계가 됩니다. 모든 스케줄러를 UTC로 유지하면 네 가지 규칙 대신 기억해야 할 규칙이 하나로 줄어듭니다. 이는 기간을 요약하는 작업에서도 중요한데, n8n AI 에이전트 워크플로에 어제 데이터를 요청할 경우 어떤 시간대를 기준으로 하느냐에 따라 24시간의 범위가 달라질 수 있기 때문입니다.
실패 유형 및 확인 가능한 출력
모든 작업이 6시간 차이로 실행됩니다. GENERIC_TIMEZONE가 설정되지 않아 내장 기본값인 America/New_York이 적용되었습니다. docker compose exec n8n printenv GENERIC_TIMEZONE은 아무것도 출력하지 않습니다. 해당 값을 설정한 뒤 컨테이너를 다시 생성하십시오.
Compose 파일을 수정했으나 아무런 변화가 없습니다. docker compose restart을 실행했기 때문에 컨테이너가 기존 환경을 그대로 유지하고 있습니다. docker compose up -d를 실행한 뒤 docker compose exec n8n printenv TZ으로 확인하십시오.
트리거는 올바르지만 타임스탬프가 잘못되었습니다. GENERIC_TIMEZONE만 설정되어 있습니다. Code 노드 내의 new Date()는 여전히 운영체제의 UTC 시간을 읽어옵니다. TZ을 동일한 값으로 설정하고 컨테이너를 다시 생성하십시오.
특정 워크플로우가 인스턴스 설정을 무시합니다. 해당 워크플로우는 설정 내에 자체 시간대를 가지고 있으며, 이는 GENERIC_TIMEZONE보다 우선합니다. 캔버스를 열고 점 3개 메뉴의 설정(Settings), 시간대(Timezone)를 확인하십시오.
일일 작업이 올해 한 번 두 번 실행되었거나 하루를 건너뛰었습니다. 예약된 시간이 일광 절약 시간제 전환 구간에 포함되어 있습니다. 실행 시간을 변경하거나 해당 워크플로우를 UTC 기준으로 변경하십시오.
FAQ
n8n 스케줄 트리거가 왜 엉뚱한 시간에 실행됩니까?
워크플로가 사용자가 예상한 것과 다른 시간대를 사용하고 있기 때문입니다. n8n은 워크플로에 설정된 시간대를 우선하며, 설정이 없으면 GENERIC_TIMEZONE에 정의된 인스턴스 시간대를, 그마저도 없으면 기본값인 America/New_York을 사용합니다. GENERIC_TIMEZONE을 별도로 설정하지 않은 자가 호스팅 인스턴스는 UTC가 아닌 뉴욕 시간 기준으로 스케줄을 처리하므로, 사용자의 실제 시간대와 차이가 발생합니다. docker compose exec n8n printenv GENERIC_TIMEZONE을 실행해 보십시오. 아무런 출력도 없다면 설정된 적이 없는 것입니다.
n8n에서 TZ와 GENERIC_TIMEZONE의 차이는 무엇입니까?
TZ는 컨테이너 내부 운영체제의 시간대입니다. 이는 컨테이너 내부에서 date이 반환하는 값, 컨테이너 로그에 찍히는 타임스탬프, Code 노드에서 new Date()이 반환하는 값, 그리고 컨테이너 내부에서 실행하는 모든 스크립트가 인식하는 시간대를 결정합니다. GENERIC_TIMEZONE는 n8n 인스턴스 자체의 시간대이며, 스케줄 노드와 $now 같은 Luxon 표현식에서 사용됩니다. 하나만 설정하면 트리거는 맞지만 타임스탬프가 틀리거나, 타임스탬프는 맞지만 트리거가 엉뚱한 시간에 실행되는 문제가 발생합니다. 두 값을 동일하게 설정하십시오.
워크플로 시간대와 GENERIC_TIMEZONE 중 무엇을 설정해야 합니까?
GENERIC_TIMEZONE를 인스턴스 전체의 기본값으로 설정하고, 특정 워크플로가 다른 시간대에 속한 경우에만 워크플로별 설정을 사용하십시오. 워크플로 설정값은 인스턴스 설정값보다 우선하며, 이후 GENERIC_TIMEZONE를 변경해도 워크플로 설정은 바뀌지 않습니다. 따라서 워크플로별로 설정된 값을 잊어버리면 나중에 문제를 추적하기 매우 어렵습니다.
서머타임 등으로 시계가 변경될 때 02:30으로 예약된 작업은 어떻게 됩니까?
해당 지역 시간은 사라지거나 두 번 발생합니다. 예를 들어 베를린은 특정 날짜에 02:00에서 03:00으로 시간이 점프하므로 LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30'은 date: invalid date '2027-03-28 02:30'을 반환합니다. 2027-10-31에는 동일한 시계 시간이 1시간 간격으로 두 번 존재하게 됩니다. 예약 작업은 가급적 00:00에서 03:00 사이를 피하거나, 워크플로를 UTC로 설정하고 사람이 확인하는 지점에서만 현지 시간으로 변환하십시오.