What a cold start actually is on Supabase
Supabase Edge Functions run Deno inside V8 isolates on a globally distributed edge network. A cold start is any request that has to boot a fresh isolate: allocate memory, load your bundled function code, and evaluate top-level module scope before your handler runs. A warm start reuses an isolate that has already done that work and jumps straight to handling the request. The difference is usually 20–80ms of cold vs a few ms of warm, but bad bundles or slow top-level work can push cold starts into the hundreds of milliseconds and turn a fast API into a laggy one.
Why isolates are faster than containers
Most function platforms boot a container or micro-VM per instance: a Linux kernel, a Node runtime, and your code, all resolved at cold time. That takes hundreds of ms even on well-tuned platforms. Deno isolates skip almost all of that. There is no per-request kernel, no Node module resolution walking node_modules, and no npm install at boot because your dependencies were bundled to a single JavaScript file at deploy time. The isolate is a lightweight sandbox inside a shared V8 process that pre-exists on every edge node. Starting one is closer to opening a browser tab than booting a server.
The four things that slow you down
- Massive bundles — every kilobyte you import is parsed on the cold path. Strip unused imports and prefer named imports so tree-shaking works.
- Heavy npm packages — big transitive graphs (aws-sdk v2, moment, whole-lodash) balloon the bundle. Prefer Deno-native modules from JSR, or lightweight alternatives like Luxon or Day.js.
- First hit in a fresh region — the edge has global regions, and one that has never served your function will be genuinely cold on the first request from that geography.
- Synchronous top-level work — DB pool init, JWT signing keys fetch, WASM decompression, or big JSON parses at module scope. All of this runs before your handler on every cold start.
Measure before you optimize
Do not guess. Deploy the function, then hit it from an external monitor (or a curl loop with --write-out '%{time_total}') from multiple regions with a query string that varies to bypass any CDN cache. Record P50, P95, and P99. If P99 is close to P50 you are already warm and your problem is elsewhere. If P99 is 5–20x P50 you have a classic cold-start tail. The Supabase dashboard shows function invocation counts and errors but does not currently break out cold vs warm; you have to infer it from tail latency.
Fix 1: shrink the bundle
Run deno info on your entry file to see the total module graph. Anything imported at the top level ends up in the bundle even if only used inside one branch. Move rarely-used imports inside the function that needs them (dynamic import()) so they load lazily on warm requests and never on cold ones. Drop dependencies that pull in Node polyfills you do not use. A bundle under 500KB is a good target; over 2MB and you will feel it on every cold start.
Fix 2: move work out of module scope
Everything you write at the top level of your function file runs on every cold start, before your handler. Fetching JWT signing keys, initializing a database pool, downloading a config file, decoding a WASM binary — all of it runs on the critical path. The fix is to move those calls inside the handler and cache the result in a module-scoped variable that is populated lazily. The first warm request pays the cost once; every subsequent request in the same isolate is instant.
Fix 3: keep isolates warm on hot paths
For latency-sensitive endpoints you own, hit the function every 30–60 seconds from an external cron or, better, from pg_cron inside your own Supabase project. A simple ping endpoint that returns 200 without touching Postgres is enough to keep the isolate resident. Do this per region you care about, not just from one location; the edge treats each region independently. This is not free (you pay for the invocations), but it is much cheaper than provisioning min-instances on a container platform.
Fix 4: skip the function when you can
If the function's whole job is 'read from a table with RLS' or 'insert a row the client already has permission to write', skip the Edge Function entirely and let the client hit PostgREST directly. The Data API is always warm, uses the same JWT, and shaves 20–40ms off the roundtrip because you cut a hop. Reserve Edge Functions for work that genuinely needs server-side secrets, orchestration across services, or third-party API calls you do not want to expose to the browser.
When cold starts stop being your problem
Cold starts are worst at low traffic. A function that serves five requests a minute will cold-start most of them because isolates get evicted between requests. The same function at 500 requests per minute stays warm across the fleet and cold starts become a rounding error. If you can shape traffic (batch background jobs, coalesce reads) so that hot paths stay above the warmth threshold, the whole class of problem disappears without any code changes.
Caching third-party calls at the isolate level
When a function calls a third-party API on every request — fetching config from a slow provider, exchanging an OAuth code, pulling a rate table — the roundtrip time dominates far more than the cold start. Cache the response in a module-scoped variable with a TTL, and every warm request inside the isolate skips the network entirely. This works because isolates persist across requests until they are evicted. It is not a distributed cache (each isolate has its own copy) but for read-heavy config it turns hundreds of ms into microseconds and shrinks your function's total execution time (which you pay for).
Database connection strategy from an Edge Function
Opening a fresh Postgres connection on every invocation is a classic anti-pattern: TLS handshake plus auth plus session setup costs 50–200ms and burns Postgres backends. Route every function through Supavisor, Supabase's connection pooler, so hundreds of concurrent isolates share a small pool of backend connections. Prefer the pooler's transaction mode for stateless queries and session mode only when you truly need session-scoped state. Even better, if the query is a simple table read gated by RLS, hit the Data API from inside the function instead of speaking Postgres directly; PostgREST is already warm and skips the pooler entirely.
Regional pinning and how routing decides your latency
Supabase's edge routes each request to the nearest healthy region that has a warm isolate for your function. The database, however, lives in the single region you chose at project creation. A function that hits Postgres from an edge on the other side of the planet pays that transcontinental latency on every query, and no amount of function optimization will fix it. If your users are global and your workload is DB-heavy, pin the project to the region with the most traffic and consider caching aggressively at the function layer. If the workload is CPU-only (crypto, image resize, LLM proxying), the edge distribution shines and the DB region does not matter.