Supabase Edge Function Cold Start Slow

First request takes 2–5s — Deno isolate not warm, heavy imports, external API call.

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

Tags: #Indie dev #Debug #Troubleshooting