What Are Edge Functions
The idea in one sentence
Section titled “The idea in one sentence”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.
Not Node.js, not a database query
Section titled “Not Node.js, 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.
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 CLI workflow
Section titled “The CLI workflow”The Supabase CLI treats Edge Functions as ordinary files in your project, deployed with a single command:
# Scaffold a new function at supabase/functions/hello-world/index.tssupabase functions new hello-world
# Run all functions locally for developmentsupabase functions serve
# Ship a function to your live projectsupabase functions deploy hello-worldsupabase 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"] 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_probe172.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.
Cold start, measured on this local stack
Section titled “Cold start, measured on this local stack”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:
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=200second request (warm isolate): time_total=0.007159s http=200third request (warm isolate): time_total=0.006022s http=200Roughly 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.
What runs where: local serve vs. hosted
Section titled “What runs where: local serve vs. hosted”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.
Limits (from the docs)
Section titled “Limits (from the docs)”Supabase publishes hard resource limits for the hosted runtime (supabase.com/docs/guides/functions/limits):
Maximum Memory: 256MBMaximum Duration (wall clock): Free plan 150s, Paid plans 400sMaximum 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.
When NOT to reach for one
Section titled “When NOT to reach for one”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.
Reading AI-generated code
Section titled “Reading AI-generated code”An AI assistant asked for “an endpoint that returns the total revenue across all orders” produced this:
🐛 Spot the bug
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 JavaScriptconst { 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.
Mental Model
Section titled “Mental Model”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).
Trade-offs
Section titled “Trade-offs”- 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 serveruns the identical Edge Runtime binary as hosted — at the cost of losing geographic distribution and the hosted secrets store locally:.env/--env-filestands in forsupabase secrets set, and there is exactly one instance, not one per region.
Common Misconceptions
Section titled “Common Misconceptions”- “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.