Edge Functions Deep Dive: Cloudflare Workers, Deno Deploy and Vercel Edge

Running code at the edge sounds simple until you have to decide where it actually runs, what it can access, and how much it will cost you when it scales. Over the last three years I have shipped edge functions on all three major platforms for use cases ranging from A/B testing and bot filtering to personalized HTML assembly. What I learned is that Cloudflare Workers, Deno Deploy, and Vercel Edge Functions solve similar problems with very different primitives. This article breaks down how each platform executes code, where their limits will catch you, and when to choose one over the others for production traffic.

Why Edge Functions Are Not Tiny Origins: The Isolate vs Container Distinction

Most engineers coming from AWS Lambda expect edge functions to be small containers running Node.js near users. That mental model will lead you to the wrong decisions. Cloudflare Workers, Deno Deploy, and Vercel Edge Functions all run on V8 isolates, not microVMs or containers. An isolate is a lightweight, sandboxed instance of the V8 JavaScript engine. Startup time is measured in single-digit milliseconds because there is no operating system to boot, no container to pull, and no Node process to warm up. In my experience, that difference only matters until it matters a lot — at 5,000 requests per second with spiky traffic, the isolate model prevents the cold start cascade I have seen cripple container-based serverless.

The tradeoff is that isolates share the underlying host process and must enforce strict CPU and memory limits. You do not get a full POSIX environment. There is no file system beyond what the runtime emulates, no native addons, and limited access to long-lived TCP connections. If you need sharp for image processing or prisma with a direct database connection, you will hit a wall quickly. This is why understanding the runtime constraints before you port code is critical. The edge is ideal for request transformation, routing, authentication, and lightweight personalization — not for heavy computation or long-running jobs.

The Web Standards Alignment

All three platforms intentionally converge on Web Platform APIs rather than Node.js APIs. Your edge function receives a standard Request and returns a Response via the Fetch API. This is deliberate. By implementing fetch, Request, Response, URL, TextEncoder, and the Streams API, code becomes portable across runtimes. I have moved a JWT verification function from Cloudflare to Deno with almost no changes because it only used Web Crypto Subtle. When you need Node compatibility, each platform provides a different level of shim support, but I recommend treating Node APIs as a compatibility layer, not a target. Writing to WinterCG-compatible Web APIs as described in the HTTP Working Group Specifications will keep your functions portable and future-proof.

Where WebAssembly Fits

When you need to run compiled languages at the edge, WebAssembly closes the gap left by isolates. All three platforms support Wasm modules, which lets you run Rust, Go, or C++ logic without leaving the isolate sandbox. I have used a Rust-compiled HTML parser via Wasm on Cloudflare to rewrite content faster than any regex-based approach. If this is on your roadmap, read more in WebAssembly at the Edge: Running Wasm Modules on CDN Networks before deciding on a build pipeline, as toolchain choices differ significantly between Workers and Deno.

Cloudflare Workers: Bindings, Storage, and the 50ms CPU Wall

Cloudflare Workers was the first mature isolate-based edge platform and it remains the most feature-complete for CDN-adjacent work. The core concept you must understand is bindings. A Worker does not connect to external services with environment variables and SDK clients in the traditional sense. Instead, you declare resources like KV namespaces, R2 buckets, Durable Objects, D1 databases, and Queues in your wrangler.toml, and the runtime injects them as bindings into your code. This eliminates connection setup latency and credential management inside the handler.

The classic use case is a Worker that checks the cache, validates a token, and fetches from origin only on a miss. The CPU clock is what surprises most teams. On the free and standard Workers plan, your code is limited to 50ms of CPU time per request (not wall time — actual time spent executing JavaScript). Wall time can be much longer because you can await fetch() for seconds while the CPU is idle. I have found that 50ms is generous for header manipulation and routing, but it forces you to be careful with JSON parsing, regex, and crypto on large payloads. The paid Unbound model removes this limit and bills for CPU time instead.

// Cloudflare Worker: JWT check + cache-aware fetch with KV
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    // 1. Check for cached personalized response in KV or Cache API
    const cache = caches.default;
    let response = await cache.match(request);
    if (response) {
      return response;
    }

    // 2. Lightweight JWT verification using Web Crypto
    const token = request.headers.get('cookie')?.match(/session=([^;]+)/)?.[1];
    if (token) {
      const valid = await verifyJWT(token, env.JWT_PUBLIC_KEY);
      if (!valid) {
        return new Response('Unauthorized', { status: 401 });
      }
    }

    // 3. Fetch from origin and cache with custom TTL
    response = await fetch(request);
    // Don't block response on cache write
    ctx.waitUntil(cache.put(request, response.clone()));
    return response;
  }
}

async function verifyJWT(token, publicKey) {
  // Web Crypto implementation - runs within CPU limit
  const [header, payload, signature] = token.split('.');
  // ... verification logic using crypto.subtle
  return true;
}

Storage Primitives and Consistency

KV is eventually consistent and optimized for reads. It is excellent for feature flags, blocklists, and configuration that changes infrequently. For state that requires strong consistency, like a shopping cart or a counter, you need Durable Objects. A Durable Object is a single-threaded isolate with transactional storage that provides coordination. In my experience, teams misuse KV for session state and then struggle with read-after-write delays that take up to 60 seconds to propagate globally. Choose Durable Objects when correctness matters, KV when scale and read latency matter.

Deno Deploy's Permission Model and Fresh Approach to Edge State

Deno Deploy is the most opinionated of the three, built directly on the Deno runtime. If you have used Deno locally, the deploy experience feels native. The biggest differentiator is the permission model. Locally, Deno requires explicit flags like --allow-net and --allow-read. On Deploy, permissions are inferred from your code and dashboard configuration, but the security mindset carries over. Imports are via URLs and npm: specifiers, and there is no hidden package installation step. This makes dependency auditing far more transparent than a bundled Node deployment.

Deploy runs on the same V8 isolate architecture but exposes Deno's broader standard library and first-class TypeScript without a build step. I've found that to be a significant productivity gain for teams that want type safety without a webpack or esbuild pipeline. The runtime supports Deno.openKv, a native key-value store built on FoundationDB that is strongly consistent and available directly in the edge function. Unlike Cloudflare's KV, Deno KV supports atomic transactions and secondary indexes out of the box, which simplifies edge state management for things like rate limiting and feature store lookups.

// Deno Deploy: Geolocation + atomic rate limiting with Deno KV
export default {
  async fetch(request) {
    const kv = await Deno.openKv();
    const ip = request.headers.get("x-forwarded-for")?.split(",")[0] ?? "127.0.0.1";
    
    // Atomic increment for rate limiting
    const key = ["rate_limit", ip];
    const { value } = await kv.get(key);
    const count = (value as number) ?? 0;

    if (count > 100) {
      return new Response("Too Many Requests", { status: 429 });
    }
    
    await kv.atomic().check({ key, versionstamp: null }).set(key, count + 1).commit();

    // Native geolocation without lookup service
    // Deno Deploy injects x-geo headers at the edge
    const country = request.headers.get("x-deno-geo-country-code") ?? "US";
    const headers = new Headers();
    headers.set("x-country", country);
    headers.set("content-type", "text/html");
    
    return new Response(`<h1>Hello from ${country}</h1>`, { headers });
  }
}

Broad NPM Compatibility Without the Baggage

Deno Deploy now supports npm: imports for most packages, but you should still verify compatibility. Packages that rely on node:fs, child_process, or native bindings will not work. I have had success running zod, hono, and std/http at the edge, but had to replace jsonwebtoken with a Web Crypto-based implementation. The lesson I apply everywhere: if a library can run in a modern browser, it will probably run on Deno Deploy. If it requires Node internals, find an alternative before you commit.

Vercel Edge Runtime: Where Edge Functions Meet the Frontend Framework

Vercel Edge Functions are designed to feel invisible inside a Next.js or SvelteKit project. You do not think about deploying an edge function — you export a config object with runtime: 'edge' and Vercel splits it to the edge network. This tight framework integration is both its greatest strength and its most common source of confusion. An Edge Function on Vercel is not a replacement for a standalone CDN worker; it is middleware and route handler logic co-located with your frontend.

Under the hood, Vercel's Edge Runtime is also based on V8 isolates and implements the same Web APIs. It is derived from the same codebase as Cloudflare Workers, which is why code is often portable between them. The key difference is the surrounding infrastructure. Edge Functions automatically share the build pipeline, environment variables, and analytics of your Vercel project. You get automatic integration with Vercel's Data Cache, Incremental Static Regeneration, and Edge Config. For teams already on Vercel, this eliminates operational overhead. For teams looking for a general-purpose edge compute layer independent of a frontend, it can feel restrictive.

// Vercel Edge Middleware: A/B test assignment without origin round-trip
// middleware.ts in a Next.js app
import { next } from '@vercel/edge';
import type { NextRequest } from 'next/server';

export const config = {
  matcher: '/:path*',
};

export function middleware(request: NextRequest) {
  const bucket = request.cookies.get('ab-bucket')?.value;
  
  if (bucket) {
    // User already assigned, add header for downstream rendering
    const response = next();
    response.headers.set('x-ab-bucket', bucket);
    return response;
  }

  // Assign new bucket at the edge - no origin fetch needed
  const newBucket = Math.random() < 0.5 ? 'control' : 'variant';
  const response = next();
  response.cookies.set('ab-bucket', newBucket, { 
    path: '/', 
    maxAge: 60 * 60 * 24 * 30 
  });
  response.headers.set('x-ab-bucket', newBucket);
  return response;
}

The 4MB Code Limit and Framework Overhead

Vercel Edge Functions have a 1-4MB compressed code size limit depending on your plan, which includes the runtime and your framework code. In practice, a Next.js Edge Function bundles a portion of Next.js itself, so your actual budget is smaller. I have run into this limit when trying to bundle large ML tokenizers or PDF parsers to the edge. Cloudflare and Deno give you more headroom for pure compute tasks. If your edge logic is primarily routing, personalization, and lightweight rewriting, Vercel's model is efficient. If you plan to ship large Wasm binaries or heavy libraries, test bundle size early. For complex assembly logic, you may want to combine Vercel Edge Middleware with Edge-Side Includes in Modern CDNs: Dynamic Content Assembly at the Edge rather than doing everything in one function.

Cold Starts, CPU Limits, and Egress: Picking the Right Platform for the Workload

Marketing pages will tell you all three platforms have zero cold starts. That is mostly true compared to containers, but there are nuances worth understanding before you pick one for a latency-sensitive path. Isolates start fast, but code still needs to be evaluated. A 2MB bundle with heavy initialization will take longer to become ready than a 10KB function. In my benchmarks, median cold start is under 5ms for all three, but p99 can spike to 15-25ms under global eviction. The practical takeaway: keep initialization outside the fetch handler and lazy-load heavy dependencies.

The more important limits are CPU time, memory, and egress. This is where cost and architecture decisions actually diverge. The table below summarizes the constraints I evaluate on every new project. These limits change, so always verify in the provider documentation before sizing, but the relative tradeoffs have remained consistent.

Capability Cloudflare Workers Deno Deploy Vercel Edge Functions
Runtime Core V8 Isolates + Service Worker API V8 Isolates + Deno API V8 Isolates (Edge Runtime)
CPU Time Limit (per request) 50ms (Bundled) / Unbounded (Unbound, billed) 50ms default, up to 400ms configurable 50ms soft limit, hard timeout 25s wall time
Memory Limit 128 MB ~256 MB 128 MB
Max Bundle / Script Size 10 MB (paid) / 1 MB (free) No hard limit, ~20MB recommended 4 MB compressed (incl. framework)
Stateful Storage KV (eventual), Durable Objects (strong), R2, D1 Deno KV (strong, transactional) Edge Config, KV (via Upstash), Postgres
Ideal Workload High-volume CDN logic, bot protection, rewriting Transactional edge APIs, stateful edge apps Framework middleware, personalization, A/B routing

Cost is the other hidden factor. Cloudflare does not charge for egress, which makes it dramatically cheaper for bandwidth-heavy transformations like image resizing or HTML rewriting. Deno and Vercel charge for egress and for function invocations in different ways. I moved a high-traffic image optimization workload from a container-based origin to Cloudflare Workers specifically because egress fees on the previous platform exceeded compute costs by 4x. For authentication or routing logic that returns small responses, the difference is negligible. For anything that touches assets, model it carefully.

Geolocation is another practical consideration. All three platforms expose country and region at the edge, but with different APIs. Cloudflare provides request.cf.country, Deno injects x-deno-geo-* headers, and Vercel exposes request.geo in the Edge Runtime. If you need fine-grained routing without a redirect, see Geolocation Routing at the Edge: Country-Level Logic Without Redirects for patterns that work consistently across providers. In my experience, you should never call an external GeoIP API from an edge function — use the platform's native signal and keep the handler under 5ms.

Debugging, Observability, and Deployment Patterns I Rely On at the Edge

Edge functions fail differently than origin code. You cannot SSH into a host, tail a log file, or attach a debugger in production. Each platform has invested in tooling, but you need to build observability in from day one. At a minimum, I always add structured logging with request IDs, a sampling-based analytics pipeline, and synthetic monitors from multiple regions.

Cloudflare's wrangler tail and Logpush give you real-time and historical logs. Workers Analytics Engine lets you emit custom events that you can query with SQL. Deno Deploy provides integrated log drains and a nice integration with OpenTelemetry. Vercel surfaces edge function logs alongside build logs and provides Web Vitals correlation in the dashboard. None of these replace a proper tracing solution. I route a 1% sample of edge invocations to a collector like Honeycomb or Axiom with trace context propagated via headers. The web.dev Articles on performance monitoring offer solid guidance on correlating edge timing with Core Web Vitals.

Testing and Preview Strategies

Never trust local emulation alone. Miniflare, deno task, and vercel dev all emulate the edge environment, but they cannot replicate global network conditions or the exact CPU constraints of production isolates. I use a three-stage pattern: unit tests with the platform's local emulator, preview deployments on a *.workers.dev or *.deno.dev subdomain with synthetic traffic, and finally a staged rollout using gradual traffic shifting. Cloudflare supports gradual rollouts via Workers versions, Deno has deployment branching, and Vercel has preview deployments per git branch. All three let you test with real user geography before you impact all traffic.

When Not to Use Edge Functions

Edge functions are not the right tool for everything labeled serverless edge. If you need long-running connections, WebSockets with persistence beyond the request, heavy file processing, or direct VPC access to a private database, you will fight the platform. In those cases, keep the logic at the origin or use a container-based edge compute offering like Cloudflare Workers with Python or Fly.io Machines. I also avoid putting complex authentication flows entirely at the edge. Doing initial JWT verification at the edge is perfect — as detailed in Edge Authentication: JWT Verification and Token Validation at CDN Layer — but token refresh and session management often belong closer to your identity provider. The edge should be a fast filter and router, not your monolith rehosted in 300 cities.

Frequently Asked Questions

Can I run the same edge function code on Cloudflare Workers, Deno Deploy, and Vercel Edge without changes?

For simple Fetch API handlers that use only Web Platform APIs like Request, Response, and crypto.subtle, portability is high — often requiring only import path changes. Issues arise when you use platform-specific bindings such as Cloudflare KV or Deno KV, Node.js compatibility shims, or framework features like Next.js middleware helpers. I recommend isolating platform-specific storage and config behind an adapter so the core handler logic stays portable. Test with WinterCG compliance in mind.

How do I handle persistent state or sessions at the edge without an origin database?

Each platform offers edge-native storage. Cloudflare provides KV for eventually consistent reads and Durable Objects for strongly consistent transactional state. Deno Deploy has Deno KV which is strongly consistent and supports atomic transactions natively. Vercel offers Edge Config for read-heavy configuration and integrates with external KV stores like Upstash Redis. For sessions, I typically store a signed session ID in a cookie, verify it at the edge with Web Crypto, and keep the actual session data in the edge store with a short TTL to avoid an origin hop on every request.

What actually counts against the 50ms CPU limit?

Only time the isolate spends executing your JavaScript counts. Waiting on fetch, reading from KV or cache, and streaming responses do not count toward CPU time. What does count is parsing JSON, running regex, looping over large arrays, verifying signatures, and executing Wasm outside of I/O waits. Wall time can be many seconds even with a 50ms CPU limit. Profile with the platform's CPU timing tools — Cloudflare shows CPU time in the dashboard, Deno logs it per isolate, and Vercel includes it in function metrics — and move heavy work to a build step or origin when possible.

Are edge functions more expensive than traditional serverless at scale?

It depends on your workload pattern. For high request volume with small, fast responses like routing, A/B assignment, or bot checks, edge functions are almost always cheaper because you pay per invocation and avoid origin egress. For bandwidth-heavy responses, Cloudflare's zero egress model is significantly cheaper than Vercel or Deno Deploy. For infrequent but computationally heavy tasks, traditional serverless with higher memory and longer timeouts can be more cost-effective. I always model cost with realistic p95 payload sizes and cache hit ratios before committing.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker