Race condition in OAuth token refresh causes "refresh token was already used" error when multiple app-server instances run concurrently

Resolved 💬 4 comments Opened Feb 1, 2026 by xiaojay30 Closed Feb 2, 2026

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:

  1. No file lock when refreshing tokens (auth.rs:1251-1267)
  2. No distributed lock or mutex across processes
  3. 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

  1. Open multiple Windsurf/VS Code windows (each spawns a separate app-server process)
  2. Wait until token refresh is due (~8 days after last refresh, per TOKEN_REFRESH_INTERVAL)
  3. 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(&current_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.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗