You deployed a Supabase Edge Function. Locally with supabase functions serve it’s a tight 100ms. In production the first request after a quiet spell is 1-5 seconds; the next ones snap back to 100-200ms. Leave it idle and it slows down again. That’s a cold start: when traffic stops, Supabase tears down your Deno isolate, and the next request has to spawn a fresh one, evaluate your code, resolve imports, and run any top-level init before it can answer.
Fastest fix: the boot cost is almost always your own code, not the platform. Supabase’s own benchmark (the persistent-storage release, July 2025) put the platform-only cold start at ~42ms average, with P99 cut from ~15s to ~460ms. So a multi-second cold start in 2026 means something in your function is heavy at boot — usually a fat npm: import or a top-level await. Move that work out of the boot path (Steps 3-4 below) and you reclaim most of the delay. If you just need the slowness to stop tonight, add a cron warm-up (Step 2).
Common causes
Ordered by hit rate, highest first.
1. Function evicted after idle
Supabase keeps a warm isolate around for a plan-dependent window after the last request, then evicts it. The next request triggers a full cold start. There’s no public “keep-alive N minutes” number you can configure — assume any function that goes minutes without traffic will be cold.
How to spot it: users report “occasionally slow at night / weekends” — low-traffic-window cold starts. (Separately: free-tier projects pause after 7 days of zero activity, which is a different and much slower problem.)
2. Heavy imports (especially npm packages)
import { Anthropic } from 'npm:@anthropic-ai/sdk'; // ~500KB
import { OpenAI } from 'npm:openai'; // ~1MB
import _ from 'npm:lodash'; // ~70KB
The CLI bundles your function into an ESZip, but a fat npm: graph still costs real time to load and evaluate on a cold isolate — typically 1-3 seconds. This is the single most common cause of a multi-second boot in 2026.
How to spot it: multiple npm: imports at the top of the function, or a deno.json/import_map.json pointing at large packages.
3. Heavy work at module top level
// Top level — runs on every cold start
const config = await fetch('https://...').then(r => r.json());
const dbConnection = await connectDB();
Deno.serve(async (req) => { ... });
Every isolate boot re-runs these, on top of the cold start itself.
How to spot it: top-of-file await, a top-level client/connection, or long synchronous compute before Deno.serve.
4. External API call during cold start
Deno.serve(async (req) => {
// This fetch is "cold" too
const data = await fetch('https://slow-third-party.com/data');
...
});
A slow upstream stacks on top of the cold start, so the first request eats both.
How to spot it: the first await in the handler is an external API you don’t control.
5. Bundled large JSON / WASM
A 5MB JSON dataset or a WASM module — parse / instantiate takes time, and counts against the 20MB bundled-function size limit (as of June 2026).
How to spot it: large .json / .wasm files in the functions directory, or a bundle close to 20MB.
6. Region mismatch (users in Asia, function in US)
Cold start itself plus cross-region round-trips reads as 5+ seconds to the user. By default an Edge Function runs in the region nearest the caller, but if you’ve pinned a region — or your project’s primary region is far from your users — the latency stacks.
How to spot it: the response header x-sb-edge-region (or the SB_REGION env var inside the function) doesn’t match where your users are.
Which bucket am I in?
| Symptom | Most likely cause | Jump to |
|---|---|---|
| Slow only after minutes of no traffic, fast otherwise | Idle eviction (#1) | Step 2 |
Always slow on first hit, several npm: imports | Heavy imports (#2) | Step 3 |
Slow even on warm-ish hits; top-of-file await | Top-level init (#3) | Step 4 |
| First request slow, blame traces to one upstream call | Cold external call (#4) | Step 5 |
| Slow for some geographies only | Region (#6) | Step 6 |
Shortest path to fix
Step 1: Measure the real cold start
# Wait ~15 minutes of no requests, then time the first one
time curl -X POST "https://xyz.supabase.co/functions/v1/my-fn" \
-H "Authorization: Bearer $KEY"
# Then immediately time a warm one
time curl -X POST "https://xyz.supabase.co/functions/v1/my-fn" \
-H "Authorization: Bearer $KEY"
The cold-minus-warm delta is your real cold-start cost. Cross-check it in the Dashboard: Edge Functions → your function → Logs / Metrics, where boot time and the x-sb-edge-region are recorded per invocation. If the warm number is already slow, you have a plain latency problem, not a cold-start one — fix the handler first.
Step 2: Cron warm-up (simplest, buys you time)
If you can accept the extra invocations, ping the function on a schedule so an isolate stays warm. Supabase Cron uses pg_cron + pg_net:
-- Supabase Dashboard → Integrations → Cron (or run in the SQL editor)
SELECT cron.schedule(
'keep-warm',
'*/5 * * * *', -- every 5 min
$$ SELECT net.http_post(
url := 'https://xyz.supabase.co/functions/v1/my-fn',
headers := '{"Content-Type": "application/json", "Authorization": "Bearer ANON_KEY"}'::jsonb,
body := '{"warmup": true}'::jsonb
); $$
);
Have the function recognize the warmup ping and return immediately, so you don’t run the real workload:
const body = await req.json().catch(() => ({}));
if (body.warmup) return new Response('ok');
Cost: ~8,640 extra invocations / month (one per 5 min). The Free plan includes a generous Edge Functions allotment, so this is usually free; check your plan’s included invocations before going below the 5-minute interval. This treats the symptom — pair it with Steps 3-4 to actually shrink the boot.
Step 3: Slim dependencies
The biggest lever. Drop fat SDKs for npm: REST calls you can make with native fetch:
// Full SDK — pulls a large module graph on cold start
import Anthropic from 'npm:@anthropic-ai/sdk';
// Native fetch — no import overhead
async function callAnthropic(messages: any[]) {
return fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': Deno.env.get('ANTHROPIC_API_KEY')!,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages,
}),
});
}
Replacing each SDK with native fetch typically saves 1-2 seconds of boot. Where you do need a package, prefer Deno-native or jsr: packages over npm:, and lazy-load anything big with a dynamic import() inside the handler so it only loads when actually used:
Deno.serve(async (req) => {
const { default: heavyLib } = await import('npm:some-heavy-lib');
...
});
Step 4: Move top-level init into the handler
// Top level — re-runs on every cold start
const config = await loadConfig();
Deno.serve(async (req) => { ... });
// Lazy + cached in the handler
let configPromise: Promise<Config> | null = null;
Deno.serve(async (req) => {
configPromise ??= loadConfig(); // only the first request loads it
const config = await configPromise;
...
});
When the isolate is warm, configPromise is already resolved — no reload. The boot path now just defines a function and calls Deno.serve, which is what Supabase optimizes for.
Step 5: Move slow external calls off the response path
If a slow upstream call doesn’t need to block the response, fire it after you respond with EdgeRuntime.waitUntil (a Supabase global), so the user isn’t waiting on cold start plus a slow third party:
Deno.serve(async (req) => {
const result = await quickProcess(req);
// Runs after the response is sent; doesn't block the user
// @ts-expect-error EdgeRuntime is a Supabase global
EdgeRuntime.waitUntil(slowExternalCall());
return Response.json(result);
});
Step 6: Run in the right region (don’t repoint the whole project)
Cross-region latency stacks with cold start. You usually do not need to change your project’s region — pin where the function runs instead. As of June 2026, set the execution region per call:
// Supabase client (JS)
import { FunctionRegion } from '@supabase/supabase-js';
const { data, error } = await supabase.functions.invoke('my-fn', {
region: FunctionRegion.ApNortheast1,
});
# Or via header
curl -X POST 'https://xyz.supabase.co/functions/v1/my-fn' \
-H 'x-region: ap-northeast-1'
# Or query param when you can't set headers (webhooks, CORS preflight)
# ...functions/v1/my-fn?forceFunctionRegion=ap-northeast-1
Valid values include ap-northeast-1, ap-southeast-1, eu-west-1, eu-central-1, us-east-1, us-west-1, and more (full list in the regional-invocation docs). Confirm with the response header x-sb-edge-region.
If your database-heavy function genuinely needs to sit next to a far-away primary, only then consider moving the project region (Dashboard → Project Settings → General → Region) — that one is slow and not trivially reversible, so assess first.
Step 7: If cold start is product-critical, change platform
When sub-100ms first-byte is a hard requirement and Steps 1-6 aren’t enough, a V8-isolate platform boots faster than a fresh Deno isolate:
- Cloudflare Workers — V8 isolates, near-zero cold start
- Vercel Edge Functions — similar isolate model
- Always-warm Node serverless — Fly.io Machines, Railway (a process that stays up, so no isolate cold start at all)
How to confirm it’s fixed
- Re-run Step 1’s cold-then-warm timing after ~15 idle minutes. The cold number should now be close to your warm number (typically under ~500ms for a slim function).
- Watch boot time in Edge Functions → Logs / Metrics for a few days — the high outliers should disappear, not just the average.
- If you added a warm-up cron, confirm it’s firing in Integrations → Cron → run history and that those invocations return
okfast.
Prevention
- Decide upfront how much cold start you can accept: background sync can be slow; a user-facing interaction should be
< 500ms. - Latency-sensitive endpoints: cron warm-up (small projects) or a V8-isolate platform (high traffic).
- Keep the module top level to declarations only — zero async
import,fetch, or heavy compute beforeDeno.serve. - Don’t import a dep you don’t need; prefer native
fetchorjsr:over fatnpm:packages, and lazy-load big ones. - Move large JSON / WASM out of the bundle into Supabase Storage or external KV, fetched on demand; keep the bundle well under the 20MB limit.
- Monitor p99 latency with cold and warm separated; alert on a high cold-start ratio.
- If your users are mainly in one region, pin the function there with
x-regionrather than guessing.
FAQ
Why is it fast locally but slow only in production?
supabase functions serve keeps one warm isolate the whole session, so you never see a cold start. Production evicts the isolate between bursts of traffic, so the first request after idle pays the full boot cost.
Didn’t Supabase make cold starts 97% faster? Why is mine still slow?
That July 2025 change fixed the platform’s boot overhead (avg ~870ms → ~42ms, P99 ~15s → ~460ms). It does nothing for time your own code spends at boot — fat imports and top-level await are still on you (Steps 3-4).
Does the Pro plan eliminate cold starts? No. Paid plans raise limits (e.g., wall-clock duration goes from 150s on Free to 400s on paid) and keep isolates warm a bit longer, but they don’t remove cold starts. The reliable fix is a warm-up ping or a slimmer function, on any plan.
Is a cron warm-up against Supabase’s terms or wasteful?
It’s a supported pattern (pg_cron + pg_net) and a few thousand tiny invocations a month are negligible. Just have the function short-circuit on the warmup body so you’re not running the real workload 8,640 times a month.
What are the current Edge Function limits I should design around?
As of June 2026: 256MB memory, ~2s CPU time per request, 150s wall-clock on Free (400s on paid), 150s request idle timeout (else 504), and a 20MB bundled-function size cap.
How do I change which region my function runs in without moving my database?
Set it per request with the client region option, the x-region header, or the forceFunctionRegion query param — no project change needed. Verify with the x-sb-edge-region response header.
Related
Tags: #Indie dev #Debug #Troubleshooting