Token refresh explorer: what happens when many requests get a 401
An access token expires while the app has several requests in flight. The app must refresh it. Change one setting and see how many refresh calls the app makes and whether the user is logged out. This is a simplified model. Its numbers are my assumptions.
The idea
- The app sends an access token with every request. It is short-lived, for example 15 minutes.
- When it expires, the server answers 401. The app sends its refresh token to get a new access token.
- One screen can start many requests. When the token expires, they can all get a 401 at the same time.
- If each request refreshes by itself, the server sees the same refresh token many times.
- With rotation, every refresh gives a new refresh token and the old one stops working. If the server sees an old one again, it treats it as stolen and ends the whole session. This is called reuse detection.
- With single-flight, the first 401 starts one refresh call. The other requests wait and share its result.
- Two failures break the rotation even with single-flight. The app can be killed before it saves the new refresh token. The reply can be lost, so the app never gets the new token.
- A short grace window lets the server accept the old token once more for a few seconds.
- Only
invalid_grantends a session. A timeout or a 5xx error does not. The session is then unconfirmed: keep the tokens and try again later.
This is a simplified model. Every number in it is my assumption, not a rule of any server or standard. Real servers differ, so check yours.
A suggested order:
- Run the first preset. Rotation ends the session.
- Run the second preset. One shared refresh fixes it.
- Run the third preset. The refresh works, but a kill before the save still logs the user out later.
- Run the fourth preset. Saving first and a grace window recover from a lost reply and a kill.
- Then change one control at a time.
Try it
Result
Press "Run the simulation" or pick a preset.
The counts cover 60 minutes of steady use. The access token expires many times in that time. If the user is logged out, the run stops there.
Timeline of the first expiry
- Press "Run the simulation" or pick a preset.
What this shows
- Run the simulation to see what it shows.
Without JavaScript
The controls in Try it need JavaScript. If JavaScript is off, use this table. It shows what the four presets give over 60 minutes with a 15-minute lifetime. With JavaScript on, you can change every setting.
| Preset | What happens | Refresh calls | Requests that succeeded | User logged out |
|---|---|---|---|---|
| 10 requests, no single-flight | Ten requests start ten refresh calls. The first swaps the token. The second sends the old one, and the server ends the session. | 10 | 1 of 10 | Yes |
| Single-flight (safe) | One call serves all ten requests. It repeats at each of the 4 expiries. | 4 | 40 of 40 | No |
| Killed mid-refresh, saved after use | The refresh and all ten requests work. The app is killed before it saves the new token. At the next start it sends the old one. | 2 | 10 of 10 | Yes |
| Reply lost, new token written first | The first call is swapped on the server but its reply is lost. The retry is accepted by the grace window. The app saved the new token first, so the kill does no harm. | 6 | 40 of 40 | No |
How the model works
The rules this tool uses. They are common practice, in a simplified form:
- A request with an expired access token gets a 401. The app then calls the refresh endpoint with its refresh token.
- With rotation, a refresh gives a new access token and a new refresh token. The old refresh token stops working.
- Reuse detection: if the server gets an old refresh token again, it ends the whole session. It answers
invalid_grant. - Access tokens that were already issued keep working until they expire. Ending the session only stops refreshing.
- A grace window: the server accepts the token it just replaced, once more, for a short time. It answers with the current refresh token.
- Only
invalid_grantends a session. A timeout or a 5xx leaves it unconfirmed. The app keeps its tokens and retries. - Single-flight: while one refresh is running, every other request waits for it and uses its result.
- Save first: the app writes the new refresh token to disk, then uses the new access token. Save after: it uses the token first and writes it later. A kill in between loses the new token. The access token lives in memory only.
My own choices for this tool, not from any standard:
- One network hop takes 0.3 s.
- A timeout is 10 s. After a timeout the app retries once, 2 s later, with the refresh token it still has.
- The grace window is 30 s. Real servers use other values, and some have none.
- After a kill, the user opens the app again 60 s later. That is longer than the grace window.
- The reply is lost for the first refresh call at the first expiry. The server has already processed that call. The app is killed once, after the first refresh finishes.
- The server answers parallel refresh calls in the order they were sent.
- The session is 60 minutes of steady use. The app sends the same burst of requests at each expiry.
- The refresh token does not expire. The clocks are exact. There is one device.
What it leaves out: refreshing a little before the token expires, refresh tokens that expire, several devices, a 5xx error on the refresh call, being offline, clock errors and background work.
Read more
- Chat app case: the send path, where a 401 means refresh once, then retry
- Retry explorer: what a lost reply does to a chat message
- Idempotency keys on mobile