Race condition in OAuth token refresh causes "refresh token was already used" error when multiple app-server instances run concurrently
What version of Codex is running?
0.93.0
What subscription do you have?
team
Which model were you using?
gpt-5.2
What platform is your computer?
Darwin 22.6.0 x86_64 i386
What terminal emulator and version are you using (if applicable)?
_No response_
What issue are you seeing?
When multiple codex app-server instances are running simultaneously (e.g., multiple VS Code/Windsurf windows), a
race condition can occur during OAuth token refresh, resulting in the error:
Your access token could not be refreshed because your refresh token was already used. Please log out and sign in
again.
What steps can reproduce the bug?
Root Cause Analysis
The token refresh mechanism in codex-rs/core/src/auth.rs lacks cross-process synchronization:
- No file lock when refreshing tokens (auth.rs:1251-1267)
- No distributed lock or mutex across processes
- No check if another process has already refreshed the token before calling the refresh API
Since OAuth uses Refresh Token Rotation (each refresh token is single-use), when multiple processes detect token
staleness simultaneously and race to refresh:
T0: app-server-1 detects token needs refresh
T0: app-server-2 also detects token needs refresh
T1: app-server-1 calls refresh API → gets new refresh_token_v2, writes to disk
T2: app-server-2 calls refresh API with stale refresh_token_v1
T3: app-server-2 receives "refresh_token_reused" error ❌
What is the expected behavior?
_No response_
Additional information
Steps to Reproduce
- Open multiple Windsurf/VS Code windows (each spawns a separate app-server process)
- Wait until token refresh is due (~8 days after last refresh, per TOKEN_REFRESH_INTERVAL)
- Trigger API calls from multiple windows around the same time (e.g., restart IDE, or use a tool like CodexBar
that polls via RPC)
Environment
- macOS (likely affects all platforms)
- Multiple codex app-server --analytics-default-enabled processes running
Suggested Fix
Add file-based locking before token refresh:
// Before refreshing, acquire a file lock
let lock_path = storage.path().with_extension("refresh.lock");
let lock = FileLock::acquire(&lock_path)?;
// Re-check if token still needs refresh (another process may have refreshed it)
let current_tokens = storage.load()?;
if !needs_refresh(¤t_tokens) {
return Ok(()); // Already refreshed by another process
}
// Proceed with refresh
let refresh_response = try_refresh_token(refresh_token, client).await?;
update_tokens(storage, refresh_response)?;
Alternatively, consider using a single shared app-server process (singleton pattern) instead of spawning one per IDE
window.
4 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Thanks, but I think there's a hole in your analysis that makes the diagnosis incorrect. Our OAuth refresh mechanism allows a refresh token to be used multiple times over a limited time window (on the order of an hour) before it is permanently invalidated. This accommodates network flakiness and other similar conditions. You're correct that the lack of a file lock can result in a race between Codex instances, but this race is mitigated by the refresh behavior on the server. In step T3 in your above analysis, app-server-2 would not receive a "refresh_token_reused" error in this situation. It would receive a proper access token / refresh token pair. One of the two instances would succeed in writing their updated token pair to the file.
We have received occasional reports of the "refresh token reused" error, so there is likely a bug lurking somewhere still. But I don't think the situation you've outlined above explains the problem. Let me know if you have any other theories or clues.
+1
@etraut-openai seeing the same issue here, uploaded logs and left it in state if you need me to do further repros. Thread id 019c2aa3-984c-79b1-9a8c-11598c0b1e79