unslothai/unsloth · warning · CodexAuthError

ChatGPT credential update is busy. Please retry.

Error message

ChatGPT credential update is busy. Please retry.

What it means

CodexAuthError raised by provider_oauth_write_guard when acquiring the cross-worker file lock for a provider's OAuth credentials times out (default 30s, acquired off the event loop via asyncio.to_thread). The guard serializes ChatGPT credential refresh and deletion across Studio workers; a concurrent long refresh holds the lock, and instead of blocking indefinitely the waiter reports 'busy, please retry'.

Source

Thrown at studio/backend/core/inference/openai_codex_auth.py:116


def _provider_file_lock(provider_id: str) -> FileLock:
    lock_name = hashlib.sha256(provider_id.encode()).hexdigest()[:24]
    return FileLock(
        str(studio_db_path().parent / f".openai-codex-refresh-{lock_name}.lock"),
        timeout = 30,
        thread_local = False,
    )


@asynccontextmanager
async def provider_oauth_write_guard(provider_id: str):
    """Serialize refresh and deletion across Studio workers without blocking the event loop."""
    file_lock = _provider_file_lock(provider_id)
    try:
        await asyncio.to_thread(file_lock.acquire)
    except FileLockTimeout as exc:
        raise CodexAuthError("ChatGPT credential update is busy. Please retry.") from exc
    try:
        yield
    finally:
        await asyncio.to_thread(file_lock.release)


def _flow_is_stale(flow: OAuthFlow, now: float) -> bool:
    if flow.status == "pending":
        return now >= flow.expires_at
    return now >= min(flow.expires_at, flow.created_at + _FLOW_TERMINAL_RETENTION_SECONDS)


async def _prune_flows() -> None:
    now = time.time()
    for flow_id, flow in list(_flows.items()):
        if _flow_is_stale(flow, now):
            await cancel_flow(flow_id)

View on GitHub (pinned to 203007d190)

Solutions

  1. Retry the operation after a short backoff — once the in-flight refresh finishes, the lock frees and the cached token is likely already fresh.
  2. Debounce refresh at the call site: check token expiry with margin and only one caller refreshes (single-flight), so workers do not pile on the lock.
  3. If it recurs constantly, investigate why the lock holder exceeds 30s (network to the token endpoint) and raise the timeout or fix connectivity.

Example fix

# before
async with provider_oauth_write_guard(provider_id):
    await refresh_credentials(provider_id)  # raises immediately under contention

# after
for attempt in range(3):
    try:
        async with provider_oauth_write_guard(provider_id):
            await refresh_credentials(provider_id)
        break
    except CodexAuthError:
        await asyncio.sleep(2 * (attempt + 1))
Defensive patterns

Strategy: retry

Try / catch

for attempt in range(3):
    try:
        async with provider_oauth_write_guard(provider_id):
            return await refresh(provider_id)
    except CodexAuthError as e:
        if 'busy' in str(e) and attempt < 2:
            await asyncio.sleep(1.5 * (attempt + 1))
            continue
        raise

Prevention

When it happens

Trigger: Two or more workers/requests simultaneously refresh or delete the same provider's ChatGPT credentials; the first holds the lock longer than the 30s timeout (slow token endpoint), and the second's file_lock.acquire raises FileLockTimeout.

Common situations: Burst of requests right after access-token expiry, all triggering refresh; a hung/slow OpenAI auth endpoint during an outage; a previous worker crashed mid-refresh leaving contention behind (the lock eventually times out).

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/ea508df863d69f1c. Report an issue: GitHub.