Best Practices
Production edge functions - limits, cold starts, observability, secrets, dev workflow, cost, pitfalls
Best Practices
Edge functions are deceptively simple to deploy and surprisingly easy to mis-deploy. These habits keep them fast, cheap, and reliable.
Stay Within the Right Niche
The single biggest mistake: using edge for things it's bad at. Edge functions are for request-shape transformation, not heavy compute or stateful logic.
Use the edge for:
- Routing, auth, header rewrites
- Personalization injection on cached HTML
- A/B testing, canaries, geo
- API aggregation (BFF pattern)
- Streaming (LLM, SSE)
Don't use the edge for:
- Heavy DB queries against a single-region database (latency)
- Long-running tasks (CPU limits)
- Anything needing local filesystem
- Complex stateful workflows
When you find yourself rewriting Node.js code in an edge function, you've probably picked the wrong tool.
Know the Limits
Edge runtimes are tightly constrained. Plan for them:
| Constraint | Typical limit (Cloudflare Workers) | Implication |
|---|---|---|
| CPU time per request | 10 ms (free), 30 s (paid) | Compute-heavy code must move to origin |
| Wall-clock time | 30 s including waits | Long-running outbound work doesn't fit |
| Memory | 128 MB | Big data structures don't fit |
Outbound fetch per request | 50 (free), 1000 (paid) | Fan-out APIs have a ceiling |
| Script bundle size | 1 MB (free), 10 MB (paid) | Pick light dependencies |
| Concurrent requests per isolate | ~1000 | Mostly fine; explosive traffic spawns more isolates |
The patterns: early-exit on obvious responses, stream large responses rather than buffer, lazy-load heavy code, fan-out cautiously.
Cold Starts and Hot Isolates
V8 isolates spin up in 1-5 ms — orders of magnitude faster than container-based serverless. But:
- First request to a new POP for your Worker takes longer (script needs to be loaded).
- After ~30 seconds idle, the isolate may be evicted.
- Globally distributed traffic keeps more POPs warm.
Practical implications:
- Don't precompute expensive globals at module load — they run on every cold start.
- Use top-level
awaitcarefully — it gates the first request. - Synthetic traffic to your endpoints keeps hot regions warm (a CDN's job, but you can add scheduled pings to less-trafficked routes).
Vercel Edge and Deno Deploy have similar characteristics. Fastly Compute@Edge boots WASM modules in sub-ms — even better cold starts.
Observability
Edge logs are scattered across hundreds of POPs. Strategies:
| Mechanism | Use |
|---|---|
console.log shipped to a backend | Cloudflare Logpush, Vercel Logs, Fastly real-time logs — pipe to your central log store |
| Structured logging | JSON to stdout; ship to Loki / Elasticsearch / your SaaS |
| Tracing | Propagate traceparent headers; emit spans from the edge |
| Sampled logging | Don't log every request at full detail — sample 1-5% |
Cloudflare's Logpush integration writes worker logs to S3, R2, GCS, or HTTP endpoints in batches. Vercel ships logs to their dashboard or via integrations. Whichever platform, get logs off the edge into your normal stack.
Errors and Failures
export default {
async fetch(request, env, ctx) {
try {
return await handleRequest(request, env, ctx);
} catch (error) {
// Log to your error tracker
ctx.waitUntil(reportError(error, request, env));
// Fall through to origin or static fallback
return fetch(request).catch(() =>
new Response('Service temporarily unavailable', { status: 503 })
);
}
},
};Every edge handler should have a top-level try/catch with a fallback. A bug at the edge is in front of every user.
For error reporting, Sentry, Datadog, and Bugsnag all have edge SDKs.
Secrets
| Don't | Do |
|---|---|
| Hardcode secrets in source | wrangler secret put NAME, vercel env add, etc |
Commit .dev.vars to git | .gitignore it; share via your team's secret manager |
| Log secrets | Mask if necessary; prefer not logging them at all |
| Send secrets to the browser | Edge code is private; client code isn't |
Most platforms encrypt secrets at rest and inject them at runtime — they don't appear in logs unless you explicitly log them.
Dev Workflow
The standard workflow:
# Local dev — same runtime as production
wrangler dev # Cloudflare Workers
vercel dev # Vercel Edge Functions
deno run --watch ... # Deno Deploy
netlify dev # Netlify Edge Functions
fastly compute serve # Fastly Compute@EdgeTest against a local runtime that matches production. Don't develop on Node.js and discover Web-API differences at deploy time.
CI:
- Lint / type-check with
tscorbiome. - Unit tests with
vitestor your test runner. - Integration tests against a deployed preview environment.
- Smoke tests post-deploy hitting health endpoints.
Deploy Strategy
Edge functions deploy fast (5-20 seconds globally). That's a double-edged sword — easy to ship broken changes worldwide.
Mitigations:
- Preview deployments — Cloudflare Workers, Vercel, Netlify all support per-branch preview URLs. Test there first.
- Gradual rollout — Cloudflare Workers supports
wrangler deploy --gradual 25(Workers for Platforms / Versioning). - Same blue-green pattern as classic apps — deploy
worker-v2, swap routes, leavev1for rollback.
Cost
Edge function pricing is per-million-requests, very cheap at small scale, accumulating at large scale.
| Concern | Mitigation |
|---|---|
| Subrequests dominate | Cache aggressively; avoid fan-out per request |
| KV reads | Lower the chattiness; cache results in-isolate per request |
| Durable Object requests | Use sparingly — they're more expensive than KV |
| Image transformations | They're cheap per-transform but multiply with traffic |
| D1 / R2 / queue ops | Per-operation pricing — usually rounding error, watch at scale |
Cloudflare's Workers Paid plan ($5/month) covers most small-to-medium teams. Vercel and Deno Deploy similar generous tiers. At enterprise scale, costs scale proportional to request volume; far cheaper than equivalent Lambda usage.
Compatibility Gotchas
Edge runtimes are not Node.js. Common surprises:
| Looks like | Actually |
|---|---|
require('crypto').createHash(...) | Use crypto.subtle.digest(...) (Web Crypto) |
fs.readFile(...) | Doesn't exist; bundle assets or fetch from R2 / KV |
process.env.X | Often available, but prefer env.X (binding) |
| Express middleware | Doesn't work; build with Web API primitives or use itty-router / hono |
| Large npm packages | Many have Node-only deps; check engines, look for "isomorphic" variants |
The ecosystem has converged on Web-standard APIs. The fewer Node.js-specific dependencies you use, the easier the edge story.
Frameworks built for edge: Hono (excellent for Workers/Deno), itty-router (tiny), Next.js (Edge runtime), SvelteKit, Remix, Astro, Nuxt — all support edge deployment for their server logic.
When Edge Is Wrong
Honest signs you're using the wrong tool:
- You keep hitting CPU/memory/subrequest limits.
- You're polyfilling Node APIs.
- You need pinning to a region (data residency, single DB).
- Your bundle is 50 MB.
- You're trying to run a database / cache server (it's not a long-running process).
In these cases: move the workload to a regional container or Lambda; keep the edge for the request-shape work that does belong there.
Common Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Single-region DB calls from edge | Each request has 100+ ms tail latency | Cache reads at edge; do writes in a sidecar |
| Inline secrets | Leaked on Git push | Use wrangler secret / platform secrets |
| Big dependencies | Slow deploys, hit bundle limit | Trim deps; use lighter alternatives |
| Synchronous CPU-heavy work | Hit CPU limit, request fails | Move to origin |
| No error fallback | Edge bug = 500 for everyone | Top-level try/catch with origin fallback |
| Long-lived connections | They don't work; isolates are ephemeral | Use queue patterns, not long-poll |
| Per-isolate global state | Inconsistent across requests | Don't rely on in-isolate state; use KV / DO |
Checklist
Production edge functions checklist
- Workload genuinely fits the edge niche (request transformation, not heavy compute)
- Top-level try/catch with origin or static fallback
- Secrets via platform secret store, never inline
- Logs shipped off-edge to your central log store
- Tracing context (
traceparent) propagated through fetch - Errors reported to a tracker (Sentry, Datadog)
- Bundle size under platform limits, with headroom
- Local dev matches production runtime (
wrangler devetc) - Preview deployments tested before production
- Synthetic checks against deployed endpoints
- Documented limits and what to do when hitting them
- KV reads optimized; per-request caching of repeated lookups
- Cost dashboard reviewed; alerts on unexpected usage spikes
- Rollback path documented (versioning / blue-green /
wrangler rollback)