Edge Functions skill for Codex
Run at the edge only what belongs there. Codex decides route by route whether code belongs at the edge, based on where its data lives, then writes a portable Web-standard handler with thin per-provider adapters. It avoids Node-only dependencies, verifies JWTs locally with crypto.subtle, keeps A/B variants cacheable, reaches databases through HTTP drivers or poolers, and compares TTFB from several regions before and after the move.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- edge-functions Installs to ~/.agents/skills/edge-functions/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install edge-functions The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project edge-functions Installs to .agents/skills/edge-functions/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/edge-functions.zipunzip -o edge-functions.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) edge-functions PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $edge-functions, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Decides what belongs at the edge and writes portable edge code for Cloudflare Workers, Vercel Edge and Routing Middleware, Netlify Edge Functions, Deno Deploy, Supabase Edge Functions, CloudFront Functions and Lambda@Edge - Web-standard Request and Response, CPU and bundle limits, missing Node APIs, database access over HTTP or pooled connections, geolocation, A/B bucketing, auth gates and waitUntil. Use when moving logic to the edge, an edge function fails on a Node API, latency got worse after going edge, or picking a runtime per route.
Use it when
- Deciding which routes should run at the edge versus in a region
- An edge function fails on a missing Node API or bundle size limit
- Latency got worse after moving database-backed code to the edge
- Writing geolocation, A/B, redirect or auth logic in front of a site
Use something else when
- You are building a full Cloudflare Workers app with bindings: use cloudflare-workers
- The work is CDN cache headers and purging rather than code: use cdn-caching
How to check the result
Tests run in the target runtime rather than Node, bundle size is within the plan limit, and TTFB from three regions improves or stays flat after the move.
The mistake it prevents
Moving a page that makes several sequential database queries to the edge, multiplying cross-region round trips and making it slower.
Edge code earns its place when it answers from data that is already at the edge (cache, KV, the request itself) or decides quickly where a request should go. It loses when it makes several round trips to a database in one region. A good edge function is small, uses only Web-standard APIs, finishes in a few milliseconds of CPU, and has a tested fallback when a binding or upstream is unavailable.
Inspect first
- Read
AGENTS.md, the framework and hosting config (wrangler.jsonc,vercel.json,netlify.toml,next.config,supabase/functions/), and which routes already run at the edge (export const runtime = "edge", middleware or proxy files, edge function directories). - List the data each candidate route touches and where that data lives (region of the database, KV, cache, third-party API).
- Check the dependencies the route imports. Anything using
fs,net,child_process, native addons or large SDKs will not run or will blow the bundle limit.
Should this run at the edge?
| Route does | Edge? |
|---|---|
| Redirects, rewrites, header changes, bot or geo blocking | Yes |
| Auth gate that verifies a JWT signature locally (no DB call) | Yes |
| A/B bucketing, feature flags from an edge store | Yes |
| Personalizing cached HTML with small fragments (HTMLRewriter, streaming) | Yes |
| Read from an edge KV or cache, fall back to origin | Yes |
| One query to a regional database | Maybe: only with an HTTP driver or pooler, and placement near the DB |
| Several sequential database queries, ORM-heavy pages | No: run regionally next to the database |
| CPU-heavy work (image processing, PDF, large JSON transforms) | No |
| Long-running jobs, websockets with server state | No, or use Durable Objects / a dedicated service |
If the answer is “maybe”, measure: time to first byte from three regions, edge versus regional, with the real database.
Runtime constraints that bite
- No filesystem, no
child_process, no raw TCP in most runtimes. Cloudflare offersconnect()for TCP and anodejs_compatflag for many Node built-ins; others vary. - CPU time per request is capped (milliseconds on free tiers). Await-time on fetch usually does not count against CPU, but wall-clock limits still apply.
- Bundle size caps are a few MB compressed. Tree-shake and avoid SDKs that pull in Node
polyfills; use the provider REST API with
fetchinstead. - Global variables persist only for the lifetime of an isolate. They are a cache, never a store, and different requests may share or not share them.
evalandnew Functionare blocked in several runtimes; some template engines and schema validators that compile code at runtime will fail.
For per-provider limits, placement controls and entry-point signatures, read references/provider-runtimes.md when targeting a specific platform or comparing two.
Portable handler shape
Write the core as a pure (Request) => Promise<Response> and adapt per provider:
// src/edge/handler.ts
import { jwtVerify, createRemoteJWKSet } from "jose";
const JWKS = createRemoteJWKSet(new URL("https://auth.example.com/.well-known/jwks.json"));
const CHECKOUT_BLOCKED = new Set<string>(["XX"]); // ISO codes from legal or compliance, not guesses
export async function handle(req: Request, country: string | undefined): Promise<Response | null> {
const url = new URL(req.url);
if (country && CHECKOUT_BLOCKED.has(country) && url.pathname.startsWith("/checkout")) {
return new Response("Not available in your region", { status: 451 });
}
if (url.pathname.startsWith("/app")) {
const token = req.headers.get("cookie")?.match(/(?:^|;\s*)session=([^;]+)/)?.[1];
if (!token) return Response.redirect(new URL("/login", url), 302);
try {
await jwtVerify(token, JWKS, { issuer: "https://auth.example.com", audience: "web" });
} catch {
return Response.redirect(new URL("/login", url), 302);
}
}
return null; // continue to origin
}
// Cloudflare Worker adapter
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext) {
return (await handle(req, req.cf?.country as string | undefined)) ?? fetch(req);
},
};
jose works in every Web-standard runtime because it uses crypto.subtle. Prefer
libraries that state edge or Workers support.
Sticky A/B bucketing
function bucket(req: Request): { variant: "a" | "b"; setCookie?: string } {
const existing = req.headers.get("cookie")?.match(/(?:^|;\s*)ab_home=(a|b)/)?.[1] as "a" | "b" | undefined;
if (existing) return { variant: existing };
const variant = crypto.getRandomValues(new Uint8Array(1))[0] < 128 ? "a" : "b";
return { variant, setCookie: `ab_home=${variant}; Path=/; Max-Age=2592000; SameSite=Lax; Secure` };
}
Rewrite to /home-a or /home-b so each variant stays cacheable on its own URL; never
vary the cache on the cookie itself.
Data access from the edge
| Option | Use when |
|---|---|
| Edge KV / Edge Config / cache API | Read-mostly config, flags, rendered fragments |
| HTTP database drivers (Neon serverless, PlanetScale, Turso/libSQL, Supabase REST) | One or two queries per request |
| Connection pooler close to the DB (Cloudflare Hyperdrive, PgBouncer, Prisma Accelerate) | Existing Postgres with TCP drivers |
| Run the function near the DB (Cloudflare Smart Placement, Vercel function region) | Many queries; you keep edge routing but compute moves to the data |
Never open a fresh TCP connection to a regional Postgres from hundreds of edge locations per request; connection storms take the database down.
Background work
Use the runtime’s waitUntil for logging, analytics and cache writes after the response:
ctx.waitUntil(...) on Workers, context.waitUntil on Netlify, waitUntil from
@vercel/functions on Vercel. Unawaited promises without it may be cancelled.
Verification
- Local runtime test, not Node:
wrangler devor@cloudflare/vitest-pool-workers,netlify dev,vercel dev,supabase functions serve,deno test. - Bundle size checked against the plan limit (
wrangler deploy --dry-run --outdir distthen inspect, or the build output size report). - TTFB compared from at least three regions before and after the move (
curl -w "%{time_starttransfer}\n" -o /dev/null -s <url>from each, or a synthetic monitor). - Failure path tested: upstream down, missing cookie, invalid JWT, unknown country.
- Production deploys only after explicit user approval.
Gotchas
- Middleware that runs on every request, including static assets, adds latency and cost. Restrict it with a matcher to the paths that need it.
Response.redirectrequires an absolute URL in several runtimes; build it withnew URL(path, req.url).Date.now()and timers may be coarse or frozen during a request in some isolates (a Spectre mitigation); do not use them for fine-grained benchmarking inside the function.- Environment variables are read differently per runtime (
envbinding in Workers,Netlify.env.get,Deno.env.get,process.envon Vercel). Centralize access behind one helper. - Geolocation headers are best-effort; never use them as an authorization control.
What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Edge Functions"
short_description: "Run at the edge only what belongs there"
default_prompt: "$edge-functions Should our Next.js API routes run on the edge runtime? Check each one" references/provider-runtimes.md
# Edge runtimes by provider
Limits change with plans and releases. Treat the magnitudes below as a guide for design
decisions and confirm exact numbers in the provider's current limits page before relying
on them. If network is unavailable in the sandbox, say which numbers were not re-checked.
## Comparison
| Runtime | Engine | Entry point | CPU budget (magnitude) | Node APIs | Placement control |
|---|---|---|---|---|---|
| Cloudflare Workers | V8 isolates (workerd) | `export default { fetch(req, env, ctx) }` | ~10 ms free; paid defaults to tens of seconds, configurable via `limits.cpu_ms` | Many via `nodejs_compat` flag | Smart Placement (`"placement": { "mode": "smart" }`) moves execution near back ends |
| Vercel Routing Middleware / Edge runtime | V8 isolates | `middleware.ts` / `proxy.ts` (Next.js 16 renames middleware to proxy), or `export const runtime = "edge"` | Must start responding within seconds; can stream longer | Subset; Vercel now recommends the Node.js runtime for most functions | Function region (`preferredRegion`, `regions`) for Node functions |
| Netlify Edge Functions | Deno | `export default async (req, context) => ...` in `netlify/edge-functions`, `export const config = { path: "/x" }` | ~50 ms CPU | Deno plus `node:` specifiers for many modules | None; runs at the nearest edge |
| Deno Deploy | Deno | `Deno.serve(handler)` | Plan dependent | `node:` built-ins and npm specifiers | Region selection per project |
| Supabase Edge Functions | Deno | `Deno.serve(async (req) => ...)` in `supabase/functions/<name>/index.ts` | Seconds of CPU, longer wall clock | `npm:` and `node:` specifiers | Can be pinned to the project region via header or invoke option |
| CloudFront Functions | Restricted JS runtime | `function handler(event) { ... }` | Sub-millisecond; no network calls, tiny code size | None | Runs at every edge location |
| Lambda@Edge | Node.js / Python Lambda | Lambda handler on viewer or origin events | Seconds (viewer events shorter than origin events) | Full Node | Regional edge caches; deployed from us-east-1, no environment variables |
| Fastly Compute | WebAssembly (JS, Rust, Go) | Fetch event handler compiled to Wasm | Plan dependent | None (Wasm) | Runs at Fastly POPs |
## Picking per route
- Pure header and URL manipulation on a CloudFront distribution: CloudFront Functions,
not Lambda@Edge. Use Lambda@Edge only for network calls or body access.
- Next.js on Vercel: keep routing middleware thin (matcher limited, no DB calls); put data
work in Node.js functions in the database region.
- Netlify sites: Edge Functions for geo, auth gates and rewrites in front of static or
cached pages; regular Functions for anything that queries a database repeatedly.
- Supabase projects: Edge Functions are fine for webhooks and light APIs; for heavy work
call a regional service or a database function.
- Cloudflare: Workers can be both edge and "near the data" with Smart Placement, and
Durable Objects give strongly consistent state at a chosen location.
## Local testing commands
```bash
npx wrangler dev # Cloudflare Workers in workerd locally
netlify dev # Netlify Edge Functions via a local Deno runtime
vercel dev # Vercel, approximate for edge routes
supabase functions serve <name> # Supabase Edge Functions locally (needs Docker)
deno test --allow-net # Deno Deploy code and shared Web-standard handlers
```
Unit-test the shared `(Request) => Response` core with the runtime's own test runner
where possible (`@cloudflare/vitest-pool-workers`, `deno test`), because Node test
runners hide missing-API failures.
## Environment variables and secrets
| Runtime | Read | Set |
|---|---|---|
| Cloudflare Workers | `env.MY_SECRET` (binding) | `wrangler secret put MY_SECRET` |
| Vercel | `process.env.MY_SECRET` | `vercel env add` |
| Netlify Edge | `Netlify.env.get("MY_SECRET")` | `netlify env:set` |
| Deno Deploy / Supabase | `Deno.env.get("MY_SECRET")` | Dashboard, or `supabase secrets set` |
| CloudFront Functions | Not supported; use KeyValueStore for config | `aws cloudfront-keyvaluestore` APIs |
| Lambda@Edge | Not supported; bake config in or fetch from SSM at cold start | Deploy-time |
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.