長時間 n8n 工作流如何處理 OAuth 權杖續期與並行刷新
The Token That Expired Mid-Run: OAuth Refresh in Long n8n Workflows
作者分享一套處理長時間 n8n 工作流 OAuth 權杖續期的方法,避免存取權杖在流程執行中過期及並行刷新互相衝突。流程應在到期前按安全餘量刷新,透過原子鎖確保同一時間只有一個執行個體刷新,並把新存取權杖與刷新權杖原子寫回共用儲存位置。作者以有效期兩分鐘的模擬權杖、五分鐘批次和三個並行執行個體測試,並建議把連續 401 轉交人工檢視,避免無限重試。
The Token That Expired Mid-Run: OAuth Refresh in Long n8n Workflows
This is part of my build-in-public series on running LLM workflows in n8n in production. Everything here is from real templates I sell and support — the mistakes are mine, the fixes are tested.
The run that died at minute sixty-one
A batch workflow processed a customer list one by one: fetch a record, call the provider API, write the result back. It usually took ninety minutes. One night it failed halfway through, and the failure was not dramatic. The first hour of calls returned 200. Then, one by one, the calls started returning 401. The workflow kept going, kept failing, and finished with half the batch done and half marked as errors that were not really errors at all.
The access token lived for sixty minutes. The workflow fetched it once at the start and reused it for every downstream call. Nobody had asked what happens when a run outlives its own credential. The token did exactly what tokens do: it expired on schedule, in the middle of my run.
The mistake was treating a credential as a static fact for the whole execution. An access token is a lease with an expiry, not a password. Any workflow that runs longer than the lease, or that may be retried after the lease ends, has to plan for renewal — or it will fail at the most boring possible moment, an hour in, with no bug to find.
Refresh before expiry, not on 401 alone
The obvious fix — catch the 401 and refresh then — helps, but only halfway. By the time you see the 401, you have already burned a call, and if several parallel branches hit the provider at once, they all see 401 at once and all try to refresh at once. That is a race, and races around token rotation are how you lock yourself out (more on that below).
So the worker now checks the token's expiry before each batch of calls, with a margin. If the token expires in less than ten minutes, it refreshes first, then proceeds. Ten minutes is not magic; it is larger than the longest single downstream call in the workflow, so a call that starts with a fresh token cannot expire mid-call under normal conditions. The rule is simple: never start work you cannot finish inside the lease you hold.
The 401 handler stays, as a safety net for clocks that drift and providers that revoke early. But it is the second line of defence, not the plan. A system whose only refresh strategy is "fail first, then recover" will fail first in production, on the customer's data, every time the lease is short.
One refresher at a time
This is the part that actually broke things a second time. After I added refresh-on-401, two concurrent executions both hit 401, both refreshed, and one of them got invalid_grant. The provider rotates refresh tokens: each refresh issues a new refresh token and invalidates the old one. Two refreshers racing means the loser presents a refresh token that is already dead.
The fix is a single owner for refresh. Token state lives in one database row, not in each execution's memory. Refreshing means claiming that row — an atomic update that only succeeds for one execution — doing the refresh, writing the new tokens back to the same row, and releasing the claim. Any other execution that needs a token while a refresh is in flight does not refresh; it waits a few seconds and re-reads the row. It gets the new token without ever touching the refresh endpoint.
If your provider does not rotate refresh tokens, this still saves you: one refresh instead of five identical ones, one audit trail, one place to look when something goes wrong. But if it does rotate — and most OAuth providers do — a lock is not an optimisation. It is the difference between a refresh and a lockout.
Persist the new token where the next run can find it
A refreshed token that only lives in the current execution's memory is a token the next execution will never see. The next run starts, reads the old token from the credential store, gets a 401, and refreshes again — or worse, tries to use a refresh token that was already rotated away by the previous run.
So the refresh writes back to the same place every run reads from: the credential store, or a single token row that the workflow reads at start. The write is atomic — new access token and new refresh token together, never one without the other — and the workflow re-reads after writing, so it proceeds with exactly what was stored. If the write fails, the run goes to review rather than proceeding with a token it cannot prove is current. A half-written token pair is worse than no token: it looks valid until the provider disagrees.
A 401 is not always "expired"
Not every 401 means "refresh and retry." A token can also be revoked, or its scopes can be narrowed, or the user can disconnect the app. Refreshing against a revoked grant does not fix anything; it just burns refresh attempts and, with some providers, trips abuse limits.
So the refresh path is capped — one refresh attempt per run per token, with a short backoff — and a second consecutive 401 after a fresh token goes straight to review, with the token state, the provider's error body, and the execution id attached. That record is what a human needs to tell "expired" from "revoked" without re-running the batch to find out. Infinite refresh loops are how a credential problem becomes an account problem.
The test is a short lease, not a long run
I do not test this by waiting an hour. The mock OAuth server issues tokens that live for two minutes, and the test workflow processes a batch that takes five minutes, with three executions running in parallel. Then I assert five things. First, no call ever goes out with a token inside its expiry margin. Second, exactly one refresh happens per expiry window, even with three executions racing. Third, every execution finishes with the token that was actually stored, not one it cached at start. Fourth, a revoked refresh token produces exactly one review record, not a retry loop. Fifth, the batch completes with no duplicated side effects from the refresh itself — refreshing a credential must never re-run the work it was protecting.
A test with a one-hour token proves the happy path. A two-minute token proves the system.
Checklist before your next long run goes live
- Token lifetime: know the lease, and never start work you cannot finish inside it.
- Refresh margin: refresh before expiry, with a margin larger than your longest single call; keep the 401 handler as a safety net only.
- Single refresher: one atomic claim per token, losers wait and re-read — never race a rotating refresh token.
- Persistent write-back: new token pair written atomically to the one place every run reads from, and re-read before use.
- Revocation handling: cap refresh attempts, send a second 401 to review with full context, never loop.
- Test with a short lease: two-minute tokens, parallel executions, one refresh per window, no duplicates.
A credential is not a configuration value. It is a lease that expires on its own schedule, and a long workflow is a promise to still be running when it does. Plan for the renewal, or the run will keep its promise and the token will not.
I'm building AI Automation Lab — production-ready n8n workflow templates. The free AI Hook Generator shows how I structure real workflows; my open-source templates live at https://github.com/zhp910318/n8n-ai-automation-lab. The store is at https://payhip.com/haopengailab, and the free workflow is at https://payhip.com/b/ulER8.
來源:DEV Community · dev.to