Summary
On a hosted project, roughly half of cold isolates freeze for ~75 seconds on the first outbound TCP connection, and the isolate's timers are frozen along with it — a setTimeout armed before the connect does not fire until the freeze ends. The freeze always ends at ~75,013ms after boot, which is the same constant at which an idle worker is shut down.
It is not the connection target: the retry connects in ~450ms on the same isolate immediately afterwards, and the behaviour is identical against two completely different hosts.
Observed
Function logs from one invocation (offsets relative to booted):
19:22:38 + 38ms internal-db: DB_POOLER_URL -> aws-1-eu-north-1.pooler.supabase.com
19:23:53 + 75021ms CLIENT_DEADLINE: database call exceeded the client deadline
19:23:54 + 75431ms acquire_song_request_lock: took 75393ms
19:23:54 + 76173ms ingestion: 200 in 76135ms
19:23:54 + 76179ms shutdown
The CLIENT_DEADLINE line is an 8-second setTimeout racing the query:
await Promise.race([
sql.begin(async (tx) => { /* one SELECT */ }),
new Promise((_, reject) => setTimeout(() => reject(deadlineError()), 8_000)),
])
It is logged at 75,021ms. The timer was armed at ~38ms, so it should have fired at ~8,038ms. The handler had started — the line at +38ms is printed by the code immediately preceding the connect.
The 75,013ms constant
Across 11 triggers:
|
values (ms) |
| freeze ends / 8s deadline finally raised |
75015, 75014, 75013, 75021, 75013, 75015 |
shutdown on a healthy idle worker |
75015, 75012, 75013, 75011, 75010, 75012 |
Those are the same number. A network timeout scatters by hundreds of ms (DNS backoff, SYN retries, TLS); this does not vary by more than ~10ms, and it coincides exactly with the idle-worker teardown interval. That is what makes me think the isolate is not being scheduled rather than waiting on the network.
Not the host
Reproduced identically against both:
db.<ref>.supabase.co (direct, AAAA only)
aws-1-eu-north-1.pooler.supabase.com:6543 (Supavisor, A records, IPv4)
Same ~75s freeze, same constant, same ~450ms success on the retry that follows.
Pattern
Triggering the function every ~95s, the outcome alternated — stall, fast, stall, fast — over 11 triggers, independent of the gap between them and of which host was configured. Healthy cold isolates complete the same query in ~500ms.
Environment
- Hosted project (not self-hosted), region
eu-north-1
- Deno driver:
postgres.js (npm:postgres@3), max: 1, connect_timeout: 5, prepare: false, ssl: 'require'
- Function invoked by a database webhook (
after insert trigger)
deno_version = 2 in config.toml
Impact
The function it affects has a fixed wall-clock budget shared by its database calls and an outbound API call that takes 40–80s. A 75s freeze spends most of the budget before any work starts, so the API call is opened with too little time and gets aborted — surfacing to users as a failure of the API call rather than of the connection that actually stalled. Because the freeze takes the timers with it, no in-isolate deadline can bound it; the only workaround I found was to detect the lost budget after the fact and requeue the job onto a fresh isolate.
What I ruled out
- Credentials / target availability — the retry succeeds immediately on the same isolate
- IPv6 reachability — reproduced on an IPv4-only pooler host
- Cold start generally — healthy cold isolates do the same work in ~500ms
- Load — single sequential invocations, ~95s apart, no concurrency
Happy to supply request IDs or the project ref privately if that helps narrow it down.
Summary
On a hosted project, roughly half of cold isolates freeze for ~75 seconds on the first outbound TCP connection, and the isolate's timers are frozen along with it — a
setTimeoutarmed before the connect does not fire until the freeze ends. The freeze always ends at ~75,013ms after boot, which is the same constant at which an idle worker is shut down.It is not the connection target: the retry connects in ~450ms on the same isolate immediately afterwards, and the behaviour is identical against two completely different hosts.
Observed
Function logs from one invocation (offsets relative to
booted):The
CLIENT_DEADLINEline is an 8-secondsetTimeoutracing the query:It is logged at 75,021ms. The timer was armed at ~38ms, so it should have fired at ~8,038ms. The handler had started — the line at +38ms is printed by the code immediately preceding the connect.
The 75,013ms constant
Across 11 triggers:
shutdownon a healthy idle workerThose are the same number. A network timeout scatters by hundreds of ms (DNS backoff, SYN retries, TLS); this does not vary by more than ~10ms, and it coincides exactly with the idle-worker teardown interval. That is what makes me think the isolate is not being scheduled rather than waiting on the network.
Not the host
Reproduced identically against both:
db.<ref>.supabase.co(direct, AAAA only)aws-1-eu-north-1.pooler.supabase.com:6543(Supavisor, A records, IPv4)Same ~75s freeze, same constant, same ~450ms success on the retry that follows.
Pattern
Triggering the function every ~95s, the outcome alternated — stall, fast, stall, fast — over 11 triggers, independent of the gap between them and of which host was configured. Healthy cold isolates complete the same query in ~500ms.
Environment
eu-north-1postgres.js(npm:postgres@3),max: 1,connect_timeout: 5,prepare: false,ssl: 'require'after inserttrigger)deno_version = 2inconfig.tomlImpact
The function it affects has a fixed wall-clock budget shared by its database calls and an outbound API call that takes 40–80s. A 75s freeze spends most of the budget before any work starts, so the API call is opened with too little time and gets aborted — surfacing to users as a failure of the API call rather than of the connection that actually stalled. Because the freeze takes the timers with it, no in-isolate deadline can bound it; the only workaround I found was to detect the lost budget after the fact and requeue the job onto a fresh isolate.
What I ruled out
Happy to supply request IDs or the project ref privately if that helps narrow it down.