Table of Contents 8 Sections
Overview
This guide explains how to build and deploy a globally distributed API rate-limiting proxy using Cloudflare Workers and Durable Objects. Incoming client requests are throttled at the edge using a rolling sliding-window algorithm, stopping denial-of-service attempts and abusive traffic before it ever touches your origin server. Every technical term is defined in the Glossary below before use.
Budget about 25–35 minutes to set up Wrangler CLI, authenticate with Cloudflare, implement the Worker and Durable Object class, and configure bindings in `wrangler.toml`. Note: Cloudflare Durable Objects require a Workers Paid plan ($5/month). This guide has been written from technical documentation and general best practice, but — like every guide currently on Workflow Vault — it is UNVERIFIED: it has not yet been run end-to-end and confirmed working by Workflow Vault's own automated testing suite. Test thoroughly in development before routing production traffic.
Glossary
- Cloudflare Workers
- A serverless execution environment running JavaScript/Wasm on Cloudflare's global edge network within milliseconds of end users worldwide.
- Durable Objects
- A distributed compute and storage primitive on Cloudflare Workers that guarantees a single, globally coordinated in-memory instance for strict consistency and race-free state management.
- Rate Limiting
- The practice of restricting the frequency of requests a client can send to an API in a given time interval to prevent server overloads, brute force attacks, and runaway costs.
- Sliding Window Algorithm
- A rate-limiting strategy that records request timestamps and purges entries older than a rolling time window (such as 60 seconds), avoiding the abrupt boundary spikes common with fixed-window counters.
- Wrangler
- Cloudflare's official command-line interface (CLI) tool for creating, developing, testing, and deploying Workers projects and cloud resources.
- CF-Connecting-IP
- A standard HTTP header populated automatically by Cloudflare containing the original visitor's real public IP address before it passed through the proxy.
- Origin Server
- Your backend API server, database, or cloud application that handles business logic and that the edge proxy protects from excessive traffic.
- HTTP 429 Too Many Requests
- The standard HTTP response status code used by APIs to signal that a client has exceeded its allowed rate limit and should pause before making further requests.
- Binding
- A configuration in `wrangler.toml` that connects a Cloudflare resource (such as a Durable Object namespace or KV store) to an environment variable available inside your Worker code.
Prerequisites
- [You'll need this already] A Cloudflare account with a Workers Paid subscription ($5/month) to enable Durable Object bindings.
- [You'll need this already] Node.js 18+ and npm installed on your development machine.
- [You'll need this already] Wrangler CLI authenticated with your Cloudflare account (run `npx wrangler login` in your terminal).
- [You'll need this already] An origin API URL (or a test endpoint like https://httpbin.org) that you want to proxy requests to.
- [You'll need this already] Basic familiarity with modern JavaScript / TypeScript classes and async/await syntax.
- [Optional, not required] A load testing tool (such as curl, Postman, or autocannon) to simulate burst traffic.
Where to Run This Automation
Where should you run this automation?
Don't have 24/7 hardware at home? Choose the setup that fits your budget and technical comfort.
Best for Freelancers, Agencies, and 24/7 Client Pipelines
If you don't have a dedicated server running at home, a Virtual Private Server (VPS) gives you a dedicated Linux server in the cloud that stays online 24/7 even when your personal laptop is closed.
- Cost: ~$4 to $6 / month (predictable flat fee, no per-execution surprises).
- Providers: Hetzner Cloud, DigitalOcean, Linode / Akamai, Railway, or OVH.
- Key advantage: You get a permanent public IP address and free SSL certificates, which are essential for receiving incoming Stripe, Airtable, or GitHub webhooks reliably.
curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER Best for Hobbyists, Testing & Zero-Cost Experimentation
You can run this automation completely free on your everyday computer (Mac, Windows, or Linux) or on low-cost home hardware like a Raspberry Pi 4/5 or repurposed mini PC.
- Cost: $0 / month (100% free).
- Software needed: Docker Desktop (free for personal use) or native Node.js / Python.
- Consideration: Automations will only execute while your computer is powered on and awake. To receive incoming webhooks locally, use tools like
ngrokorCloudflare Tunnelsfor secure local tunneling.
Best for Zero-Maintenance 1-Click Operations
If you don't want to manage Docker containers or terminal commands, you can run automations directly on managed serverless platforms or official SaaS plans:
- Cloudflare Workers: Free tier includes 100,000 requests per day with 0ms cold starts.
- n8n Cloud / Make / Zapier: Fully managed hosted platforms with automated backups and visual scenario builders.
- Railway / Render: 1-click Git container deploys with auto-healing and managed PostgreSQL databases.
Initialize the Cloudflare Worker project
In your terminal, run the command below to scaffold a blank Cloudflare Worker project. Choose the 'Hello World' Worker template and select TypeScript or JavaScript based on your preference, then navigate into the newly created folder.
npm create cloudflare@latest rate-limit-proxy -- --type hello-world
cd rate-limit-proxy Using Cloudflare's official scaffolding tool configures the standard project directory, installs the latest `@cloudflare/workers-types`, and creates a baseline `wrangler.toml` file.
Running `ls` inside the directory shows `wrangler.toml`, `package.json`, and a `src` directory with an entrypoint file.
Implement the RateLimiter Durable Object class
Create or edit `src/limiter.js` (or `src/limiter.ts`) to define the `RateLimiter` class. This class maintains an array of recent request timestamps in memory. When a request arrives, it filters out timestamps older than 60,000 milliseconds (1 minute). If the remaining count equals or exceeds the limit (60 requests), it returns an HTTP 429 response; otherwise, it records the new timestamp and returns HTTP 200.
export class RateLimiter {
constructor(state, env) {
this.state = state;
this.env = env;
this.requests = [];
}
async fetch(request) {
const now = Date.now();
const windowMs = 60 * 1000; // 1 minute sliding window
const maxRequests = 60; // 60 requests allowed per window
// Evict timestamps outside the rolling window
this.requests = this.requests.filter(t => now - t < windowMs);
const remaining = Math.max(0, maxRequests - this.requests.length);
if (this.requests.length >= maxRequests) {
const oldest = this.requests[0];
const retryAfterSeconds = Math.ceil((oldest + windowMs - now) / 1000);
return new Response(JSON.stringify({
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
retryAfterSeconds
}), {
status: 429,
headers: {
'content-type': 'application/json',
'X-RateLimit-Limit': String(maxRequests),
'X-RateLimit-Remaining': '0',
'Retry-After': String(retryAfterSeconds)
}
});
}
// Record current request
this.requests.push(now);
return new Response(JSON.stringify({ allowed: true, remaining: remaining - 1 }), {
status: 200,
headers: {
'content-type': 'application/json',
'X-RateLimit-Limit': String(maxRequests),
'X-RateLimit-Remaining': String(remaining - 1)
}
});
}
} Durable Objects guarantee that all requests routed to a specific client ID execute against the exact same memory instance, preventing the race conditions that occur with eventual-consistency stores like KV.
The class exports a constructor accepting `state` and an async `fetch(request)` method that computes the sliding window accurately.
Create the main Worker reverse-proxy handler
In `src/index.js`, export the default Worker handler. Extract the client identifier from the `CF-Connecting-IP` header or an `x-api-key` header (falling back to a default identifier in local dev). Derive a Durable Object ID with `env.RATE_LIMITER.idFromName(clientId)`, call the stub, and either return the 429 response immediately or proxy the request to your backend origin.
import { RateLimiter } from './limiter.js';
export { RateLimiter };
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// Identify client by API Key or fallback to IP address
const clientKey = request.headers.get('x-api-key') ||
request.headers.get('CF-Connecting-IP') ||
'127.0.0.1';
// Get the Durable Object stub for this specific client
const doId = env.RATE_LIMITER.idFromName(clientKey);
const stub = env.RATE_LIMITER.get(doId);
// Check rate limit
const rateCheck = await stub.fetch(request.url);
if (rateCheck.status === 429) {
return rateCheck;
}
// Forward allowed request to origin
const originUrl = new URL(request.url);
originUrl.hostname = 'httpbin.org'; // Replace with your real origin host
originUrl.protocol = 'https:';
const proxyRequest = new Request(originUrl.toString(), request);
const originResponse = await fetch(proxyRequest);
// Clone response to attach rate limit header info
const response = new Response(originResponse.body, originResponse);
response.headers.set('X-RateLimit-Limit', '60');
response.headers.set('X-RateLimit-Remaining', rateCheck.headers.get('X-RateLimit-Remaining') || '0');
return response;
}
}; This acts as the gatekeeper. Blocked requests are terminated at the edge with near-zero latency, protecting your origin backend from processing unwanted load.
Incoming requests are evaluated by the Durable Object; valid requests receive an origin response decorated with `X-RateLimit` headers, while excessive requests receive a 429.
Configure wrangler.toml bindings and migrations
Open `wrangler.toml` and configure the Durable Object binding and class migration. Set `main = "src/index.js"`, add `[durable_objects]` with binding `RATE_LIMITER`, and define `[[migrations]]` with tag `v1` and `new_classes = ["RateLimiter"]`.
name = "rate-limit-proxy"
main = "src/index.js"
compatibility_date = "2024-09-23"
[durable_objects]
bindings = [
{ name = "RATE_LIMITER", class_name = "RateLimiter" }
]
[[migrations]]
tag = "v1"
new_classes = ["RateLimiter"] Cloudflare requires an explicit migration declaration when adding a new Durable Object class so the platform can provision and route the storage classes correctly.
`wrangler.toml` cleanly links the `RATE_LIMITER` binding to the `RateLimiter` class.
Deploy to Cloudflare and verify rate limiting
Deploy your Worker using `npx wrangler deploy`. Once published, run a quick loop in your terminal using `curl` to fire 65 requests in rapid succession to test threshold enforcement.
# Deploy the Worker to Cloudflare's edge
npx wrangler deploy
# Test with a quick 65-request burst
for i in {1..65}; do
echo -n "Request #$i: "
curl -s -o /dev/null -w "HTTP %{http_code}\n" https://rate-limit-proxy.YOUR_SUBDOMAIN.workers.dev/get
done Automating rapid test requests verifies that the first 60 requests return HTTP 200 with decreasing `X-RateLimit-Remaining` counts, and the subsequent requests immediately return HTTP 429 with appropriate retry headers.
The console shows HTTP 200 for requests 1 through 60, followed by HTTP 429 on requests 61 through 65.
Workflow architecture
Incoming HTTP requests hit Cloudflare's nearest edge data center. The Worker extracts the client's IP or API key and routes a subrequest to the corresponding Durable Object singleton. The Durable Object executes an in-memory sliding-window calculation in sub-millisecond time. If within limits, the request proceeds to your origin server; if over limit, the proxy immediately returns an HTTP 429 response with `Retry-After` headers, dropping the load before reaching origin infrastructure.
Final result
A high-performance edge rate limiter that strictly enforces request quotas (e.g. 60 req/min) per client with negligible latency overhead, shielding origin services from bursts and malicious scraping. As noted above, this guide is unverified by Workflow Vault — test against a staging endpoint before deploying to production.
Troubleshooting
All requests get rate-limited immediately during local testing (`wrangler dev`)
In local development environments, `CF-Connecting-IP` is often missing, causing all test requests to share an empty or undefined string as their Durable Object key.
Add a fallback identifier such as `request.headers.get('CF-Connecting-IP') || '127.0.0.1'` or pass a test `x-api-key: user123` header during development.
Deploy fails with error 'Durable Object migration required'
The `RateLimiter` class was declared in `wrangler.toml` without an accompanying `[[migrations]]` block.
Add `[[migrations]]` with `tag = "v1"` and `new_classes = ["RateLimiter"]` to your `wrangler.toml`.
Rate limits reset unexpectedly during a deployment
Deploying a new Worker version or cluster redeployment can restart the in-memory Durable Object instances.
For mission-critical billing enforcement across deploys, persist the timestamp array to `this.state.storage.put('timestamps', this.requests)` in the Durable Object.
Origin server returns CORS errors when 429 is returned
The 429 error response generated at the edge does not include Access-Control-Allow-Origin headers.
Add `'Access-Control-Allow-Origin': '*'` and `'Access-Control-Allow-Headers': '*'` to the 429 response headers in the Durable Object.