108Implement a single-flight token refresh mechanism: 10 failed requests trigger only one refresh, followed by safe retries
Problem
A dashboard fires 10 requests at once just as the access token expires. All 10 get 401 Unauthorized. A naive client refreshes the token 10 times. Refresh tokens are often single-use, so the second refresh fails and the user is logged out.
Implement createAuthClient({ request, refreshToken, initialToken, onAuthFailure = () => {} }) returning { fetch(url), token }:
request(url, token)performs the call and resolves to{ status, data }.refreshToken()resolves to a new access token (or rejects).
Rules for client.fetch(url):
- Send the current token. If the status is not 401, return the response.
- On 401, refresh at most once at a time (single-flight): concurrent 401s all wait for the same refresh, then retry with the new token.
- If a request failed with an old token that has already been replaced (the refresh finished while it was in flight), retry with the current token without refreshing again.
- Retry each request only once. A second 401 rejects with an
Errorwhosestatusis 401. - If the refresh fails, every waiting request rejects with the refresh error, and
onAuthFailure(error)is called once for that failed refresh. - After a refresh settles, a later expiry can trigger a new refresh.
Try it yourself
Write your solution, run it, then submit to check it against the tests.
Press Run or ⌘ Enter to execute your code.
Step by step
Build the solution one idea at a time.
- 1
Remember which token each request used
Read the current token into a local variable before sending. When a 401 comes back, compare it with the current token: if they differ, someone already refreshed, so just retry.
- 2
Single-flight the refresh
Keep
refreshing, a promise ornull.refreshOnce()returns the existing promise if a refresh is in flight; otherwise it starts one, stores the new token on success, and infinallysetsrefreshingback tonull, so a future expiry can refresh again. - 3
Report a failed refresh once
Attach
onAuthFailureinsiderefreshOnce, on the shared promise, so it runs once per failed refresh no matter how many requests were waiting. Each waiting request still rejects with the error. - 4
Retry exactly once
After waiting for the refresh, send the request again with the new token. If that also returns 401, give up with an error that has
status = 401. Without this limit, a server that keeps returning 401 (for example a revoked session) causes an infinite loop.
Approach
Cache the in-flight refresh promise so every concurrent 401 awaits the same refresh, compare the token each request used with the current one to avoid needless refreshes, and cap retries at one.
Solution
function createAuthClient({ request, refreshToken, initialToken, onAuthFailure = () => {} }) {
let token = initialToken;
let refreshing = null;
function refreshOnce() {
if (!refreshing) {
refreshing = Promise.resolve()
.then(() => refreshToken())
.then(
(newToken) => {
token = newToken;
return newToken;
},
(error) => {
onAuthFailure(error);
throw error;
},
)
.finally(() => {
refreshing = null;
});
}
return refreshing;
}
async function authFetch(url) {
const usedToken = token;
const response = await request(url, usedToken);
if (response.status !== 401) return response;
// Only refresh if nobody replaced the token since this request was sent.
if (usedToken === token) await refreshOnce();
const retry = await request(url, token);
if (retry.status === 401) {
const error = new Error(`Unauthorized: ${url}`);
error.status = 401;
throw error;
}
return retry;
}
return {
fetch: authFetch,
get token() {
return token;
},
};
}Complexity
Time: at most one refresh per expiry, and at most two requests per call. Space: O(1).
Edge cases
- Ten concurrent 401s cause exactly one refresh and ten retries.
- A slow request that returns 401 after the refresh completed retries with the new token and does not refresh again.
- A refresh failure rejects every waiting call and calls
onAuthFailureonce. - A request that is still 401 after refreshing is rejected instead of looping.
- Once a refresh has finished, the next expiry starts a new one.
Follow-up questions
Answer each one out loud first, then reveal the model answer.