Skip to content

[Bug] The restart drain fence refuses new requests for the full 60s while Codex exhausts its retries in about six seconds #6643

Closed

Description

@tone-leh

Client or integration

Codex CLI

Area

Service lifecycle

Summary

The restart handoff drains for up to MEMORY_DRAIN_RESTART_MS (60s). The intent is to let an in-flight turn finish, but during that whole window every NEW data-plane request is refused, and Codex spends its five sampling retries in roughly six seconds. So the client gives up well before the replacement is listening, and the turn fails even though the restart itself is quick.

Measured on this machine (2026-09-26): with the 60s default, a 42-second fence failed the turn with unexpected status 503 - and that was already after the refusal stopped being a fatal capacity error (see the related issue below). A two-second cap, plus the replacement's roughly one-second bind gap, passed. In other words the fence length, not the refusal itself, is what the client runs out of.

Reproduction

  1. ocx start --port 10100.
  2. Start a Codex turn.
  3. Trigger the drain-and-restart path (dashboard POST /api/system/restart, or ocx service restart) while the turn is in flight.
  4. Watch the turn: the client retries against a listener that is refusing for the full fence, exhausts its retries, and ends with unexpected status 503 instead of continuing against the replacement.

Version

2.78.0

Operating system

macOS 27.0.1 (arm64). The fence is shared code, so this should apply on every platform.

Provider and model

Not provider-specific.

Logs or error output

Source location: src/server/management/system-restart.ts - the deadline is set at line 366 (const restartDeadlineMs = now() + MEMORY_DRAIN_RESTART_MS;) and consumed at line 393 (const remainingMs = Math.max(0, restartDeadlineMs - now());), which is then handed to drain(undefined, remainingMs).

Suggested change

Bound the restart fence rather than the drain - keep the drain free to finish an in-flight turn, but stop refusing new requests for longer than the client can retry:

const RESTART_HANDOFF_INFLIGHT_WAIT_MS = 2_000;
const remainingMs = Math.min(
  Math.max(0, restartDeadlineMs - now()),
  RESTART_HANDOFF_INFLIGHT_WAIT_MS,
);

A turn cut by this cap is retried by the client against the replacement that the same handoff starts, which is why two seconds is enough: the gap it has to cover is the replacement's bind, not the full drain.

This is what we run as a local patch. Happy to open a PR if you would rather review it that way.

Related: the code carried by the refusal is the other half of this handoff problem, filed separately as #6642.

Screenshots and supporting files

None.

Redacted configuration

{ "hostname": "127.0.0.1", "port": 10100 }

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. added
    bugSomething isn't working
    serviceService lifecycle (WinSW/launchd/scheduler)
    on Oct 5, 2026
  2. Ingwannu commented on Oct 6, 2026

    @Ingwannu
    Owner

    Confirmed the current source behavior at dev f924820652: manual restart begins refusing new data-plane traffic before the flush delay, and passes the remaining shared 60-second restart budget to drain/shutdown. The configured bound is confirmed; your measured Codex retry window and successful two-second local patch remain reporter evidence, not measurements I repeated.

    A focused PR is welcome, but a blanket shorter drain can interrupt an already-sent turn whose execution outcome is unknown. Please cover completed versus still-active/ambiguous turns, delayed replacement readiness, cancellation/explicit stop, automatic versus manual restart, and failure recovery using synthetic clocks/handlers. Preserve the no-automatic-replay rule for uncertain upstream execution; a replacement becoming ready does not prove resending an interrupted request is safe. Document the tradeoff instead of promising that every cut turn can retry safely.

    #6642 is the separate client error-code question. I have not restarted the service or changed this timeout; retain owner review of the shared lifecycle policy.

  3. lidge-jun commented on Oct 7, 2026

    @lidge-jun
    Owner

    #6686 made the drain response retryable. Shortening the 60 s fence by default can interrupt long turns, so the follow-up would be an explicit opt-in grace on the restart API; held until then.

  4. lidge-jun commented on Oct 8, 2026

    @lidge-jun
    Owner

    Resolved on dev by #6724 (a12bb1f820), which carries #6703. The default 60 s restart drain is unchanged. An authenticated caller can now pass {"drainGraceMs": <1..60000>} to POST /api/system/restart for a shorter grace, so a restart doesn't hold Codex's retries behind the full 60 s fence. A cut turn may already have run upstream and is not replayed automatically. Closing manually because PRs target dev.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingserviceService lifecycle (WinSW/launchd/scheduler)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions