Supabase Edge Function Cold Start Slow: Causes + Fix

First request takes 1–5s while warm ones are 100ms. Diagnose the real cause (heavy imports, top-level await, region) and cut boot time.

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?

SymptomMost likely causeJump to
Slow only after minutes of no traffic, fast otherwiseIdle eviction (#1)Step 2
Always slow on first hit, several npm: importsHeavy imports (#2)Step 3
Slow even on warm-ish hits; top-of-file awaitTop-level init (#3)Step 4
First request slow, blame traces to one upstream callCold external call (#4)Step 5
Slow for some geographies onlyRegion (#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

  1. 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).
  2. Watch boot time in Edge Functions → Logs / Metrics for a few days — the high outliers should disappear, not just the average.
  3. If you added a warm-up cron, confirm it’s firing in Integrations → Cron → run history and that those invocations return ok fast.

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 before Deno.serve.
  • Don’t import a dep you don’t need; prefer native fetch or jsr: over fat npm: 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-region rather 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.

Tags: #Indie dev #Debug #Troubleshooting