when a Next.js app offloads auth and webhook handling to Supabase Edge Functions, the first request after a period of inactivity can spike latency past 2 seconds – a noticeable jank for realtime dashboards.
why the pause happens
edge functions run on isolated V8 isolates that are torn down after a configurable idle timeout (default ~5 minutes). on warm invocations the isolate reuses any in‑process state, but a cold start forces a fresh container, npm install, and most importantly a new pg client connection. the connection handshake alone can add 500‑800 ms, especially when TLS is involved.
keep‑alive vs pool inside the isolate
unlike traditional server processes you cannot keep a long‑living connection pool across isolates. the usual pg.Pool will still open a new socket on each cold start, then close it when the isolate dies. the trick is to lazily create a single client and reuse it only while the isolate lives.
import { createClient } from '@supabase/supabase-js'
import { Client } from 'pg'
let pgClient: Client | null = null
export const handler = async (event: any) => {
if (!pgClient) {
// first call in this isolate – establish the connection once
pgClient = new Client({
connectionString: process.env.SUPABASE_DB_URL,
ssl: { rejectUnauthorized: false }
})
await pgClient.connect()
}
const { data } = await pgClient.query('SELECT now()')
return new Response(JSON.stringify({ now: data[0].now }))
}
the pgClient lives in module scope, so subsequent invocations reuse the same socket until the isolate is reclaimed.
trade‑offs to watch
-
idle timeout – if your traffic pattern has >5 min gaps, every request still incurs the full cold start. you can tune
EDGE_FUNCTIONS_TIMEOUTin Supabase dashboard, but longer lifetimes increase resource consumption and cost. - connection limits – each warm isolate holds an open DB connection. on a high‑scale deployment you could exhaust the Postgres max‑connections count. a pragmatic guard is to cap concurrent edge functions with Supabase quotas or implement a tiny “connection broker” that proxies through a lightweight pooler like PgBouncer.
-
error handling – when an isolate is reclaimed the client is abruptly closed, causing
ECONNRESETon in‑flight queries. wrap queries in a retry block that re‑creates the client ifpgClientis null or the query throws a connection error. -
TLS handshake cost – you can shave a few hundred ms by using Supabase’s native
pgconnection string that enablessslmode=requirewithout full certificate validation, but be aware of security posture in production.
monitoring cold starts
Supabase logs expose X-Edge-Function-Invocation headers with cold-start:true. ship a tiny middleware that records the latency delta and feeds it to your observability stack – you’ll quickly see whether your traffic pattern justifies a higher idle timeout or a fallback to a traditional serverless function.
anyone else juggling edge isolates with Postgres? how do you balance connection churn vs latency in your own stack
Top comments (0)