n8n으로 야간 배치 작업을 운영하던 중, '실패가 무서워서' 모든 노드에 재시도 설정을 적용했더니 실패 알림이 5배로 늘어났습니다. 원인은 재시도로 해결되는 실패(일시적)와 아무리 재시도해도 해결되지 않는 실패(영구적)를 분류하지 않은 채, 같은 설정을 다섯 개의 노드 전체에 무분별하게 적용했기 때문입니다. 본 글에서는 실패를 3가지로 분류하고, retryOnFail, 에러 워크플로우, 그리고 Idempotency Key의 세 가지 대책을 전환하는 설계 방식을, 실제로 가져와 작동 가능한 워크플로우 JSON과 함께 설명합니다.
23:40에 시작한 야간 배치(외부 API 호출 1개, LLM 노드 3개, Notion 쓰기 1개, 총 5개 노드)의 실측 타임라인입니다.
| 시간 | 발생한 일 |
|---|
| 23:40 | 배치 시작 |
| ... | |
| 이날 밤의 429 에러는 '1분 기다리면 사라지는' 일시적인 것이었습니다. 대기 시간 없이 최대 5회 즉시 재시도하는 것은, 성공 확률을 전혀 높이지 못하면서 상대방 측의 레이트 제한 카운터와 자신의 API 할당량만 소모시키는 자해 행위였습니다. 재시도 횟수만큼 실패가 늘어나는 이 이상한 상태는 바로 이 메커니즘 때문에 발생합니다. | |
분류를 다시 한 결과가 다음 표입니다. n8n에서 발생하는 실패는 3가지 종류로 정리할 수 있으며, 대책이 1대1로 대응됩니다.
| 실패 유형 | 구체적인 예시 | 올바른 대책 |
|---|
| 일시적 (기다리면 해결됨) | 429・503・네트워크 타임아웃 | retryOnFail=ON・maxTries=3・waitBetweenTries=5000 |
| ... | | |
| 핵심은 '재시도로 해결되는 것은 첫 번째 줄에만 해당한다'는 사실입니다. 두 번째 줄에 재시도를 추가하면 실패 반복이 늘어나고, 세 번째 줄에 재시도를 추가하면 중복 등록이 증가합니다. 저는 이 3가지를 구별하지 않고 모든 노드에 적용했고, 제목에서 설명한 결과가 실제로 나타났습니다. | | |
일시적 실패를 예상하는 노드에는 노드의 Settings 탭에서 다음 값을 입력합니다.
- Retry On Fail: ON
- Max Tries: 3
- Wait Between Tries: 5000 (밀리초)
n8n의 재시도는 고정 간격이며, 지수 백오프(Exponential Backoff)는 표준 기능에 없습니다. 그럼에도 불구하고 '5초 × 최대 3회'는 1분 만에 회복되는 종류의 레이트 제한에 대해서는 충분히 실용적이었습니다. 더 긴 대기 시간을 구현하고 싶다면 Wait 노드를 이용해 직접 만들지 말고, 먼저 상대방 API의 제한 설계(예: 시간당 할당량 등)를 확인하는 것이 비용 대비 효과가 높다는 것이 실측을 통해 얻은 교훈입니다.
설정 절차는 3가지입니다.
- 새로운 워크플로우(이름 예시: error handler)를 만들고, 실패 알림 노드(Slack 또는 이메일)를 하나 배치합니다.
- 본체 워크플로우의 Settings → Error Workflow에서 만든 error handler를 지정합니다.
- 노드 단위로 처리할 때는, 노드의 Settings → On Error → Continue (using error output)을 선택합니다. 그러면 노드에 빨간색 에러 출력이 추가되므로, 거기서 분기합니다.
error handler가 받는 데이터는 다음 형태입니다 ($json 바로 아래).
{
"workflow": { "id": "123", "name": "야간 배치 본체" },
"execution": {
...
중요한 것은 lastNodeExecuted와 execution.url입니다. 알림에 URL을 넣어두면, 실패 다음 날 아침에 로그를 열어보지 않고 현장으로 바로 이동할 수 있습니다.
Notion이나 스프레드시트에 쓰기 노드는 재시도나 재실행 시 같은 행이 2번 들어갑니다(조용히, 알림 없이). 동일한 처리를 한다면 같은 값이 되는 키를 만들어 외부 측의 유니크 제약 조건에 전달하는 것이 가장 경제적인 설계였습니다.
// Code 노드 (Run Once for All Items)
const key = $now.toFormat('yyyy-MM-dd') + '-' + String($execution.id);
return [{ json: { idempotencyKey: key } }];
이 키를 Notion의 유니크 텍스트 속성(동일 키인 경우 덮어쓰는 운영)이나 사내 DB의 유니크 제약 조건에 전달하면, 재시도나 재실행에도 단 한 행만 들어갑니다.
{
"name": "야간 배치_재시도 설계 데모",
"settings": { "executionOrder": "v1" },
...
インポート手順: n8n의 워크플로우 목록 → 「Import from Clipboard」에서 JSON을 붙여넣습니다. 「외부 API (429가 반드시 반환됨)」는 httpstat.us를 사용하며, 429가 반드시 반환되도록 설정되어 있습니다. 실행하면 「5초 × 3회 재시도 → 에러 출력 → 분류 → 알림 노드」의 흐름이 그 자리에서 보입니다. 성공 쪽의 흐름만 보고 싶을 때는 URL의 /429를 /200으로 바꾸세요. 영구적인 실패를 테스트할 때는 /404로 바꿉니다 (404는 재시도가 필요 없는 분류이지만, retryOnFail을 붙인 채로 두면 여기에서도 3번 실행됩니다. 이것을 제거하는 것이 이 글의 요지입니다).
저는 LLM 노드 3개에도 retryOnFail=ON・maxTries=5를 붙인 채로 사용하고 있었습니다. LLM 측의 429는 「일일 할당량(quota)」에 따른, 회복이 다음 날이 되는 종류의 제한입니다. 재시도 5회는 할당량을 낭비하는 것으로 확정되었고, 다음 날 아침에도 같은 장소에서 전부 실패했습니다. LLM 노드의 재시도는 제거하고 에러 워크플로우를 「야간 실패는 아침에 한 번 알림」으로 변경하자, 할당량은 회복되었고 알림은 15통에서 1통이 되었습니다. LLM의 레이트 제한은 네트워크의 일시적 실패가 아니라, 일일 할당량이라는 종류의 제한으로 다루는 것이 정답이었습니다.
- retryOnFail은 「일시적 실패 (429・503・타임아웃)」 노드에만 붙인다. 값은 Max Tries=3・Wait Between Tries=5000부터 시작한다.
- 영구적인 실패 (401・404・검증)는 에러 워크플로우로 넘긴다. 알림에 execution URL을 넣어, 다음 날 아침 로그를 열지 않아도 현장으로 갈 수 있는 형태로 만든다.
- 쓰기 노드는 멱등 키(Idempotency Key). 「날짜+실행 ID」로 키를 만들고 외부의 고유 제약 조건에 전달하면 중복 등록이 사라진다.
- 다음 액션: 오늘 작동하는 워크플로우를 하나 열어, 모든 노드의 Settings에서 retryOnFail을 붙인 위치를 확인한다. LLM 노드와 쓰기 노드에 붙어 있다면 제거하는 것이 첫 번째 조치이다.
참고 도서: n8n × AI 자동화 실전 입문 — 노코드로 만드는 AI 워크플로우 (Kindle・Kindle Unlimited 읽기放題 대상) (하야마 유우키 지음)