You deployed a Supabase Edge Function. Locally with supabase functions serve it’s a tight 100ms. In production, the first request is 2-5 seconds; subsequent requests snap back to 100-200ms. Half an hour idle and it slows down again. Classic cold start, present on almost every serverless platform — but Supabase Edge Functions (Deno isolate based) have their own quirks.
Mental model: when traffic stops, Supabase evicts your Edge Function (Deno isolate is released). The next request has to spawn a new isolate, load code, import deps, run top-level init — that adds up to your 2-5 seconds.
Common causes
Ordered by hit rate, highest first.
1. Function evicted after idle
Supabase Edge Functions get evicted after ~5-15 minutes of idle. Next request triggers full cold start.
How to spot it: Users report “occasionally slow at night / weekends” — low-traffic-window cold starts.
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
Deno has to download + transpile these on first cold start — adds 1-3 seconds.
How to spot it: Multiple npm: imports at the top of the function.
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 cold start.
How to spot it: Top-of-file await or long synchronous work.
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');
...
});
Slow upstream → cold start + upstream slowness both land on the first request.
How to spot it: First await in the handler is an external API.
5. Bundled large JSON / WASM
A 5MB JSON dataset or WASM module — parse / instantiate takes time.
How to spot it: Large .json / .wasm files in the functions directory.
6. Region mismatch (users in Asia, function in US)
Cold start itself + cross-region latency = 5+ seconds perceived.
How to spot it: Supabase Dashboard → Settings → Project → Region differs from your main user base.
Shortest path to fix
Step 1: Measure 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"
# Compare to subsequent (warm)
time curl -X POST "..."
Record cold vs warm delta — that’s the cold-start cost.
Step 2: Cron warm-up (simplest)
If you can accept the extra invocations:
-- Supabase Dashboard → Database → Cron
SELECT cron.schedule(
'keep-warm',
'*/5 * * * *', -- every 5 min
$$ SELECT net.http_post(
url := 'https://xyz.supabase.co/functions/v1/my-fn',
headers := '{"Authorization": "Bearer ANON_KEY"}'::jsonb,
body := '{"warmup": true}'::jsonb
); $$
);
Function recognizes warmup and returns quickly:
if (body.warmup) return new Response('ok');
Cost: ~8640 extra invocations / month (one per 5 min), fine on free tier.
Step 3: Slim dependencies
// ❌ Full SDK
import Anthropic from 'npm:@anthropic-ai/sdk';
// ✅ Native fetch (skips 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',
},
body: JSON.stringify({ model: 'claude-opus-4-7', messages }),
});
}
Replacing each SDK with native fetch saves 1-2 seconds.
Step 4: Move top-level init into the handler
// ❌ Top level
const config = await loadConfig();
Deno.serve(async (req) => { ... });
// ✅ Lazy + cached in handler
let configPromise: Promise<Config> | null = null;
Deno.serve(async (req) => {
configPromise ??= loadConfig(); // only first request loads
const config = await configPromise;
...
});
When warm, configPromise is already resolved — no reload.
Step 5: Async-ify external calls
Deno.serve(async (req) => {
const result = await quickProcess(req);
// Slow external work doesn't block the response
// @ts-expect-error EdgeRuntime is a Supabase global
EdgeRuntime.waitUntil(slowExternalCall());
return Response.json(result);
});
Step 6: Switch platform / region
If cold start is critical to your product or your traffic is global:
Low-cold-start options:
- Cloudflare Workers (V8 isolate, ~5ms cold start)
- Vercel Edge Functions (similar)
- Always-warm Node serverless (Fly.io Machine, Railway)
Or fix Supabase region:
- Dashboard → Project → Settings → Region
→ choose the one nearest your users
Region changes aren’t trivially reversible — assess before flipping.
Prevention
- Decide upfront how much cold start you can accept: background sync → fine; user-facing interaction → must be < 500ms
- Latency-sensitive endpoints: cron warm-up (small) or switch to Cloudflare Workers (large)
- Zero top-level async imports / fetch / heavy compute — just declarations
- Don’t import a dep you don’t need; prefer native fetch over an SDK
- Large JSON data → Supabase Storage / external KV, fetched on demand
- Monitor p99 latency, separate cold vs warm; alert on high cold-start ratio
- If users are mainly Asia / EU, pick that region — cross-continent latency stacks with cold start
Related
Tags: #Indie dev #Debug #Troubleshooting