How does n8n refresh OAuth access tokens
n8n stores refresh tokens in encrypted oauthTokenData and refreshes generic OAuth2 on 401. Google needs access_type=offline before a refresh token exists.
William Spurlock Founder — Spurlock Studios 34 MIN
n8n refreshes OAuth access tokens by reading the refresh token out of the encrypted credential (oauthTokenData), POSTing grant_type=refresh_token to the vendor token URL, writing the new token payload back onto that same credential, and retrying the API call. On generic OAuth2 that refresh is reactive: it runs when the resource API returns tokenExpiredStatusCode (default 401), not when expires_in elapses on the clock. Google will not mint a refresh token at all unless the consent URL included access_type=offline. n8n’s built-in Google OAuth2 credential already sends access_type=offline&prompt=consent. A generic OAuth2 credential does not, until you put those values in Auth URI Query Parameters and reconnect.
This page is the mechanism. Silent overnight 401 loops, pause, and expiry alerts live in OAuth tokens will expire. The production spine is the Production n8n handbook. Across 600+ automations built and 500+ live, the “n8n does not refresh” ticket is usually “Google never issued a refresh token” or “the API never returned 401.”
I will not invent a studio-wide “refresh success rate.” Prove this credential with a captured status and an unattended retry.
The short answer
- Store. Access token and refresh token live in the credential blob n8n encrypts before it writes the database. Workers share
N8N_ENCRYPTION_KEY. - When. Generic OAuth2: after a matching HTTP status (default 401). Then persist. Then retry. Not “every 50 minutes because
expires_insaid so” — n8n staff have treated HTTP-200-with-a-dead-token-body as working-as-designed on that path (issue #32423). - Google extra config. Built-in Google OAuth2: already
access_type=offline&prompt=consentin source (GoogleOAuth2Api.credentials.ts). Generic OAuth2: you type that string into Auth URI Query Parameters, set Authentication to Body, then Sign in again. - Check. Is this grant even allowed to have a refresh token? Did Google return one? Did the resource API return the status n8n watches? Did the new payload get written?
- Not this page. Calendar for Google Testing grants, pause-on-auth, and one alert per credential — that is the expiry-ops post.
| Piece | What n8n does | What you configure |
|---|---|---|
| Token store | Encrypted oauthTokenData on the credential | Encryption key shared by every process |
| Refresh trigger (generic) | Status == tokenExpiredStatusCode | Default 401; change only if you captured 403-as-expiry |
| Refresh request | Token URL + grant_type=refresh_token | Access Token URL, client id/secret, Authentication Header vs Body |
| Google refresh token mint | Built-in already asks offline+consent | Generic: Auth URI Query Parameters, then reconnect |
Reconnect without those query params is a new access token and, often, still no refresh token. Google is explicit: the refresh token is only in the code-exchange response if access_type was offline on the authorize request (web server OAuth).
How does n8n store the refresh token?
It stores it inside the credential, encrypted, in the same payload as the access token. n8n encrypts credentials with an instance key before they hit the database. On first launch it generates that key into ~/.n8n unless you set N8N_ENCRYPTION_KEY. In queue mode every worker must use the same key or it cannot read — or write back — the blob after a refresh.
The field name operators hit in discussions and in n8n’s own OAuth helper is oauthTokenData. That object is what a successful Sign in writes, and what a successful refresh overwrites. It is not a node parameter. It is not workflow static data.
Google names the fields that land in that blob after a code exchange. refresh_token is only present if access_type=offline was on the authorize request (token response). If that key is missing, n8n stored an access token and a timer you are not using.
| Key Google may return | What it is | Stored in oauthTokenData? |
|---|---|---|
access_token | Bearer you send to Gmail/Sheets/Drive | Yes — this is what expires |
expires_in | Access-token lifetime in seconds | Often yes; generic n8n still waits for HTTP status |
refresh_token | Grant to mint a new access token | Only if Google issued it |
refresh_token_expires_in | Only when the user granted time-based access | Rare; do not assume it is always there |
scope | Space-delimited scopes actually granted | Informational |
token_type | Google: Bearer | Informational |
| Place | Allowed as the token store? | Why |
|---|---|---|
n8n Credentials UI / DB (oauthTokenData) | Yes | Encrypted; refresh helper updates it |
| Secret manager injected at deploy | Yes, if that is how you already inject credentials | Still one blob the runtime can update |
| Set node / sticky note / git JSON | No | Leak, no rotation, refresh cannot write back |
| Workflow static data “until n8n supports X” | No | Same anti-pattern n8n declined as the #32423 workaround |
Procedure — treat storage as a write path, not a screenshot:
- Confirm the credential exists in Credentials, not as a header pasted into HTTP Request.
- Confirm every queue worker has the same
N8N_ENCRYPTION_KEY. - After Sign in, do not copy the token JSON into Slack “so we have a backup.”
- After a refresh, assume the new payload is the only payload. A stale worker that writes an older
oauthTokenDatalast wins.
A refresh token n8n cannot persist is a refresh token you do not have. The next 401 has nothing to send to the token URL.
When does n8n refresh an access token?
On generic OAuth2, after the resource API returns the expired-token HTTP status. Default 401. Current generic OAuth2 source exposes Token Expired Status Code so you can set 403 when that is what the vendor actually returns (OAuth2Api.credentials.ts). Older n8n builds only matched 401 — hedge your version if the field is missing in the credential modal.
It does not mean: wake up because expires_in is stale. Community and GitHub have asked for proactive refresh from expiry metadata. As of the generic path documented in #32423, staff called status-only refresh working-as-designed. If a later n8n release adds an expires-before-request toggle, read that release note. Do not assume your graph already has it.
Numbered path for one HTTP Request with generic OAuth2 (authorization code):
- Node loads the credential and sends the current access token to the resource API.
- If the status is not
tokenExpiredStatusCode, n8n returns that response. No token URL call. - If the status matches, n8n POSTs to the Access Token URL with
grant_type=refresh_token(or re-fetches for client credentials). - On success it writes the new payload onto the credential.
- It retries the original resource request with the new access token.
- On token-URL failure the error surfaces. There is no silent Sign in.
That sequence is why a 200-with-dead-body never refreshes, and why a missing refresh token fails at step 3, not at step 1.
| Event | Generic OAuth2 | What you see |
|---|---|---|
| Resource API returns 401 (default) | Refresh (or client-credentials fetch), persist, retry | One failed status, then a success in the same execution if the grant is good |
| Resource API returns 403 meaning expired | Refresh only if you set the status field to 403 | “n8n never refreshes” until you change the field |
| Resource API returns 200 + error body | No refresh | Green node, dead token — detect the body yourself |
Token URL returns invalid_grant | Refresh already failed | Re-consent. Status code will not invent a grant |
Clock passed expires_in, no matching status yet | No refresh on generic path | Next call uses the dead access token until a matching status arrives |
Decision list:
- Capture the status of a dead-token call, not only the JSON message.
- If it is not 401, set
tokenExpiredStatusCodeto the status you captured — or branch on the body if the status is 200. - If it is 401 and refresh still never runs, you are missing a refresh token or the token URL rejected the grant.
- Do not add a Cron that “refreshes” by reconnecting. That is a human in a loop.
n8n’s HTTP Request docs are the operator surface for generic OAuth2: Authorization Code, Client Credentials, or PKCE, plus optional Auth URI Query Parameters (HTTP Request credentials). The refresh behavior lives in the runtime around that credential, not in a node toggle labeled “auto refresh.”
What extra Google config is actually required?
Google extra config is on the authorize URL, not on the Gmail node. If you never asked for offline access, Google returns an access token and no refresh token. n8n then has nothing to POST when that access token dies.
Google’s web-server docs: set access_type to offline so the authorization server returns a refresh token the first time the app exchanges the code. Default is online. prompt=consent forces the consent screen so you can mint a refresh token after an earlier online-only grant (offline access, prompt).
n8n’s generic OAuth2 field for that is Auth URI Query Parameters. The credential source placeholder is access_type=offline. For Google, send both:
access_type=offline&prompt=consent
| Consent request | Google typically returns | Unattended refresh later? |
|---|---|---|
No access_type (Google default online) | Access token, often no refresh_token | No |
access_type=offline on first consent | Access token + refresh token | Yes, until Google revokes the grant |
access_type=offline on a later silent auth | Access token, refresh token often omitted | No, unless you still have the first refresh token stored |
access_type=offline&prompt=consent | Consent screen; refresh token in the exchange | Yes — this is why n8n’s Google credential hard-codes both |
Procedure — generic Google OAuth2:
- Put
access_type=offline&prompt=consentin Auth URI Query Parameters. - Set Authentication to Body (Google’s token POST is form body; n8n’s Google credential hides this as
body). - Save. Sign in with Google again. Saving the field without reconnect does not rewrite
oauthTokenData. - If Google still withholds
refresh_tokenbecause this user already consented, revoke the app under Google Account third-party access and Sign in once more with those query params on the request.
Use the current Google parameter names. Older Stack Overflow threads still say approval_prompt=force. Google’s current web-server docs document prompt=consent. n8n’s Google credential source uses prompt=consent. Prefer that string. If a non-Google vendor still documents approval_prompt, follow that vendor — do not mix Google’s current names onto their authorize URL.
| You already did | Google’s next code exchange | Extra config still needed? |
|---|---|---|
First Sign in, no access_type | Access token, no refresh token | Yes — add offline+consent, revoke, reconnect |
First Sign in, access_type=offline | Refresh token included | No, unless you later lose the stored blob |
Second Sign in, offline, no prompt | Refresh token often omitted | Yes — prompt=consent or revoke third-party access |
| Query params edited, no new Sign in | Stored payload unchanged | Yes — reconnect is the write |
Google also says, in the client-library notes, that the tokens event which carries refresh_token occurs on the first authorization with access_type=offline, and that you must re-authorize if you already granted the app without those constraints (offline access). n8n cannot invent a field Google did not return.
Reconnect is not extra config. Extra config is the authorize query string (and Body on generic). Reconnect is the write of a new oauthTokenData. Doing only the second step repeats the first-hour demo.
| Action | Changes query params on next consent? | Writes new oauthTokenData? |
|---|---|---|
| Edit Auth URI Query Parameters, Save | Yes, for the next Sign in | No |
| Sign in / Reconnect | Uses whatever is saved now | Yes |
| Change Client Secret only | No | Maybe not — still the old refresh grant |
| Revoke app in Google Account, then Sign in | Forces a new consent | Yes, if offline is on that request |
Built-in Google nodes do this without that text field. Generic HTTP Request against Google does not. Extra config is required there. It is not required because n8n “forgot to refresh.” It is required because Google never issued the grant.
Built-in Google OAuth2 vs generic OAuth2 — which needs extra fields?
Use the predefined Google credential on Google nodes when it exists. Use generic OAuth2 on HTTP Request when you are calling a Google URL the node does not cover — and then you own the query params.
n8n recommends predefined credential types on HTTP Request when the platform has one (HTTP Request credentials). For Google that split is documented as OAuth2 single-service, generic Google OAuth2 API, or service account (Google credentials).
| Credential | Auth URI query params | Authentication | You type extra Google config? |
|---|---|---|---|
Built-in Google OAuth2 (googleOAuth2Api) | Hidden default access_type=offline&prompt=consent | Hidden body | No — already in source |
| Generic OAuth2 API | Empty string unless you fill it | Default Header | Yes — query params + usually Body |
| n8n Cloud Managed OAuth2 (listed Google nodes) | n8n’s app, not your Cloud project | n8n-managed | No Cloud Console client; you still cannot edit those hidden fields |
| Google service account | N/A — JWT, no user refresh token | N/A | Different grant. Prefer it when the API allows it |
Checklist:
- Google node (Gmail, Sheets, Drive, …) → predefined Google credential, not a hand-rolled generic unless you have a reason
- HTTP Request to a Google API → predefined Google type if n8n offers it on that node; otherwise generic with the query string above
- Server-to-server Google work the node supports → service account, not a founder OAuth login
- n8n Cloud Managed OAuth2 → fine for the listed nodes; do not expect a Google Cloud “Testing” toggle you do not own
n8n Cloud users who click Sign in with Google on the listed nodes are on Managed OAuth2. They do not paste access_type anywhere. Self-host and Custom OAuth2 still create a Web client in Google Cloud and paste the n8n redirect URI (OAuth2 single service). Extra config for refresh is a generic problem first.
Redirect URI is not refresh config, but a mismatch aborts consent before any token is stored. Copy OAuth Redirect URL from the n8n credential. Localhost is valid for Google during development (http://localhost:5678/rest/oauth2-credential/callback is the shape n8n documents). Production needs the public origin, exact protocol, and port. A working localhost Sign in does not prove the production redirect will mint a refresh token on the production host — reconnect there too.
| Host | Redirect you register in Google Cloud | Refresh-token note |
|---|---|---|
| Local n8n | The localhost callback n8n shows | Fine for proving extra query params |
| Self-host HTTPS | https://<your-domain>/rest/oauth2-credential/callback | Reconnect on this host; do not copy oauthTokenData from laptop |
| n8n Cloud custom OAuth | Cloud callback n8n shows; authorized domain n8n.cloud on the consent screen | Still no Auth URI field on built-in Google |
Which grant types even issue a refresh token?
Authorization Code (and PKCE) can. Client Credentials usually must not. If you picked the wrong grant, n8n cannot “refresh” a token the vendor never sent. RFC 6749 §4.4.3 says a client-credentials access-token response should not include a refresh token. n8n’s generic Client Credentials path fetches a new access token from the token URL; it does not present a stored refresh token that does not exist.
| Grant in the n8n credential | Refresh token expected? | What n8n does when the access token is dead |
|---|---|---|
| Authorization Code | Yes, if the vendor issued one (Google: offline access) | grant_type=refresh_token |
| PKCE | Same family as Authorization Code; still needs vendor-issued refresh token | Same refresh grant if stored |
| Client Credentials | No, per RFC 6749 | Re-fetch access token with client id/secret |
Google’s refresh call, when you do have the grant, is an HTTPS POST to https://oauth2.googleapis.com/token with client_id, client_secret, refresh_token, and grant_type=refresh_token in the body (Refresh an access token). n8n’s Google credential uses that token URL and Body authentication. Your generic credential must point at the same Access Token URL or the refresh POST goes to the wrong host.
| Body field Google expects | n8n maps it from |
|---|---|
grant_type=refresh_token | Runtime, on refresh |
refresh_token | Stored oauthTokenData |
client_id / client_secret | Credential fields, sent as Body on the Google type |
| Access Token URL host | Hidden https://oauth2.googleapis.com/token on Google type; you type it on generic |
If you pointed generic OAuth2’s Access Token URL at the authorize URL, Sign in can still complete in a browser and refresh will POST to the wrong endpoint. That is extra config of a different kind: the token URL is not the auth URL.
Decision list:
- Open the credential. Read Grant Type. If it is Client Credentials, stop looking for
refresh_token. - If it is Authorization Code against Google, look for Auth URI Query Parameters (generic) or use the built-in Google type.
- If the vendor is not Google, read their authorize-URL docs for the offline/refresh flag. Do not copy Google’s query string onto a token URL that is not Google’s.
- Do not invent a Refresh Token grant type in the graph. Store what the code exchange returned.
Wrong grant is the cheapest “refresh is broken” ticket. It is also the one a Cron cannot fix.
What does n8n write back after a refresh?
The whole new token payload, onto the same credential. Google’s refresh response is a new access token. It may or may not include a new refresh token. Some vendors rotate the refresh token on every use; Google’s documented refresh response is a new access token and does not require you to assume rotation. If n8n persists the response and a second worker writes an older blob afterward, the next refresh uses a discarded grant.
You do not configure a “write-back” toggle. The OAuth helper updates credential token data after refresh. Your job is: one writer for that credential, same encryption key, no copy of oauthTokenData living on the canvas.
| After refresh | Persist? | Operator risk |
|---|---|---|
New access_token | Yes — required | If this write fails, the next call still sends the dead bearer |
New refresh_token if the vendor rotated | Yes — required | Keeping the first refresh token forever fails on rotating vendors |
expires_in | Stored if the vendor sent it | Generic path still waits for HTTP status unless your n8n version added expiry-based refresh (hedge; do not assume) |
| Error JSON from the token URL | Not a success write | invalid_grant means re-consent, not retry the CRM |
Checklist:
- Queue workers share
N8N_ENCRYPTION_KEY - You are not running two n8n instances against one Google client with racing Sign ins
- You did not paste yesterday’s token JSON back into the credential “as a restore”
- Token URL host matches the vendor (Google:
https://oauth2.googleapis.com/token)
n8n cannot refresh with a refresh token it overwrote with an empty object. Write-back is the mechanism. Treat it like a database row, because it is one.
Google’s refresh response is a new access token; the refresh token field may be absent on that response. Keep the existing refresh token if the vendor did not rotate. Overwriting oauthTokenData with only access_token and dropping refresh_token is how a “successful” refresh orphans the grant. n8n’s helper is supposed to merge/persist the payload it got — do not hand-edit the blob to “clean it up.”
What should operators check before blaming n8n?
Check the grant, the consent URL, the stored payload, then the status code. In that order. “Reconnect” is step five, and only after you know why the first grant was incomplete.
Silent expiry ops (pause, one alert, Testing calendar) is the cousin post. This checklist is mechanism only.
- Grant Type is Authorization Code or PKCE if you expect a refresh token
- Google generic: Auth URI Query Parameters is
access_type=offline&prompt=consent - Google generic: Authentication is Body, not the generic default Header
- You clicked Sign in after those fields were saved
- Built-in Google credential was used on Google nodes (hidden params already set)
- Access Token URL is the vendor token endpoint, not the authorize URL
- Dead-token status is 401 or whatever you put in Token Expired Status Code
- Token URL errors are not
invalid_grant(the refresh token is already dead) - Encryption key matches on main and workers
- This is not Client Credentials dressed up as “OAuth refresh”
| Symptom | Check first | Not first |
|---|---|---|
| Works for ~one hour, then 401 forever | Refresh token never issued (Google extra config) | n8n “forgot” to schedule a refresh |
| 401 then immediate retry success | Mechanism is working | More nodes |
| 200 + vendor error code | Status trigger never fires | Token Expired Status Code = 200 |
invalid_grant on token URL | Grant revoked / Testing / unused window | Raising retries |
| Refresh works on one worker, not the other | Encryption key split | Google Cloud client id |
Capture two URLs before you change Cloud Console branding. Operators mix them constantly.
| URL | Host (Google) | What a failure here means |
|---|---|---|
| Resource API | gmail.googleapis.com, sheets.googleapis.com, … | Access token rejected or the API call is wrong |
| Token / Access Token URL | oauth2.googleapis.com/token | Refresh grant rejected — missing token, Header vs Body, invalid_grant |
| Authorize URL | accounts.google.com/o/oauth2/v2/auth | Consent never completed; extra query params live here |
Procedure — split the errors:
- Reproduce once. Note the HTTP status on the node (resource).
- If n8n attempted a refresh, note the token-URL status and body (
invalid_grantvs network vs 401). - If there was no token-URL call, the helper never thought the access token was expired — wrong status, or 200 body.
- If there was a token-URL call and it failed, extra Google config and reconnect are in play, not “add retries.”
- Only then open Google Cloud OAuth client settings (redirect URI, client secret).
If the last four Google-specific boxes are empty, extra config is needed. If they are full and the API returns 401 and retry still never happens, then look at persistence and workers. Do not start in the Google Cloud branding screen.
How do I prove the refresh token is there?
Prove it with behavior, not with a secret dump in Slack. n8n Cloud does not document a public “export oauthTokenData” button for operators, and you should not query credentials_entity as a habit. Google’s own OAuth guidance is to store refresh tokens in secure persistent storage and handle invalidation as a first-class path (OAuth best practices). n8n is that storage. Your proof is an unattended call after the access token is dead.
Google access tokens are short-lived. Google documents that they periodically expire and that offline access is how you mint a new one without a browser (offline access). Token responses often include expires_in on the order of an hour. Treat that as vendor metadata, not an n8n timer.
What you can observe without exporting secrets:
| Surface | Self-host | n8n Cloud |
|---|---|---|
| Execution: 401 then retry 2xx | Yes | Yes |
| Credential modal: Sign in completed | Yes | Yes |
Raw oauthTokenData keys | Sometimes visible to instance admins; treat as secret | Not a documented operator export — do not file a support ticket asking them to paste it |
| Token URL error in the node output | Yes, if the refresh ran | Yes, if the refresh ran |
N8N_ENCRYPTION_KEY mismatch | Your workers | n8n operates the fleet; still do not clone credentials across instances |
Procedure — staging proof:
- Use a non-production Google user or a throwaway Cloud project.
- Connect with the extra query params (or the built-in Google credential).
- Run one call. It should 2xx.
- Wait until the access token is dead (use the vendor
expires_inyou actually received; do not invent a studio SLA). - Run again with nobody in a consent screen.
- Pass: 2xx, or a 401 followed by a retry 2xx in the same execution.
- Fail: 401 /
invalid_tokenwith no retry, or an immediaterefreshToken is required/invalid_grant.
| Result | Meaning | Next |
|---|---|---|
| Unattended 2xx after access-token death | Refresh token is stored and the helper ran | Ship that credential shape to prod |
| 401, then retry 2xx | Reactive refresh fired | Keep tokenExpiredStatusCode matched |
| 401, no retry | No refresh token, wrong status field, or skip-refresh option somewhere | Operator checklist above |
Token URL invalid_grant | Refresh token revoked or never valid | Re-consent with offline+consent; do not hammer the resource API |
Do not “prove” it by pasting refresh_token into a ticket. If your security review needs evidence, screenshot the execution that retried, not the secret.
Why Google Authentication should be Body on generic OAuth2
Because Google’s token endpoint expects the refresh POST as form fields in the body, and n8n’s first-party Google credential already chooses Body. Generic OAuth2 defaults to Header (HTTP Basic for the client id/secret). Some vendors want Header. Google’s documented refresh request puts client_id, refresh_token, and grant_type in the body (HTTP/REST refresh).
If you copied Google’s authorize and token URLs into generic OAuth2 and left Authentication on Header, the code exchange might still work on a good day and the refresh POST can fail in a way that looks like “n8n does not refresh.”
| Setting | Generic OAuth2 default | Google built-in | Use for Google generic |
|---|---|---|---|
| Authentication | Header | Body (hidden) | Body |
| Auth URI Query Parameters | empty | access_type=offline&prompt=consent | Same string |
| Authorization URL | you type it | https://accounts.google.com/o/oauth2/v2/auth | That URL |
| Access Token URL | you type it | https://oauth2.googleapis.com/token | That URL |
Checklist:
- Authentication Body
- Query parameters include
access_type=offline - Query parameters include
prompt=consentif this Google account already consented once - Redirect URI in Google Cloud matches the n8n credential OAuth Redirect URL exactly, including
httpvshttpsand port (redirect mismatch) - You reconnect after changing Authentication. The stored tokens were minted under the old POST shape.
Header vs Body is not a style choice. It is which RFC client-auth method the token URL implements. Match the vendor. For Google, match n8n’s own Google credential.
Failure mode: Sign in works, then the first unattended hour dies
The consent screen succeeded. The first Gmail or Sheets call succeeded. The next scheduled run returns 401 / invalid_token / refreshToken is required. Nothing in n8n “expired the credential.” The access token died on Google’s clock and there was no refresh token to mint another.
What it costs: a pipeline that looks finished in the demo, then drops the first overnight batch. You reconnect at 9am. It works until the next access-token lifetime. Teams call that flaky n8n. It is a missing offline grant.
| What you observed | Likely mechanism hole | Fix |
|---|---|---|
| First hour green, then 401, no retry | No refresh_token in oauthTokenData | Extra query params + reconnect (generic); or switch to built-in Google |
| 401 then retry still 401 | Refresh POST failed (invalid_grant, wrong token URL, Header vs Body) | Capture the token URL error, not only the resource 401 |
| Dies every ~7 days, refresh did work until then | Google External + Testing grant life — n8n documents this as the app becoming unauthorized (troubleshooting) | Publish / Internal app; ops detail on the expiry post |
| Dies after you added a worker | Stale oauthTokenData overwrite or split encryption key | Same key; one writer |
| HTTP 200 with a vendor expiry code | Reactive refresh never saw a 401 | Do not wait for n8n to read the body |
What you do instead of a second reconnect:
- Stop using production as the experiment.
- Recreate the generic credential with offline+consent+Body, or use built-in Google.
- Prove unattended refresh in staging (previous section).
- Only then point production nodes at that credential.
A green Sign in is evidence you have an access token. It is not evidence you have a refresh token. Those are different rows in Google’s response.
Operators report a refreshToken is required error on Google nodes when the stored payload has no refresh token. Confirm the exact string on your n8n version; the meaning is the mechanism talking, not a Sheets outage. Extra config + reconnect, or switch to the built-in Google credential that already sends offline+consent. Do not raise HTTP Request timeout to “give refresh more time.” The helper either has a refresh token or it does not.
Queue workers and the encryption key — why refresh looks random
Refresh is a read-modify-write on an encrypted row. If worker A decrypts, refreshes, and writes, then worker B still holds a decrypted copy from boot with the old refresh token, B can write last and destroy A’s grant. If B cannot decrypt at all, B cannot refresh and the node errors on that worker only. Both look like “OAuth is flaky” in the execution list.
n8n’s documented rule: set N8N_ENCRYPTION_KEY for all workers in queue mode (custom encryption key). Newer encryption-key rotation (a second data key behind the instance key) is a feature-flagged deploy option (rotate encryption keys). If you have not turned that on, do not debug it. Debug the instance key first.
| Setup | Refresh write-back | Typical fail |
|---|---|---|
Single main process, key in ~/.n8n | Fine | Losing that file = losing the ability to decrypt every credential |
Queue mode, same N8N_ENCRYPTION_KEY on main + workers | Fine | Still avoid two apps racing Sign in on one Google client |
| Queue mode, workers missing the env var | Workers cannot read or persist tokens | Failures stick to whichever process drew the job |
| Two n8n environments, one Google OAuth client, both reconnecting | Google may invalidate older refresh tokens (Google caps live refresh tokens per user per client — refresh token expiration) | Staging Sign in kills production |
Checklist:
-
N8N_ENCRYPTION_KEYis set explicitly in production, not “whatever first boot wrote” - Main, webhook, and workers print the same key configuration path (do not print the key)
- Staging has its own Google OAuth client id
- You did not clone the SQLite file onto a second host without the key
Random 401s that follow the worker name in the log are encryption or write-back. They are not Google extra config. Fix the process topology before you add prompt=consent a third time.
What not to build instead of credential refresh
Do not build a workflow that watches the clock, calls Google’s token URL with a refresh token you pasted into a Set node, and writes the new access token into headers. That duplicates n8n’s credential helper, stores a secret on the canvas, and still loses rotations. n8n already POSTs grant_type=refresh_token when the generic path sees the expired status.
| Workaround | Why it shows up | Why you will regret it |
|---|---|---|
| Cron + HTTP Request to the token URL | “n8n does not refresh on expires_in” | You now own token persistence; the credential helper can fight you |
| Static data as the token cache | Copied from a forum thread | No encryption, no write-back into Credentials |
| Reconnect via the API on a schedule | Google Testing seven-day clock | You automated the symptom; see expiry-ops, do not put it here as a design |
| Continue on Fail on the OAuth node | Keep the graph green | Swallows the 401 that is the refresh signal |
| Token Expired Status Code = 200 | “maybe it will refresh on success” | You will refresh on every 200 |
Allowed exceptions are narrow:
- Vendor returns HTTP 200 with an expiry code — branch on the body and throw, so the helper or your error path can run. That is detection, not a second token store.
- Client Credentials — there is no refresh token. A new token fetch is the mechanism. Do not wrap it in Authorization Code fields.
If the vendor’s status never matches, change tokenExpiredStatusCode or throw on the body. If Google never sent refresh_token, change Auth URI Query Parameters and reconnect. Those are the two levers. A third workflow is not a third lever.
When should I hire vs DIY this?
DIY if you can finish the operator checklist on a staging credential in a sitting: correct grant, Google extra params if generic, Body for Google, reconnect, unattended proof after access-token death. Hire when the token lives across workers, multiple Google projects, or a vendor that does not return 401 — and you do not have time to capture raw responses.
The clock for a production rail is still access, staging, and hardening, not “add query params.” That clock is how long production automation takes. The bill is how much automation costs. This page does not replace either.
| Situation | DIY | Hire / audit |
|---|---|---|
| One Google node, built-in credential, single n8n process | Yes — Sign in, prove unattended | Only if Testing vs Production still confuses the team |
| HTTP Request + generic Google OAuth2 | Yes if you will set query params + Body and reconnect | If you need a custom Google scope n8n’s generic list rejects — read generic Google scopes before you assume HTTP Request is free |
| Queue mode, several workers, one shared Google client | Possible | Worth a review the moment staging Sign in can kill prod |
| Vendor 200 + body expiry, or Header-only token URL quirks | Possible if you can capture traffic | Hire if you cannot get a raw status/body |
| Service account is available and this is server-to-server | DIY the service account | Do not hire someone to “fix OAuth refresh” on a grant you should not be using |
Skip this week if you only have Execute Workflow on an unpublished graph. Generic refresh and Google offline access are production-credential problems. They do not show up honestly on a manual run you baby-sit with a consent screen still in cache.
If you cannot name who owns the Google Cloud client and who owns the n8n encryption key, you are not ready to DIY the mechanism. You are ready to lose the grant twice.
One-week DIY slice, mechanism only:
- Inventory: Google node vs HTTP Request vs service account.
- Built-in Google where it exists. Generic: offline+consent+Body, then Sign in.
- Staging unattended proof after
expires_in. - Encryption key check if you have more than one process.
- Stop. Alerts, pause, and Testing calendars are the other post.
That is a credential sitting, not a product sprint. If step 3 fails and you cannot capture token-URL vs resource-URL errors, that is the hire signal — not “add AI to classify 401s.”
FAQ
How does n8n refresh OAuth access tokens?
It reads oauthTokenData from the encrypted credential, calls the token URL with grant_type=refresh_token when the resource API returns tokenExpiredStatusCode (default 401 on generic OAuth2), writes the new payload back, and retries. Google must have issued a refresh token first, which requires access_type=offline on the authorize request. Built-in Google OAuth2 already sends access_type=offline&prompt=consent; generic OAuth2 needs those Auth URI Query Parameters and a reconnect.
How do I measure whether does n8n refresh OAuth access tokens is working?
Measure an unattended call after the access token is dead: a 2xx, or a 401 followed by a retry 2xx in the same execution. Use the vendor expires_in you actually received as the wait, not a made-up studio interval. A green Sign in only proves an access token. A token-URL invalid_grant means the refresh grant failed, which is a different measurement than a resource 401.
What usually fails first when teams try this?
Google generic OAuth2 without access_type=offline (and without prompt=consent on a re-auth), so no refresh token is stored. Next is a vendor that expires tokens with 403 or a 200 body while n8n still waits for 401. Third is queue workers missing N8N_ENCRYPTION_KEY, so write-back never lands. Reconnecting without fixing those three repeats the same hour of success.
How long does this take to show results?
The extra Google fields take minutes. Proof takes one access-token lifetime — often about an hour on Google token responses, but use the expires_in you got. If you only reconnect and leave, you will think it is fixed until the next expiry. A staging credential that survives that wait is the result. Publishing a Google Testing app so the refresh grant itself lasts is a separate, longer Cloud Console change.
What should I skip if I only have a week?
Skip building a Cron that hits the token URL yourself, and skip dumping oauthTokenData onto the graph. Skip production Sign ins while generic Google still has Header auth and empty query params. Do pick built-in Google where it exists, or set offline+consent+Body, reconnect, and prove one unattended retry. Leave Testing-vs-Production and alert design to the expiry-ops post if that is all the week has room for after proof.
When is this not worth doing yet?
When the API key or service account is the real grant and you are forcing user OAuth anyway. When the workflow is still unpublished and every run is a manual Execute with a fresh consent. When you cannot capture a dead-token HTTP status. Fix hosting and a single staging credential first; the mechanism does not exist until n8n can persist oauthTokenData and the vendor can return a refresh token.
CTA
Get the refresh token issued, stored, and proven on a 401 retry — extra Google query params on generic OAuth2, then reconnect. Do not schedule a second graph to impersonate the credential helper.
Keep the handbook open for the rest of the spine. For a credential-mechanism review on your n8n estate, use automation or book the audit.
What questions does this article answer?
- How does n8n refresh OAuth access tokens?
- It reads `oauthTokenData` from the encrypted credential, calls the token URL with `grant_type=refresh_token` when the resource API returns `tokenExpiredStatusCode` (default 401 on generic OAuth2), writes the new payload back, and retries. Google must have issued a refresh token first, which requires `access_type=offline` on the authorize request. Built-in Google OAuth2 already sends `access_type=offline&prompt=consent`; generic OAuth2 needs those Auth URI Query Parameters and a reconnect.
- How do I measure whether does n8n refresh OAuth access tokens is working?
- Measure an unattended call after the access token is dead: a 2xx, or a 401 followed by a retry 2xx in the same execution. Use the vendor `expires_in` you actually received as the wait, not a made-up studio interval. A green Sign in only proves an access token. A token-URL `invalid_grant` means the refresh grant failed, which is a different measurement than a resource 401.
- What usually fails first when teams try this?
- Google generic OAuth2 without `access_type=offline` (and without `prompt=consent` on a re-auth), so no refresh token is stored. Next is a vendor that expires tokens with 403 or a 200 body while n8n still waits for 401. Third is queue workers missing `N8N_ENCRYPTION_KEY`, so write-back never lands. Reconnecting without fixing those three repeats the same hour of success.
- How long does this take to show results?
- The extra Google fields take minutes. Proof takes one access-token lifetime — often about an hour on Google token responses, but use the `expires_in` you got. If you only reconnect and leave, you will think it is fixed until the next expiry. A staging credential that survives that wait is the result. Publishing a Google Testing app so the *refresh* grant itself lasts is a separate, longer Cloud Console change.
- What should I skip if I only have a week?
- Skip building a Cron that hits the token URL yourself, and skip dumping `oauthTokenData` onto the graph. Skip production Sign ins while generic Google still has Header auth and empty query params. Do pick built-in Google where it exists, or set offline+consent+Body, reconnect, and prove one unattended retry. Leave Testing-vs-Production and alert design to the expiry-ops post if that is all the week has room for after proof.
- When is this not worth doing yet?
- When the API key or service account is the real grant and you are forcing user OAuth anyway. When the workflow is still unpublished and every run is a manual Execute with a fresh consent. When you cannot capture a dead-token HTTP status. Fix hosting and a single staging credential first; the mechanism does not exist until n8n can persist `oauthTokenData` and the vendor can return a refresh token.
Last reviewed
Automation
Automation After the show is not you at 1 a.m.
Post-show onboarding — thank-you, join path, merch nudge — belongs in a human-gated n8n rail, not your thumb at load-out.
Automation Paperwork that is not the plant
Invoice and PO matching, intake, and support triage in n8n with Metrc fences — the paperwork operators hate, not a menu widget.
Automation Saturday still books — the missed-call rail for trades
A missed-call text-back that routes zip and books a slot beats voicemail and Saturday desk coverage you cannot keep staffed. If a kid is cheaper, say so.
Automation Why doesn’t worker concurrency cap my n8n sub-workflows
Worker concurrency does not cap n8n sub-workflows. Each Execute Workflow child is a new execution the production limit skips, usually on the parent worker.
Will's Journal in your inbox.
What I learned this week building for shops, floors, and houses.
You're on the list.
Sign-up failed — try again.
By subscribing, you agree to the Privacy Policy.