OAuth "invalid_grant" and you can't refresh the token? Why re-authentication is the only fix
Your background job goes to refresh an OAuth access token and gets {"error": "invalid_grant"}. You retry. Same error. You retry with backoff. Same error. That's the tell: invalid_grant is not transient — the grant is dead, and no amount of retrying brings it back. The only fix is a new grant, which means re-authenticating.
What invalid_grant actually means
OAuth invalid_grant is the server saying the credential you presented — an authorization code or a refresh token — is no longer usable. The reasons cluster:
- Expired or revoked refresh token. The token aged out, or was revoked by a password change, an admin action, inactivity, or the user un-authorizing your app.
- Single-use code reused. Authorization codes are one-time. Exchanging the same code twice is treated as an attack and typically revokes the issued tokens.
- Concurrent-refresh race. Two requests tried to refresh at the same moment; the first rotated the refresh token, so the second presented one that's already been superseded —
invalid_grant. - Client / redirect mismatch. The grant doesn't match the client_id, secret, or redirect_uri you're presenting.
The defining property: you can't refresh out of an invalid_grant. Unlike an expired access token (which a refresh fixes), a dead grant requires starting over — send the user through the authorization flow to obtain a fresh grant.
How to handle it
- Don't retry the refresh. Detect
invalid_grantand stop — retrying is guaranteed to fail and can look like abuse. - Re-authenticate. Trigger the authorization flow so the user grants access again; store the new refresh token.
- Serialize refreshes. Use a lock or single-flight so two requests never refresh the same token concurrently — that alone eliminates the race cause.
- Handle rotation correctly. If the provider rotates refresh tokens, always persist the new one from each refresh response, and never reuse the old.
For agents with long-lived integrations
An agent running unattended will hit invalid_grant eventually — tokens get revoked, refreshes race. Treating it as retryable wastes calls and never recovers. diagnose_infra_error returns the oauth family with the correct handling, so the agent knows to stop refreshing and escalate to re-authentication instead of looping:
diagnose_infra_error({error: "invalid_grant refresh token"})
// → { family: "oauth",
// fix: "can't refresh out of invalid_grant — re-authenticate; serialize refreshes to avoid the race" }Machine-readable guide: https://snapback.sh/llms.txt · Tool discovery: https://snapback.sh/.well-known/mcp.json · MCP endpoint: https://api.snapback.sh/mcp. Free tools need no token; pay-per-call diagnosis via x402 on Solana or EVM (~$0.0032 USDC).