Skip to content

What Are Edge Functions

Edge Functions are server-side TypeScript functions that run on Deno, deployed globally close to your users, for whenever your logic is not a database query.

Every layer covered so far in this course lives close to Postgres: PostgREST turns tables into a REST API, RPC functions run SQL-adjacent logic inside the database, and RLS enforces access at the row level. Edge Functions are the layer for everything that does not fit that shape — calling a third-party API, holding a secret that should never sit in the database, doing CPU-bound work outside SQL, or exposing a stateless HTTP endpoint with fully custom request and response handling.

The runtime detail matters: Edge Functions run on Deno, not Node.js. Deno ships with a global Deno.serve for handling HTTP requests, first-class TypeScript support with no build step, and secure-by-default sandboxing. If you have written Node.js server code before, the request/response shape will feel familiar, but the APIs and permission model are Deno’s.

supabase/functions/hello-world/index.ts
Deno.serve((req) => {
return new Response('Hello from the edge')
})

Choosing an Edge Function over a Postgres RPC function

Section titled “Choosing an Edge Function over a Postgres RPC function”

The Auto-Generated API module already gave you a way to run custom logic close to the data: a Postgres RPC function. That is still the right tool when the logic is fundamentally a database operation — best expressed in SQL, or needs to run inside a single transaction alongside other writes. Reach for an Edge Function instead when any of the following is true:

  • You need to call an external, third-party API (a payment provider, an email service, an AI model).
  • The logic depends on a secret (an API key, a signing key) that must never be readable from the database or the client.
  • The work is CPU-bound in a way SQL cannot express cleanly (image processing, complex string parsing, calling out to another library).
  • You want a plain stateless HTTP endpoint with full control over headers, status codes, and the request/response cycle.

If none of those apply and the operation is really just “read or write some rows with some logic,” an RPC function is usually simpler and faster because it never leaves the database — see Database Functions as RPC for why an atomic, in-database operation is often the better default.

The Supabase CLI treats Edge Functions as ordinary files in your project, deployed with a single command:

Terminal window
# Scaffold a new function at supabase/functions/hello-world/index.ts
supabase functions new hello-world
# Run all functions locally for development
supabase functions serve
# Ship a function to your live project
supabase functions deploy hello-world

supabase functions new creates the boilerplate file and folder. supabase functions serve starts a local Deno runtime so you can iterate without deploying. supabase functions deploy uploads the function so Supabase’s edge infrastructure runs it close to wherever the request comes from.

flowchart LR
  client["Client app"] -->|"GET/POST via PostgREST"| rest["Auto-generated REST API"]
  client -->|"rpc()"| rpcfn["Postgres RPC function"]
  client -->|"functions.invoke()"| edge["Edge Function (Deno, deployed at the edge)"]
  rest --> db[("Postgres")]
  rpcfn --> db
  edge -->|"optional"| db
  edge -->|"optional"| ext["Third-party API"]
An Edge Function as a third path alongside the REST/RPC API and direct database access

What actually runs when you serve: the isolate model

Section titled “What actually runs when you serve: the isolate model”

supabase functions serve and Supabase’s hosted platform are both powered by the same open-source Edge Runtime (github.com/supabase/edge-runtime) — “a server based on Deno runtime, capable of running JavaScript, TypeScript, and WASM services,” per the project’s own README, built so the same engine can be used “to locally test and self-host Supabase’s Edge Functions (or any Deno Function).” It runs two distinct runtimes, not one:

  • A main runtime that proxies incoming requests, has no user-facing limits, and can read every environment variable.
  • A user runtime — a separate, sandboxed Deno isolate per function — that actually executes your code, with enforced memory and time limits, and access to only the environment variables the main runtime explicitly allows through.

That sandboxing is not just theoretical. Proof from this local stack: a function using the postgres npm driver to reach the database pooler by its Docker network hostname (supabase_pooler_probe) fails inside the user runtime even though the exact same hostname resolves fine from a plain docker exec into the same container:

# from inside the running edge-runtime container directly (docker exec):
$ getent hosts supabase_pooler_probe
172.18.0.14 supabase_pooler_probe
# from inside a function running in that container's user-runtime isolate:
[Error] Error: getaddrinfo ENOTFOUND supabase_pooler_probe
at ... postgres/3.4.9/src/query.js:36:24
at Object.handler (file:///var/tmp/sb-compile-edge-runtime/edge-notes-pooler/index.ts:7:25) {
code: "ENOTFOUND",
syscall: "getaddrinfo",
hostname: "supabase_pooler_probe"
}

The same function reaches the database fine over a raw IP or host.docker.internal (used in the next lesson) — only the container’s own Docker-internal DNS name is unreachable from inside the isolate. This is a concrete, observable instance of the user-runtime sandbox restricting what the “outer” container can otherwise do, exactly the kind of isolation the two-runtime model exists to enforce. A full lifecycle treatment — permission model internals, module graph caching, and how isolates get retired and replaced — is covered in Edge Runtime Isolates.

An isolate has to boot before it can serve its first request; subsequent requests to the same warm isolate skip that cost. Proved with curl -w, timing a bare Deno.serve “hello world” function under supabase functions serve on this Mac:

scripts/edge-cold-start.sh
curl -s -o /dev/null -w "time_total=%{time_total}s http=%{http_code}\n" \
"$FUNCTIONS_URL/edge-hello" -H "Authorization: Bearer $ANON_KEY" -H "apikey: $ANON_KEY"
first request (cold isolate): time_total=0.066398s http=200
second request (warm isolate): time_total=0.007159s http=200
third request (warm isolate): time_total=0.006022s http=200

Roughly a 10x difference between the first request (which pays isolate boot cost) and every request after it on a warm isolate — real numbers from this Mac’s local stack, not a hosted-platform benchmark; absolute numbers on Supabase’s actual edge network will differ, but the cold-vs-warm shape is the same mechanism described above: a fresh user-runtime isolate has to be created before the first request, and a per-worker policy (the local stack’s policy = "per_worker" in config.toml) keeps it alive to serve the ones after it.

supabase functions serve runs the identical Edge Runtime binary that Supabase’s hosted platform runs, on your own machine, against your local Postgres — which is why the proofs in this module transfer directly to production behavior. The differences that do exist are about placement and scale, not the request-handling code path:

  • Local: one Edge Runtime instance, one project, --env-file-driven secrets, hot-reloads on file change (policy = "per_worker"), no geographic distribution.
  • Hosted: functions deploy to Supabase’s global edge network and run close to whichever region a request comes from; secrets come from supabase secrets set; isolates scale horizontally across regions rather than living in one process on your laptop.

Supabase publishes hard resource limits for the hosted runtime (supabase.com/docs/guides/functions/limits):

Maximum Memory: 256MB
Maximum Duration (wall clock): Free plan 150s, Paid plans 400s
Maximum CPU Time: 2s of actual CPU time per request (excludes async I/O wait)
Request idle timeout: 150s (no response before this returns a 504 Gateway Timeout)

The CPU-time limit is easy to misread: it is 2 seconds of the CPU actually computing, not 2 seconds of wall-clock time — an await fetch(...) to a slow third-party API does not burn CPU time while it waits, so a function can legitimately run far longer than 2 seconds as long as it is not computing for more than 2 of those seconds. This is also, concretely, why CPU-bound work (image processing, heavy parsing) is a good reason to reach for an Edge Function in the first place, and why a function that tries to do a large in-memory aggregation over rows pulled from Postgres is fighting exactly this limit — see the bug below.

The flip side of “Edge Functions are for logic that is not a database query” is a common trap: pulling rows out of Postgres into a function just to do work Postgres itself would do faster, with no network round trip and no CPU-time budget to fight. If the job is “aggregate/filter/join rows,” it belongs in a Postgres function callable over /rpc — see Database Functions as RPC — not inside a Deno isolate with a 2-second CPU budget.

An AI assistant asked for “an endpoint that returns the total revenue across all orders” produced this:

🐛 Spot the bug

supabase/functions/revenue-report/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'
Deno.serve(async (_req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
)
// Pull every order row into the function and sum it in JavaScript
const { data: orders } = await supabase.from('orders').select('amount')
const total = (orders ?? []).reduce((sum, o) => sum + o.amount, 0)
return new Response(JSON.stringify({ total }), {
headers: { 'Content-Type': 'application/json' },
})
})
Show the answer

This works on a small table and then quietly gets worse as orders grows: every row is fetched over the network into the isolate, held in memory, and summed in a JavaScript loop, inside a runtime with a 256MB memory ceiling and a 2-second CPU-time budget per request. A table with a few hundred thousand rows either blows the memory limit, burns through the CPU-time budget doing the reduce, or both — and the fix has nothing to do with Edge Functions at all: select sum(amount) from orders computes the same number inside Postgres, in one round trip, using an engine built to aggregate rows efficiently. The corrected version calls supabase.rpc('total_revenue') (or even a plain .select('amount').limit(0)-style aggregate via PostgREST) backed by a security invoker, stable Postgres function — the exact “database functions as RPC” pattern from the earlier module — and the Edge Function, if it is still needed at all, is left to do only what an RPC call cannot: authorization checks, a third-party call, or shaping the response for a specific client.

An Edge Function is not “your server code, just somewhere else” — it is a short-lived, sandboxed Deno isolate that Supabase’s Edge Runtime boots on demand, keeps warm for a while to absorb the next few requests cheaply, and eventually retires. The main runtime / user runtime split exists so that “the thing routing your request” and “the thing running your arbitrary code” are never the same trust boundary — proved above by the very real DNS restriction inside the user runtime that does not exist for docker exec into the same container. Thinking of an Edge Function as “a tiny, temporary, sandboxed server that Postgres cannot see” is the model that predicts both its strengths (isolation, a secret the client never touches, code that can call anything on the internet) and its limits (a real memory ceiling, a real CPU-time budget, no persistent state between cold isolates).

  • Edge Functions run arbitrary TypeScript with full internet access — at the cost of a hard memory ceiling (256MB) and CPU-time budget (2s/request) that a Postgres function never has to think about, because Postgres itself is the thing doing the computing there.
  • A warm isolate serves requests about 10x faster than a cold one (measured above) — at the cost of unpredictability: whether a given request hits a warm or cold isolate depends on recent traffic, not anything the function’s own code controls.
  • Local supabase functions serve runs the identical Edge Runtime binary as hosted — at the cost of losing geographic distribution and the hosted secrets store locally: .env/--env-file stands in for supabase secrets set, and there is exactly one instance, not one per region.
  • “An Edge Function is basically a small Node.js server.” It runs on Deno, inside a purpose-built sandboxed runtime with its own permission model and resource limits — code that assumes Node.js APIs or bare npm-style imports will not work unmodified, covered in the next lesson.
  • “If my function is slow, I should just give it more memory/time.” The hosted limits (256MB, 2s CPU time, 150s/400s wall clock) are fixed platform ceilings, not a dial to turn — a function that needs more than that is usually doing work that belongs somewhere else (Postgres for aggregation, a queue/background job for genuinely long-running work).
  • “Edge Functions always run in the same place as my database.” They deploy to a global edge network and run close to the request, not close to the database — a function that makes several sequential round trips to Postgres pays real network latency per trip, unlike a Postgres function’s SQL running right next to its own data.
  • “CPU-time limit means my function can only run for 2 seconds.” The 2-second budget is CPU time actually spent computing, not wall-clock time — a function can legitimately take much longer than 2 seconds if most of that time is spent await-ing network I/O (a slow third-party API, a database query) rather than burning CPU.
What runtime do Supabase Edge Functions run on
When should you choose an Edge Function over a Postgres RPC function
What are the two runtimes inside Supabase's Edge Runtime, and what is each responsible for
On this local stack, roughly how much slower was the first (cold) request to a function than a later (warm) one
What does the hosted platform's "2 seconds of CPU time" limit actually measure
An AI-generated function fetches every row of a large orders table and sums an amount column in a JavaScript reduce(). What is the real risk, and what is the fix?