Findings
Browser audio: the uplink must answer before the call ends
Date: 2026-10-11
What we saw#
Voice calls through the public workbench ended as audio_lost after about two
minutes. The voice client logged the uplink settling with status 524, no
ended event, after 136 s, with a Cloudflare error page as the body. The same
calls against an in-cluster workbench, with no proxy in the path, did not drop.
Why#
The uplink is one chunked POST held open for the whole call. Workbench sent its
response (200 {played_ms}) only when the request body ended or the session
ended, because that is when the result is known. Cloudflare's proxy gives up on
an origin that has not sent response headers within 100 s and answers 524 to
the client, which closes the uplink. Any call longer than that died, whatever
the audio was doing. The SSE downlink was never affected: its headers go out at
once and a ping follows every 15 s.
Clear, reader drops, reloads and the server's own timeouts were ruled out
first: Fastify sets requestTimeout and the socket timeout to 0, and an
in-process probe of 50 clears, a silent gap and a reader reconnect left the
uplink open.
What changed#
The uplink now answers 200 with Content-Type: application/json as soon as
it is accepted, writes a newline every 20 s (UPLINK_HEARTBEAT_MS), and ends
with {"played_ms"} exactly as before. Leading whitespace is valid JSON, so a
client that parses the body as one document still works; a client that treats
the response ending as the end of the uplink also works. Refusals (409, 404,
415) are still decided before any header goes out.
When the session ends mid-upload, the response is ended and the socket is
destroyed. A Connection: close header can no longer be chosen at that point,
since the headers are already sent, and keep-alive would otherwise read the
rest of an endless body. The socket has to be captured from the request:
res.socket is already detached by the time end's callback runs.
Limits#
- Cloudflare also caps request bodies by plan (100 MB on Free and Pro). One uplink per call at 24 kHz reaches that after about 35 minutes. Longer calls need the audio routes on a DNS-only hostname, or a client that rotates its uplink.
- A proxy that buffers the whole request body would still break the uplink. The smoke test heard agent audio in real time through Cloudflare, so its proxy streams it.