n8nで夜間バッチを運用していて、「失敗が怖いから」と全ノードにリトライ設定を付けたら、失敗通知が5倍になりました。原因は、リトライで直る失敗(一時的)と、いくらリトライしても直らない失敗(恒久的)を分類しないまま、同じ設定を5つのノード全部に敷き詰めたことでした。この記事では、失敗を3分類してretryOnFail・エラーワークフロー・冪等キーの3つの対策を切り替える設計を、インポートして動くワークフローJSON付きで書きます。
何が起きたか: リトライを付けたら失敗が5倍になった
23:40起動の夜間バッチ(外部API取得1本・LLMノード3本・Notion書き込み1本、計5ノード)で実測したタイムラインです。
| 時刻 | 起きたこと |
|---|---|
| 23:40 | バッチ開始 |
| 23:47 | 外部APIが429(レート制限)を返す。retryOnFail=ON・maxTries=5・待ち0秒で即時5連打し、5回とも429 |
| 23:52 | LLMノードもレート制限に当たり、同設定でその日のquotaを5回分消費 |
| 0:05 | Notion書き込みが409(競合)を返す。ここも5回リトライして、全部失敗 |
| 0:12 | 通知に「失敗」が5回×3ノードで計15通 |
この夜の429は「1分待てば消える」一時的なものでした。待ち0秒で上限5回の即時再試行は、成功確率を一切上げずに、相手側のレート制限カウンターと自分のAPI quotaを回すだけの自傷でした。リトライの数だけ失敗が増えている、という妙な状態はこの仕組みで起きます。
失敗は3種類。対策は1対1で切り替える
分類し直した結果が次の表です。n8nで起きる失敗は3種類に整理でき、対策が1対1に対応します。
| 失敗の種類 | 具体例 | 正しい対策 |
|---|---|---|
| 一時的(待てば直る) | 429・503・ネットワークタイムアウト | retryOnFail=ON・maxTries=3・waitBetweenTries=5000 |
| 恒久的(何回やっても直らない) | 401権限不足・404のURL・バリデーションエラー | リトライは付けない。エラーワークフローへ逃して通知 |
| データ依存(やり直すと二重登録) | 部分完了したあとの書き込み | 冪等キーを外部側に持たせ、同じ処理は1回だけ成功させる |
ポイントは「リトライで直るのは1行目だけ」という事実です。2行目にリトライを付けると失敗の繰り返しが増え、3行目にリトライを付けると二重登録が増えます。私はこの3種類を区別せずに全部のノードへ付けていて、まさにタイトル通りの結果になりました。
直し方1: 一時的失敗にはretryOnFail(上限3回・5秒待ち)
一時的失敗を想定するノードには、ノードのSettingsタブで次の値を入れます。
- Retry On Fail: ON
- Max Tries: 3
- Wait Between Tries: 5000(ミリ秒)
n8nのリトライは固定間隔で、指数バックオフは標準機能にはありません。それでも「5秒×上限3回」は、1分で回復する種類のレート制限に対して十分実用的でした。もっと長い待ちを組みたくなったらWaitノードで自作するのではなく、まず相手側APIの制限設計(1時間あたりの枠など)を確認する方が費用対効果が高い、というのが実測からの学びです。
直し方2: 恒久的失敗はエラーワークフローへ逃す
設定手順は3つです。
- 新規ワークフロー(名前例: error handler)を作り、失敗通知ノード(Slackやメール)を1つ置く
- 本体ワークフローのSettings → Error Workflowで、作ったerror handlerを指定する
- ノード単位で拾うときは、ノードのSettings → On Error → Continue(using error output)を選ぶ。ノードに赤いエラー出力が増えるので、そこから分岐する
error handlerが受け取るデータは次の形です($json直下)。
{
"workflow": { "id": "123", "name": "夜間バッチ本体" },
"execution": {
"id": "4567",
"url": "https://n8n.example.com/execution/4567",
"lastNodeExecuted": "外部API",
"error": { "message": "Request failed with status code 401" }
}
}
重要なのは lastNodeExecuted と execution.url です。通知にURLを入れておけば、失敗の翌朝にログを開かず現場へ直行できます。
直し方3: 再実行で二重登録が起きる書き込みには冪等キー
Notionやスプレッドシートへの書き込みノードは、リトライや再実行で同じ行が2回入ります(静かに、通知なしで)。同じ処理であれば同じ値になるキーを作り、外部側のユニーク制約に渡すのが一番安い設計でした。
// Codeノード(Run Once for All Items)
const key = $now.toFormat('yyyy-MM-dd') + '-' + String($execution.id);
return [{ json: { idempotencyKey: key } }];
このキーをNotionのユニークなテキストプロパティ(同一キーなら上書きする運用)や社内DBのユニーク制約に渡せば、リトライでも再実行でも1行しか入りません。
3つ全部を入れたワークフローJSON(インポートしてそのまま動く)
{
"name": "夜間バッチ_リトライ設計デモ",
"settings": { "executionOrder": "v1" },
"nodes": [
{
"parameters": {
"rule": { "interval": [ { "field": "cronExpression", "expression": "40 23 * * *" } ] }
},
"name": "23:40起動",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [200, 300]
},
{
"parameters": {
"jsCode": "const key = $now.toFormat('yyyy-MM-dd') + '-' + String($execution.id);\nreturn [{ json: { idempotencyKey: key } }];"
},
"name": "冪等キー生成",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [420, 300]
},
{
"parameters": { "method": "GET", "url": "https://httpstat.us/429", "options": {} },
"name": "外部API(429が必ず返る)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [640, 300],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"onError": "continueErrorOutput"
},
{
"parameters": {},
"name": "成功側: ここから書き込み(冪等キーを必ず渡す)",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [880, 220]
},
{
"parameters": {
"jsCode": "const msg = (($json.error || {}).message) || $json.message || '';\nlet cat = 'transient';\nif (/429|503|timeout|timed out/i.test(msg)) cat = 'transient';\nelse if (/401|403|404/i.test(msg)) cat = 'permanent';\nelse if (msg) cat = 'data';\nreturn [{ json: { category: cat, message: msg, workflow: $workflow.name } }];"
},
"name": "失敗を3分類",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [880, 400]
},
{
"parameters": {},
"name": "通知をここに接続(Slack / メール)",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1100, 400]
}
],
"connections": {
"23:40起動": { "main": [[{ "node": "冪等キー生成", "type": "main", "index": 0 }]] },
"冪等キー生成": { "main": [[{ "node": "外部API(429が必ず返る)", "type": "main", "index": 0 }]] },
"外部API(429が必ず返る)": {
"main": [
[ { "node": "成功側: ここから書き込み(冪等キーを必ず渡す)", "type": "main", "index": 0 } ],
[ { "node": "失敗を3分類", "type": "main", "index": 0 } ]
]
},
"失敗を3分類": { "main": [[{ "node": "通知をここに接続(Slack / メール)", "type": "main", "index": 0 }]] }
}
}
インポート手順: n8nのワークフロー一覧 → 「Import from Clipboard」でJSONを貼り付ける。「外部API(429が必ず返る)」は httpstat.us を使い、429が必ず返るようになっています。実行すると「5秒×3回のリトライ → エラー出力 → 分類 → 通知ノード」の流れがその場で見られます。成功側の流れだけ見たいときはURLの /429 を /200 に変えてください。恒久的失敗を試すときは /404 に変えます(404はリトライ不要の分類ですが、retryOnFailを付けたままにしているとここでも3回走ります。外すのがこの記事の趣旨です)。
失敗パターン: LLMノードにリトライを付けたままにしていた
私はLLMノード3本にもretryOnFail=ON・maxTries=5を付けたままにしていました。LLM側の429は「1日あたりのquota」で、回復が翌日になる種類の制限です。リトライ5回はquotaの無駄撃ちとして確定し、翌朝も同じ場所で全部失敗しました。LLMノードのリトライは外し、エラーワークフローで「夜間の失敗は朝一に1通通知」へ変えたところ、quotaは回復し、通知は15通から1通になりました。LLMのレート制限はネットワークの一時的失敗ではなく、日次quotaという種類の制限として扱うのが正解でした。
まとめ
- retryOnFailは「一時的失敗(429・503・タイムアウト)」のノードだけに付ける。値はMax Tries=3・Wait Between Tries=5000から始める
- 恒久的失敗(401・404・バリデーション)はエラーワークフローへ逃す。通知にexecution URLを入れ、翌朝ログを開かず現場へ行ける形にする
- 書き込みノードは冪等キー。「日付+実行ID」でキーを作り、外部のユニーク制約に渡せば二重登録が消える
- 次のアクション: 今日動いているワークフローを1本開き、全ノードのSettingsでretryOnFailの付け場所を確認する。LLMノードと書き込みノードに付いていたら外すのが最初の一手です。
参考書籍: n8n × AI自動化実践入門 — ノーコードで作るAIワークフロー(Kindle・Kindle Unlimited読み放題対象)(葉山悠希 著)