App: shine-music-school (org personal)
Region: ams — app and MPG cluster co-located
MPG cluster: 9g6y30wgm9krv5ml, PostgreSQL 16.14
Machines: 801935a66d16d8, d8d5d46b4d9438 (2 × shared-1x, 2 GB)
Client: psycopg 3.3.4 + psycopg_pool, min 3 / max 10 per machine, direct MPG
endpoint (no PgBouncer), statement_timeout=5000, lock_timeout=2000,
keepalives 1/30/10/3, tcp_user_timeout=10000
Summary
On two endpoints only, a request occasionally acquires a healthy pooled
connection, sends a query, and never receives a response. The TCP socket stays
established and the peer keeps acknowledging at the network level. Nothing
server-side ends it: statement_timeout cannot fire because the server appears
never to have received the statement, and tcp_user_timeout cannot fire because
the bytes are being acknowledged.
Before we deployed a client-side deadline these ran to exactly 600 seconds
and then failed with a bare psycopg.OperationalError. Since 2026-09-07 22:52
UTC our own watchdog kills them at ~9.2 s, so the symptom is now capped, but the
underlying event is still happening roughly 8 times a day.
This looks like the issue reported in
https://community.fly.io/t/intermittent-client-side-hangs-on-managed-postgres/27736,
where large result payloads intermittently never resolve on MPG (0% failure on
AWS RDS, ~1% on MPG above 500 KB), and which was reported fixed around 30 April
to 5 May 2026 “just that there were a few physical hosts that missed the
rollout”.
Our main question: did the hosts running the machines above receive that
rollout?
Why we think it is payload-size related
Only two endpoints have EVER produced this fault, out of a large API surface.
Both are the two biggest single reads in the application:
| Endpoint | Response size |
|---|---|
GET /api/students |
~1,294 KB of JSON (717 rows) |
GET /api/scoreboard |
the other large aggregate read |
Every other route in the app returns small payloads and has never hung.
Evidence
Recorded by our own fault logger. elapsed_ms is the wall time of the failing
attempt; pool_* are the pool gauges sampled at the moment of the fault, which
show the pool was NOT exhausted.
The 600-second events (before our client deadline existed)
| UTC | Machine | Elapsed | Route | Pool at fault |
|---|---|---|---|---|
| 2026-09-07 09:01:45 | 801935a66d16d8 | 600,095 ms | /api/scoreboard | size 9, available 8, waiting 0 |
| 2026-09-07 10:54:52 | d8d5d46b4d9438 | 600,100 ms | /api/students | size 9, available 8, waiting 0 |
| 2026-09-07 17:48:43 | 801935a66d16d8 | 600,092 ms | /api/scoreboard | size 7, available 6, waiting 0 |
| 2026-09-07 18:19:21 | d8d5d46b4d9438 | 600,106 ms | /api/students | size 10, available 9, waiting 0 |
| 2026-09-07 19:31:26 | d8d5d46b4d9438 | 600,082 ms | /api/scoreboard | size 9, available 8, waiting 0 |
Note how tightly clustered 600,082 to 600,106 ms is. Something is imposing a
600-second bound, and it is not us: we had no timeout of any kind that could
produce it, which is why we went looking.
The same events after our 9-second client deadline (2026-09-07 22:52 UTC onward)
| UTC | Machine | Elapsed | Route | Pool at fault |
|---|---|---|---|---|
| 2026-09-08 08:10:07 | 801935a66d16d8 | 9,281 ms | /api/students | size 7, available 6, waiting 0 |
| 2026-09-08 10:46:58 | d8d5d46b4d9438 | 9,245 ms | /api/scoreboard | size 7, available 6, waiting 0 |
| 2026-09-08 10:48:24 | 801935a66d16d8 | 9,151 ms | /api/students | size 7, available 6, waiting 0 |
| 2026-09-08 12:15:50 | 801935a66d16d8 | 9,169 ms | /api/students | size 7, available 6, waiting 0 |
| 2026-09-08 13:34:54 | 801935a66d16d8 | 9,339 ms | /api/scoreboard | size 6, available 5, waiting 0 |
| 2026-09-08 14:27:13 | d8d5d46b4d9438 | 9,262 ms | /api/scoreboard | size 7, available 6, waiting 0 |
| 2026-09-08 15:04:53 | 801935a66d16d8 | 9,272 ms | /api/students | size 7, available 6, waiting 0 |
| 2026-09-08 19:08:45 | d8d5d46b4d9438 | 9,136 ms | /api/scoreboard | size 8, available 7, waiting 0 |
Both machines are affected, so it is not one bad machine.
What we ruled out, and how
- Pool exhaustion. The gauges above are captured at fault time: 5 to 9
connections free and nobody queueing. (We did separately have a genuine pool
exhaustion problem on other routes, visible as ~15,000 ms faults with
available: 0. We found and fixed that ourselves on 2026-09-07; it was our
bug, it is gone from the log after that date, and it is not what this report
is about.) - PostgreSQL. No setting on the cluster is 600 seconds.
statement_timeout
is 5,000 ms and never fired on these, which is itself the tell: the server
does not appear to have received the statement. - PgBouncer. Not in the path; we connect to the direct endpoint.
- A dead socket.
tcp_user_timeout=10000was already deployed and did not
fire, and the next occurrence came 34 minutes later on a still-working app, so
the peer was still acknowledging. - Our own code. Reproduced locally with a TCP proxy that keeps calling
recv()and stops forwarding: psycopg then waits on a live, silent socket
indefinitely, which is exactly the observed behaviour.
What we would like
- Confirmation of whether the physical hosts for
801935a66d16d8and
d8d5d46b4d9438, and the MPG cluster9g6y30wgm9krv5ml, received the fix
rolled out around 2026-04-30 to 2026-05-05 for the large-payload hang. - If they did, whether this is a regression or a distinct issue.
- Anything you would like us to capture next time it happens. We log every
occurrence with timestamps and pool state and can add packet captures or a
pg_stat_activitysnapshot at fault time if that is useful.
Happy to give timestamps in any format, or to grant access. We are not blocked,
because our client-side deadline contains it, but we are absorbing an unexplained
9-second failure on the two heaviest pages roughly eight times a day.