---
title: "How to run code at the edge with Netlify Edge Functions | Netlify Knowledge Base"
description: "Guides and articles to help you get the most out of the Netlify platform."
source: "https://www.netlify.com/knowledge-base/how-to-run-code-at-the-edge-with-netlify-edge-functions/"
last_updated: "2026-09-24T05:10:38.000Z"
---
## Key takeaways

Put a file in `netlify/edge-functions/`, export a default handler taking a `Request` and a `Context`, and export a `config` with a narrowly-scoped `path`. It runs on Deno at the network edge, close to your visitor. Return a `Response` to answer directly, a `URL` to rewrite same-site, or nothing to let the request continue.

-   **A function without a route silently never runs.** No config export and no `netlify.toml` entry means it deploys clean, with no error and no warning, and never executes.
-   **Scope `path` narrowly.** `path: "/*"` intercepts every request including static assets, adding latency and billing an invocation for each.
-   **Check your framework adapter first.** Next.js, Astro, Remix, SvelteKit, and Nuxt generate edge middleware. Duplicating it causes conflicts.
-   **Caching needs both parts.** Cache headers do nothing without `cache: "manual"`, and `cache: "manual"` without headers caches nothing.
-   **CPU limit is 50 ms per request.** This is compute time, not time spent waiting on resources.
-   **Env vars in `netlify.toml` are not available here.** Scope must include Functions.

## Close to the visitor, before the origin

Some work is worth doing before a request reaches your application at all. Checking whether someone’s logged in. Sending a visitor in Paris to the French page. Splitting traffic for an experiment. Rewriting a URL. Doing any of that at your origin means a round trip you didn’t need.

Edge Functions run that code at the network edge on a Deno runtime. The programming model is small: a request comes in, you look at it, and you either answer it, redirect it, rewrite it, or wave it through. What makes them worth understanding carefully is that several of the failure modes are silent, which is unusual and worth knowing up front.

## When to use edge, and when to use serverless

**Edge** suits low-latency request and response manipulation: auth checks and redirects, geolocation logic, A/B personalization, content localization, header and body transforms, SSR at the edge.

**Serverless** suits everything shaped differently: long-running work up to 15 minutes, heavy Node dependencies, database-heavy operations, background and scheduled tasks, or memory above 512 MB. See How to add a backend API endpoint with Netlify Functions.

Before you write one, **check whether your framework’s adapter already generates it.** Next.js Middleware, Astro middleware, and the SvelteKit, Remix, and Nuxt equivalents all compile to edge functions already. Hand-writing one that duplicates adapter middleware causes conflicts. Look at How to deploy Next.js, Astro, Nuxt, or SvelteKit to Netlify first.

A few places Edge Functions don’t reach at all: **Split Testing disables them** entirely, **custom headers (including basic auth headers) don’t apply** to them, **prerendering doesn’t apply** to paths they serve, and they’re **not part of Netlify’s HIPAA-compliant offering.**

## Worked example

```
import type { Config, Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {  // return Response | URL (rewrite) | undefined (continue chain)};
export const config: Config = { path: "/products/*" };
```

Files go in `YOUR_BASE_DIRECTORY/netlify/edge-functions`, or a custom directory set with the `edge_functions` key under `[build]` in `netlify.toml`. Keep it **outside** your publish directory so source files aren’t deployed. `.js`, `.ts`, `.jsx`, and `.tsx` all work, though if a `.ts` and `.js` share a name, the `.ts` is ignored and the `.js` deploys.

### Redirect on geo and cookie

```
export default async (req: Request, { cookies, geo }: Context) => {  if (geo.city === "Paris" && cookies.get("promo-code") === "15-for-followers") {    return Response.redirect(new URL("/subscriber-sale", req.url));  }};
```

Returning nothing lets the request continue, which is what happens for everyone who isn’t in Paris with that cookie.

### Rewrite

```
export default async (request: Request, { geo }: Context) => {  if (geo.city === "Paris") return new URL("/subscriber-sale", request.url);};
```

Returning a `URL` rewrites to a **same-site** URL with a 200 status and leaves the address bar unchanged. To reach another site or external content, use `fetch()` instead.

### Transform the response

```
import type { Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {  const response = await context.next();  const text = await response.text();  return new Response(text.toUpperCase(), response);};
```

`context.next()` runs the rest of the chain and hands you the origin `Response`. Only call it when you actually need the body, because it costs latency otherwise.

To transform a **different** path, use `fetch()`. Be aware that starts a **new** request chain and re-runs any edge functions matching that path. `context.next()` reaches a static asset or serverless function at the same internal path without re-running edge functions.

### Reading the request body

A body can only be read once, so pass a fresh request to `next()`:

```
export default async (req: Request, context: Context) => {  const body = await req.json();  if (!isValid(body.access_token)) return new Response("forbidden", { status: 403 });  return context.next(new Request(req, { body: JSON.stringify(body) }));};
```

### Response caching

Both halves are required. Headers without `cache: "manual"` do nothing, and `cache: "manual"` without headers caches nothing.

```
import type { Config, Context } from "@netlify/edge-functions";
export default async (req: Request, context: Context) => {  return new Response("Hello world", {    headers: { "cache-control": "public, s-maxage=3600" },  });};
export const config: Config = { cache: "manual", path: "/hello" };
```

Use caching only for endpoint-style responses that are reusable across clients, like shared SSR HTML. **Never** for middleware, routing, or per-client personalization. Cached responses don’t count toward invocations, which is a nice side effect.

### SSR at the edge

```
import React from "https://esm.sh/react";import { renderToReadableStream } from "https://esm.sh/react-dom/server";import type { Config, Context } from "@netlify/edge-functions";
export default async function handler(req: Request, context: Context) {  const stream = await renderToReadableStream(    <html><body><h1>Hello {context.geo.country?.name}</h1></body></html>  );  return new Response(stream, { status: 200, headers: { "Content-Type": "text/html" } });}
export const config: Config = { path: "/hello" };
```

### Ordering multiple functions on a path

```
[[edge_functions]]  path = "/admin"  function = "auth"
[[edge_functions]]  path = "/admin"  function = "injector"  cache = "manual"
```

Header matching uses an `[edge_functions.header]` sub-table.

Execution order: config-file declarations run before inline ones, framework-generated before user-written, and non-cached before cached. Within `netlify.toml` it’s top to bottom. Within inline config it’s **alphabetical by file name**, which is rarely what you want, so use `netlify.toml` when order matters. If the same function is declared both inline and in toml, the two merge and inline fields win.

## Common failure modes

**Your edge function does nothing and there’s no error.** Check the route first. Edge functions are not auto-assigned a URL. With no `config` export and no `netlify.toml` declaration, it deploys cleanly with no build error and no warning, and never executes.

**Everything got slower and your invocation count jumped.** `path: "/*"`. It intercepts every request including static assets, adding latency to each and billing an invocation for each. Match only what you need.

**Your cached function is serving instead of a real file.** A cached function **shadows static files**. `cache: "manual"` on `/*` makes `/cat.png` serve the function rather than the image.

**Your cache headers do nothing.** Missing `cache: "manual"`. Or you set them somewhere other than inline in code, which is required.

**Caching seems broken locally.** There’s no local caching. Cache headers are ignored under `netlify dev`.

**Your cached response disappeared after a deploy.** A new deploy in the same context voids `s-maxage`, `max-age`, and `Expires`, because deploys are atomic.

**Your env var is undefined.** Variables in `netlify.toml` are **not** available to edge functions. The scope has to include **Functions**; Build-scoped variables are build-only. And values are frozen at deploy time, so changing one needs a new deploy.

**You set an env var at runtime and it didn’t stick.** `Netlify.env.set` and `delete` are invocation-scoped only. Use the Netlify env API to actually update a value.

**Your geo logic looks broken locally.** Mock it: `--geo=mock` gives San Francisco, or `--geo=mock --country=XX` for a specific country.

**Your function doesn’t run on a rewritten request.** A function on the **target** of a static rewrite does not run for rewritten requests.

**Your redirects stopped firing.** If a function returns a `Response`, declared redirects for that path are skipped.

**Your cross-subdomain cookie won’t set.** `netlify.app` is on the Public Suffix List. You need a custom domain.

**Your npm package fails at the edge.** npm support is beta. Packages needing native binaries (Prisma) or runtime dynamic imports (cowsay) may fail. Prefer `node:` built-ins or Deno URL imports.

**Your import map is ignored.** Import maps in `deno.json` are unsupported. Use a separate file declared via `deno_import_map` in `[functions]`.

**You hit the CPU limit on something that mostly waits.** The 50 ms limit is CPU time and excludes waiting on resources, but `waitUntil` work still counts against it.

**Your manual deploy errors out.** Manual deploys need Netlify CLI 12.2.8 or newer.

## Reference

### Return values

Return

Effect

`Response`

Respond directly. Ends the chain; declared redirects for the path don’t run.

`URL`

Rewrite to a **same-site** URL with 200 status. Address bar unchanged.

`undefined` / bare `return;`

Bypass this function, continue the chain

### `config` properties

Property

Notes

`path`

`URLPattern` string or array. Must start with `/`.

`excludedPath`

Exclude routes from `path`. Must start with `/`.

`pattern` / `excludedPattern`

Regex alternatives to `path` / `excludedPath`

`method`

String or array of HTTP methods. **Inline only.**

`header`

Header conditions: `true` (present), `false` (absent), or a regex on the value. Names case-insensitive.

`cache`

`"manual"` to opt into caching

`onError`

`"fail"` (default), a same-site `"/path"`, or `"bypass"`. Inline only.

### `Context` object

Property

What you get

`geo`

`city`, `country {code,name}`, `subdivision {code,name}`, `latitude`, `longitude`, `timezone`, `postalCode`

`cookies`

`get(name)`, `set(options)`, `delete(name|options)` (CookieStore standard)

`next(options?)` / `next(request, options?)`

Continue the chain. `options.sendConditionalRequest`.

`params`

Path params. `/pets/:name` gives `{ name: "winter" }`. Query string via `request.url`.

`ip`, `requestId`, `server.region`

Request metadata

`site`

`id`, `name`, `url`

`account.id`

Account identifier

`deploy`

`context`, `id`, `published`, `skewProtectionToken`

`waitUntil(promise)`

Work after the response is sent. Still counts toward the CPU limit.

`Netlify.context` gives the same object inside the handler, and `null` outside it.

Netlify adds no headers to edge requests, so use `context` for client information.

### Error handling

`onError` value

Behavior

`"fail"` (default)

Generic error page, stops the chain

`"/custom-path"`

Rewrite to a same-site path, served without invoking that path’s edge functions

`"bypass"`

Skip the erroring function, continue the chain

Fail **closed** for critical logic like auth. Fail **open** for progressive enhancement like localization, where `bypass` is right.

### Limits

Limit

Value

Code size

20 MB compressed (bundle)

Memory

512 MB per deployed set

CPU execution

50 ms per request (excludes resource waiting)

Response header timeout

40 s

Invocations per month

Varies by plan. Cached responses don’t count.

### Runtime and modules

Deno, with the standard Web APIs: `fetch`, `Request`, `Response`, `URL`, `console`, `atob` / `btoa`, `TextEncoder` and `TextDecoder` (plus stream variants), Web Crypto (`crypto.randomUUID`, `getRandomValues`, `subtle`), `WebSocket`, timers, the Streams API, `URLPattern`, and `Performance`.

Import style

Example

Node built-ins

`import { randomBytes } from "node:crypto"`

Deno URL modules

`import React from "https://esm.sh/react"`

npm packages (beta)

`npm install`, then import by name

### Caching headers supported

`Cache-Control`, `CDN-Cache-Control`, `Netlify-CDN-Cache-Control`, `Expires`, `Vary`, `Netlify-Vary`. All must be set inline in code.

### Local dev

```
npm install netlify-cli -gnetlify dev      # runs edge functions on local requests at :8888
```

Geo mocking with `--geo=mock` or `--geo=mock --country=XX`. Debug with `--edge-inspect` or `--edge-inspect-brk`.

Logs live under **Logs & Metrics > Edge Functions**, where each `console` log names the emitting function. Filter by name or path glob and time. Retention is at least 24 hours, 7 days on some plans.

## FAQs

**How do I run middleware at the edge on Netlify?** Create a file in `netlify/edge-functions/`, export a default handler taking `(request, context)`, and export a `config` with a `path`. Return a `Response`, a `URL` to rewrite, or nothing to continue the chain.

**Why isn’t my edge function running?** Almost certainly it has no route. Edge functions aren’t auto-assigned a URL, and one without a `config` export or a `netlify.toml` declaration deploys with no error and never executes.

**What’s the difference between Edge Functions and Netlify Functions?** Edge Functions run on Deno at the network edge with a 50 ms CPU limit, for low-latency request manipulation. Netlify Functions run on Node with up to 60 seconds (15 minutes in background mode) for heavier work, databases, and scheduled tasks.

**How do I do a geolocation redirect?** Read `context.geo` in an edge function and return `Response.redirect(...)` when it matches. Test locally with `netlify dev --geo=mock --country=DE`.

**Why are my cache headers being ignored on an edge function?** You need `cache: "manual"` in the config as well. Headers alone do nothing, and `cache: "manual"` alone caches nothing.

**Can an edge function rewrite to an external URL?** No. Returning a `URL` rewrites same-site only. Use `fetch()` to reach external content.

**Can I use npm packages in an edge function?** It’s in beta. Install and import by name, but packages requiring native binaries or runtime dynamic imports may fail. Prefer `node:` built-ins or Deno URL imports.

**Why is my env var undefined at the edge?** Variables declared in `netlify.toml` aren’t available to edge functions. Set it in the UI or CLI with a scope including Functions, then redeploy, since values are frozen at deploy time.

**Do edge functions work with Split Testing?** No. With Split Testing enabled, edge functions do not run.

## Related

-   [`URLPattern` API](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API)
-   [Netlify CLI: get started](https://docs.netlify.com/cli/get-started/)

This article is generated from Netlify’s open-source agent guidance at [netlify/context-and-tools](https://github.com/netlify/context-and-tools), the same reference our AI coding agents use.

* * *

Got something to run at the edge? Start at [netlify.new](https://netlify.new).