Skip to main content
All guides
How it works

Supabase Edge Functions cold starts: what to expect and how to fix them

Deno on the edge cold-starts in tens of milliseconds — but only when the code is small, imports are cached, and the region is warm. Here is the playbook.

Last updated July 18, 2026

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.

FAQ

What is a typical Supabase Edge Function cold start in ms?
For a small function (under 200KB bundled, no synchronous top-level work) served from a warm region, 20–80ms is typical. A 1MB bundle with a JWT-keys fetch and a DB pool init at module scope will push that into 200–400ms. First-ever hit in a new region can be higher still because the edge node has to fetch the deployment artifact before it can boot the isolate.
Do Edge Functions run in every region?
They run in Supabase's global edge network, currently covering North America, Europe, Asia-Pacific, and South America. A request is routed to the nearest edge, which cold-starts the function on first hit and keeps it warm on repeat traffic. You do not choose the region; the edge does. Your Postgres database still lives in its single project region, so any function that hits the DB pays that latency regardless of edge location.
Can I pay for provisioned concurrency like on AWS Lambda?
No. Supabase Edge Functions do not currently offer provisioned or reserved concurrency. The recommended way to keep isolates warm is the external cron pattern: hit a lightweight ping endpoint every 30–60 seconds from pg_cron or an outside scheduler. This trades a few thousand cheap invocations for consistent sub-100ms latency on your real traffic.
How big can my function bundle be?
Supabase currently allows deployments up to 20MB per function. Practically, aim for under 1MB. Every kilobyte over that is parsed on every cold start, and the biggest gains come from dropping unused npm packages, preferring JSR/Deno-native modules, and moving optional dependencies behind dynamic import() so they only load when the code path that needs them actually runs.
Do Edge Functions cost extra beyond my Supabase plan?
The Free plan includes 500K function invocations per month. Pro includes 2 million, with additional invocations billed per million. There is no separate compute charge; you pay per invocation and per GB-second of execution time. Keeping functions small and fast is not just a latency win, it is directly cheaper because execution time is part of the bill.
Are Deno APIs the same as Node APIs?
Overlapping but not identical. Deno implements Web-standard APIs (fetch, Request, Response, crypto.subtle) natively and provides a Node compatibility layer for common built-ins like Buffer, path, and crypto. Most modern npm packages work through the compatibility layer, but anything that spawns child processes or expects a full Node filesystem will fail. Prefer packages that document Edge or Deno support explicitly.
Can I stream responses from an Edge Function?
Yes. Return a Response whose body is a ReadableStream and Supabase's edge will stream chunks to the client as you write them. This is how you implement server-sent events for LLM token streaming or long-running progress updates. Streaming does not change cold-start behavior, but it lets you send the first byte immediately after boot instead of waiting for the whole response to buffer.
How do I debug a slow function?
Add console.log timestamps at each stage (import complete, DB connected, handler start, handler end) and read them in the Functions logs in the dashboard. The timestamps make cold-start work visible: anything logged before 'handler start' is on the critical path. Once you see where the time goes, apply the four fixes: shrink the bundle, defer top-level work, keep warm, or skip the function entirely.

Keep reading

See also