Edge Compute Demo

Cloudflare Developer Platform ← Demo

How It's Built

This demo runs entirely on Cloudflare's edge network — no origin server, no containers, no load balancers. Every component you see in the pipeline is a distinct Cloudflare primitive, wired together using platform-native features.

Architecture

Request flow

Browser
   │  fetch() from JS — session key in query param, never in browser URL
   ▼
DNS → DDoS Protection → WAF → CDN   ← Cloudflare network layer (automatic)
   │
   ▼
Entrypoint Worker  (main-worker.klym.workers.dev)
   │
   ├─── RPC ──▶  Rate Limiter Worker  ──▶  Durable Object  (persistent counter)
   │                    │
   │             allowed / blocked
   │
   ├─── RPC ──▶  Rewriter Worker
   │                    │
   │                    ├─── RPC ──▶  Normalize URL Worker  (lang prefix, path canonicalization)
   │                    │
   │             rewritten URL + request ID
   │
   └─── fetch ▶  Origin Worker  (demo HTML response)
   │
   ▼
Browser  ← headers carry pipeline trace, rewritten URL, rate-limit state
Cloudflare Network Layer

Before a request reaches any worker

🌐
DNS Product
Cloudflare is authoritative DNS for klym.net. The custom domain edge-demo.klym.net is a CNAME proxied through Cloudflare — the origin IP is never exposed.
🛡️
DDoS Protection Product
Automatic, always-on volumetric attack mitigation at Cloudflare's network edge. No configuration required — it absorbs L3/L4 floods before they reach the Workers runtime.
🔒
WAF Product
Web Application Firewall inspects HTTP requests for known attack patterns (OWASP Top 10, CVEs). Managed rulesets run before the request is handed to any Worker.
CDN / Cache Product
Static assets can be cached at Cloudflare's edge PoPs globally, reducing latency by serving content from the nearest point of presence rather than going all the way to the origin.
Workers

The pipeline is five Workers

Each Worker is a separate deployable unit with its own wrangler.toml. They run in V8 isolates — not containers — so cold starts are sub-millisecond and there is no per-instance billing.

🎯
Entrypoint Worker Orchestrator
Receives every inbound request. Enforces session keys, assembles the pipeline trace, calls Rate Limiter and Rewriter via RPC, then forwards to Origin.
Deployed as main-worker. The only Worker that is publicly addressable — all others are internal.
🚦
Rate Limiter Worker Worker + DO
Two-layer design: a RateLimiterWorker (WorkerEntrypoint) exposes typed RPC methods to the Entrypoint, and a RateLimiterDO (DurableObject) owns the persistent counter.
The Worker is the public RPC surface — checkLimit(key) and resetLimit(key). It resolves the DO stub via idFromName(key) and delegates the actual check to the DO.

The DO runs as a single-threaded actor. blockConcurrencyWhile in its constructor loads the persisted counter before any request is handled, preventing concurrent cold-starts from racing to read 0. Every increment is written to DO storage immediately, so the counter survives isolate eviction.

Fixed budget of 10 requests per session key — requests 1–10 pass, 11+ are blocked with a 429.
✏️
Rewriter Worker RPC
Detects deprecated URL patterns (/old/*, /legacy/*, /v1/*) and short-circuits with a redirect. Otherwise calls Normalize URL and generates a request ID.
Returns timing for its own execution and the Normalize URL sub-call so the trace can show them as separate hops.
🔤
Normalize URL Worker RPC
Lowercases the path, strips trailing slashes, and injects a language prefix from Accept-Language if the path doesn't already have one.
Called by the Rewriter — never directly by the Entrypoint. Supports: en, fr, de, es, ja.
🖥️
Origin Worker Response
Renders the demo HTML response. Receives the assembled pipeline trace as a request header and embeds it in the page. Acts as a stand-in for a real origin server.
In a production setup this would be your actual origin — a server, R2 bucket, or another Worker.
🎮
Demo UI Worker UI Host
Serves this interactive page. Completely decoupled from the pipeline — it makes fetch() calls to the Entrypoint from the browser, then animates the trace data from response headers.
No service bindings. The browser URL never changes regardless of what path you send.
Service Bindings & Workers RPC

How workers call each other

All inter-worker calls use Service Bindings — not HTTP. The call stays on Cloudflare's internal network, adds zero network latency, and is billed as a single request.

// wrangler.toml — declare the binding
[[services]]
binding = "RATE_LIMITER"
service = "rate-limiter-worker"
entrypoint = "RateLimiterWorker"

// TypeScript — call the remote method as if it were local
const { allowed, remaining } = await env.RATE_LIMITER.checkLimit(cookieId);

Workers RPC requires the callee to extend WorkerEntrypoint and expose public async methods. Cloudflare generates the type-safe stub automatically from the exported class — no schema, no codegen step.

🔗
Service Bindings Feature
Declared in wrangler.toml at deploy time. The binding name becomes a typed property on env. No URLs, no auth tokens, no network hops.
📞
Workers RPC Feature
Typed method calls across Workers using the WorkerEntrypoint class. Requires compatibility_date ≥ 2024-04-03. Arguments and return values are serialized transparently.
Durable Objects

Consistent state across all isolates

Workers are stateless by design — each request may land in a different isolate, anywhere in Cloudflare's network. Durable Objects solve this by giving you a single-threaded actor with persistent storage that all isolates coordinate through.

Session key as DO name. RATE_LIMITER_DO.idFromName(cookieId) maps each session to exactly one Durable Object instance worldwide. All requests for that session hit the same actor.
blockConcurrencyWhile in the constructor. Loads the persisted counter before any request is handled, serializing concurrent initializations. Prevents the counter from being read as 0 by two simultaneous requests.
Storage API for durability. ctx.storage.put('count', this.count) persists the counter to DO storage after every increment. The value survives isolate eviction.
Fixed budget, not time-windowed. The counter only resets when you click New Session, which fires a resetLimit() RPC. This makes the demo deterministic — requests 1–10 always pass, 11+ always block.