Guide for writing Netlify Edge Functions. Use when building middleware, geolocation-based logic, request/response manipulation, authentication checks, A/B testing, or any low-latency edge compute. Covers Deno runtime, context.next() middleware pattern, geolocation, and when to choose edge vs serverless.
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
Edge functions run on Netlify's globally distributed edge network (Deno runtime), providing low-latency responses close to users.
For framework projects, check the framework reference (the netlify-frameworks skill) before hand-writing an edge function — framework adapters emit their own edge middleware, so the behavior you need may already be generated. A custom edge function that duplicates adapter-generated middleware causes conflicts.
import type { Config, Context } from "@netlify/edge-functions";
export default async (req: Request, context: Context) => {
return new Response("Hello from the edge!");
};
export const config: Config = {
path: "/hello",
};Place files in netlify/edge-functions/. Uses .ts, .js, .tsx, or .jsx extensions.
export const config: Config = {
path: "/api/*", // URLPattern path(s)
excludedPath: "/api/public/*", // Exclusions
method: ["GET", "POST"], // HTTP methods
onError: "bypass", // "fail" (default), "bypass", or "/error-page"
cache: "manual", // Enable response caching
};Scope path narrowly — path: "/*" intercepts every request, including static assets. A /* match runs the edge function on every CSS, JS, image, and font request, not just your HTML pages — adding latency to each asset and billing an edge invocation for it. Match only the routes you need (e.g. path: "/", path: "/app/*"), or keep a broad path but exclude static assets with excludedPath (e.g. excludedPath: ["/*.css", "/*.js", "/*.png", "/*.woff2"]).
Cache headers on an edge response do nothing without cache: "manual". Setting Cache-Control (or any cache header) on the Response an edge function returns has no effect unless the function also opts in with config.cache = "manual". It's both or neither: without the flag the response is never cached, no matter what headers you set.
An edge function runs only if it is bound to a path. Bind it either with an inline export const config = { path: ... } in the function file (shown above), or with an [[edge_functions]] entry in netlify.toml that names the file:
[[edge_functions]]
path = "/admin/*"
function = "auth" # runs netlify/edge-functions/auth.tsA file in netlify/edge-functions/ with no path binding still deploys, but silently never runs. There is no build error and no warning — nothing routes a request to it, so it is never invoked. If an edge function "isn't doing anything," first confirm it declares a path inline or has a matching [[edge_functions]] entry.
When several edge functions match the same path, they run as a chain in this order:
netlify.toml run first, in the order they appear in the file (top to bottom).export const config) run next, in alphabetical order by filename.cache: "manual") always run after non-caching ones.To guarantee a specific order (e.g. an auth gate that must run before a personalization rewrite), declare the functions in netlify.toml in the order you want — don't depend on inline config, whose order is alphabetical by filename and easy to get wrong. Declaring the same function both inline and in netlify.toml merges them into an inline declaration (inline config wins), which forfeits the deterministic netlify.toml ordering.
In Netlify's request chain, edge functions execute before redirect and rewrite rules ([[redirects]], _redirects). Two consequences bite often:
path to the URL the client actually requests — an edge function declared on the target of a rewrite will not fire for requests that only reach that target via the rewrite.Response, the request chain stops there and redirect rules for that path never run. Return context.next() (or undefined) if you want redirects to still apply.Use context.next() to invoke the next handler in the chain and optionally modify the response:
export default async (req: Request, context: Context) => {
// Before: modify request or short-circuit
if (!isAuthenticated(req)) {
return new Response("Unauthorized", { status: 401 });
}
// Continue to origin/next function
const response = await context.next();
// After: modify response
response.headers.set("x-custom-header", "value");
return response;
};Return undefined to pass through without modification:
export default async (req: Request, context: Context) => {
if (!shouldHandle(req)) return; // continues to next handler
return new Response("Handled");
};export default async (req: Request, context: Context) => {
const { city, country, subdivision, timezone } = context.geo;
const ip = context.ip;
if (country?.code === "DE") {
return Response.redirect(new URL("/de", req.url));
}
};Local dev with mocked geo: netlify dev --geo=mock --country=US
Read and write cookies through the context.cookies helper instead of hand-parsing the Cookie header or building Set-Cookie strings:
export default async (req: Request, context: Context) => {
const bucket = context.cookies.get("bucket"); // read from the request
context.cookies.set({ name: "bucket", value: "a" }); // set on the response
context.cookies.delete("legacy_session"); // tell the client to delete it
return context.next();
};cookies.get(name) — reads a named cookie from the incoming request.cookies.set(options) — sets a cookie on the outgoing response (same option shape as the web CookieStore.set standard).cookies.delete(name) — instructs the client to delete the cookie.Use Netlify.env (not process.env or Deno.env):
const secret = Netlify.env.get("API_SECRET");import { randomBytes } from "node:crypto";import X from "https://esm.sh/package")For URL imports, use an import map:
// import_map.json
{ "imports": { "html-rewriter": "https://ghuc.cc/worker-tools/html-rewriter/index.ts" } }# netlify.toml
[functions]
deno_import_map = "./import_map.json"| Use Edge Functions for | Use Serverless Functions for |
|---|---|
| Low-latency responses | Long-running operations (up to 15 min) |
| Request/response manipulation | Complex Node.js dependencies |
| Geolocation-based logic | Database-heavy operations |
| Auth checks and redirects | Background/scheduled tasks |
| A/B testing, personalization | Tasks needing > 512 MB memory |
| Resource | Limit |
|---|---|
| CPU time | 50 ms per request |
| Memory | 512 MB per deployed set |
| Response header timeout | 40 seconds |
| Code size | 20 MB compressed |
b4ac277
Also appears in
last in sync May 18, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.