cd /news/ai-agents/the-token-that-expired-mid-run-oauth… · home › topics › ai-agents › article
[ARTICLE · art-162399] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

The Token That Expired Mid-Run: OAuth Refresh in Long n8n Workflows

A developer running long n8n LLM batch workflows documented how a 60-minute OAuth access token expired mid-run, causing a ninety-minute batch job to fail halfway with a wave of 401 errors. The fix was to check token expiry before each batch with a ten-minute safety margin, keep a 401 handler as a fallback, and centralize refresh behind a single atomic database-row lock so concurrent executions cannot race on rotating refresh tokens and trigger invalid_grant lockouts.

by read6 min views2 publishedNov 2, 2026

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.

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.

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.

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. 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.

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.

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.

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.

── more in #ai-agents 4 stories · sorted by recency
── more on @n8n 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-token-that-expir…] indexed:0 read:6min 2026-11-02 · —