Skip to content

Remote-control transport couples iOS latency to shared queues and backpressure #27592

Open

Description

@hlky

Summary

Remote control between ChatGPT iOS and Codex Desktop is currently internet-mediated, and the app-server transport layer has at least two queue-coupling points that can turn ordinary WAN jitter into very visible mobile lag.

During investigation and local testing, I found one concrete root cause for slow iOS -> desktop steering and one remaining root cause for desktop -> iOS latency / reliability under load.

A prototype fix for the inbound path exists locally on branch codex/remote-control-reliability-fixes at commit 782f5ea585.

Related broad symptom report: #22773

Current architecture note

This path is not currently a direct LAN/local-network transport. The iOS <-> desktop control path rides the remote-control internet/websocket flow, so transport stalls are amplified by WAN variance.

That does not explain the full severity of the lag by itself. There are also transport-layer blocking points in the desktop host.

Root cause 1: inbound iOS -> desktop steering was serialized behind app-server queue waits

Before the local fix, the remote-control websocket reader held the ClientTracker lock for the whole read loop and then awaited handle_message(...), which could await transport_event_tx.send(...) for up to 30 seconds.

That meant:

  • one blocked IncomingMessage forward could stop the reader from consuming subsequent websocket frames
  • steering messages from iOS could sit behind a bounded app-server queue instead of being read immediately
  • pong traffic was also delayed behind the same serialized path

Relevant code paths:

  • codex-rs/app-server-transport/src/transport/remote_control/websocket.rs
  • codex-rs/app-server-transport/src/transport/remote_control/client_tracker.rs

Observed effect

After splitting websocket frame reads from transport-event forwarding with an internal inbound inbox, steering from iOS to desktop became effectively immediate in local testing.

Root cause 2: outbound desktop -> iOS routing can still head-of-line block unrelated traffic

The remote-control reliability change switched remote-control outbound delivery from disconnect-on-full to wait-on-full. That is directionally correct for reliability, but it is currently awaited inline from the single outbound routing loop.

That means one slow remote-control writer can hold up the shared app-server outbound router instead of just slowing that one mobile client.

Relevant code paths:

  • codex-rs/app-server/src/transport.rs
  • codex-rs/app-server/src/lib.rs

Why this matters

This turns per-client backpressure into global latency. Under load, a slow iOS remote-control stream can delay notifications/responses for unrelated connections.

Remaining design risk: message-slot accounting instead of byte accounting

Remote-control queue/buffer capacity is now much larger, which helps avoid hard disconnects, but capacity is still counted in message slots rather than bytes.

That means a burst of large envelopes can still:

  • consume a lot of memory
  • create bursty latency instead of clean flow control
  • hide overload until the process is already buffering too much work

Relevant code paths:

  • codex-rs/app-server-transport/src/transport/remote_control/mod.rs
  • codex-rs/app-server-transport/src/transport/remote_control/websocket.rs

Suggested follow-up

  1. Keep the inbound reader/forwarder split so websocket reads are never blocked on transport_event_tx backpressure.
  2. Change remote-control outbound routing so a full remote-control writer does not block the shared outbound router for unrelated clients.
  3. Consider byte-based accounting (or at least hybrid byte + message caps) for remote-control outbound buffering.
  4. Add field metrics/logging for:
    • transport-event queue depth
    • remote-control writer queue depth
    • unacked envelope count
    • ACK latency
    • disconnect / timeout reason
  5. Decide explicitly whether the product should eventually support a LAN-short-circuit transport for local iOS <-> desktop control, since the current architecture always pays internet-path latency.

Validation

The inbound prototype fix passed focused tests in codex-app-server-transport, and local manual testing showed a clear improvement in iOS -> desktop steering latency.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    app-serverIssues involving app server protocol or interfaces
    on Jun 11, 2026
  2. github-actions commented on Jun 11, 2026

    @github-actions
    Contributor

    Potential duplicates detected. Please review them and close your issue if it is a duplicate.

    Powered by Codex Action

  3. hlky commented on Jun 11, 2026

    @hlky
    Author

    Note: I am currently using a build including the branch mentioned, so it is tested in practice, and I can confirm that both inbound and outbound latency is massively reduced.

  4. disco32r commented on Aug 4, 2026

    @disco32r

    Additional concrete outbound failure mode (0.146.0; present on current main)

    I reproduced a narrower mechanism behind the unresolved outbound head-of-line blocking described above. This is separate from the inbound reader/forwarder issue.

    In current websocket.rs:

    • BoundedOutboundBuffer::insert increments one global replay-occupancy counter for every retained envelope; occupancy is released by ACK processing.
    • The single run_server_writer_inner receiver drains server_event_rx only while that global occupancy is below CHANNEL_CAPACITY (128).
    • One dequeued event can be split into multiple envelopes before insertion, so retained occupancy can reach or exceed 128. Replay is keyed by (ClientId, StreamId), but the receive gate is global: one client that stops acknowledging can prevent the shared receiver from serving unrelated clients and streams.
    • Large history-bearing responses were the observed trigger in my workload, including thread/read and thread/resume. The transport segments generic server envelopes with a 100 KiB target and 150 KiB maximum; those methods are examples, not transport special cases.
    • ClientClosed cleanup runs after client_tracker.handle_message(...) succeeds and invalidates inbound reassembly/deduplication state for the specified stream, or for the whole client when no stream is supplied. Neither branch purges outbound replay entries.

    The trigger is workload-dependent: remote control, a history-heavy response, multiple clients, and one stalled ACK stream. That explains why a clean install or a light thread may not reproduce it.

    A local 0.146.0 mitigation restored normal concurrent-client progress by decoupling live delivery from replay ACK occupancy, imposing global and per-stream replay bounds, and purging replay on explicit client/stream closure. I would not submit that local patch verbatim: current main should also cover worker-exit, idle-expiry, and same-stream reinitialization paths, and segmented-message eviction should remove a complete sequence rather than retain unusable trailing chunks.

    A focused upstream change should:

    1. Continue draining the already-bounded server_event_rx regardless of replay ACK latency, while preserving producer-channel and WebSocket-sink backpressure.
    2. Define an explicit replay-overflow contract—eviction, disconnect, or error behavior—and preserve sequence/reconnect semantics.
    3. Bound retained replay globally and per stream, ideally with byte or hybrid accounting, so one stalled stream cannot monopolize it.
    4. Purge replay on explicit ClientClosed and every other client/stream teardown path.
    5. Add regressions covering cross-client isolation at saturation, exact-stream versus whole-client cleanup, reconnect ordering, safe ACK-after-eviction, and whole-sequence handling for segmented messages.

    If this direction aligns with the maintainers' intended solution, would a Codex maintainer explicitly invite a focused PR against main? I would scope it to codex-app-server-transport and exclude local updater/pinning hardening.

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

    appIssues related to the Codex desktop appapp-serverIssues involving app server protocol or interfacesbugSomething isn't workingperformanceremote

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions