All Workflows / Cloudflare Workers
Cloudflare Workers
Advanced 20 min READ

Rate-limited API proxy at the edge

Use Workers and Durable Objects to throttle requests before they reach your origin.

Target Architecture

A Cloudflare Worker reverse-proxy deployed across Cloudflare's edge network that intercepts incoming API traffic, derives a client identity from their IP address or API key, coordinates state with a lightweight Durable Object instance to enforce a strict sliding window limit (such as 60 requests per minute), and returns HTTP 429 Too Many Requests with rate-limit headers whenever the limit is reached.

INTEGRATED RUNTIMES:

Tools used

Cloudflare Workers Durable Objects Wrangler
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.

Before you begin

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.

Hardware & Cloud Guide

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.
Quick 1-liner to install Docker on any Ubuntu/Debian VPS: curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER
RECOMMENDED SPECS
RAM: 1GB – 2GB
vCPU: 1 Core
Storage: 20GB SSD
Uptime: 99.9%

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 ngrok or Cloudflare Tunnels for secure local tunneling.
LOCAL CHECKLIST
Docker Desktop: Installed
RAM Overhead: ~400MB
Webhooks: Tunnel required

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.
SERVERLESS TIERS
Cloudflare: 100k req/day free
GitHub CI: 2,000 min/mo free
Maintenance: Zero OS updates

Step-by-step implementation

Click the numbered phase buttons as you complete each step to track your live progress.

Phase Progress 0/5 completed (0%)
PHASE 1

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.

bash
npm create cloudflare@latest rate-limit-proxy -- --type hello-world
cd rate-limit-proxy
Why this matters

Using Cloudflare's official scaffolding tool configures the standard project directory, installs the latest `@cloudflare/workers-types`, and creates a baseline `wrangler.toml` file.

Expected Output

Running `ls` inside the directory shows `wrangler.toml`, `package.json`, and a `src` directory with an entrypoint file.

PHASE 2

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.

javascript
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)
      }
    });
  }
}
Why this matters

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.

Expected Output

The class exports a constructor accepting `state` and an async `fetch(request)` method that computes the sliding window accurately.

PHASE 3

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.

javascript
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;
  }
};
Why this matters

This acts as the gatekeeper. Blocked requests are terminated at the edge with near-zero latency, protecting your origin backend from processing unwanted load.

Expected Output

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.

PHASE 4

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"]`.

toml
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"]
Why this matters

Cloudflare requires an explicit migration declaration when adding a new Durable Object class so the platform can provision and route the storage classes correctly.

Expected Output

`wrangler.toml` cleanly links the `RATE_LIMITER` binding to the `RateLimiter` class.

PHASE 5

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.

bash
# 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
Why this matters

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.

Expected Output

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.

Recommended Extensions & Scaling

  • Implement tiered rate limits based on API key tiers (e.g., Free: 30 req/min, Pro: 300 req/min) stored in Workers KV.
  • Persist timestamp counters to Durable Object storage (`state.storage`) to maintain count continuity across Worker hibernations.
  • Add geographic rate limiting or IP reputation scoring using Cloudflare's `request.cf.botManagement` scores.

Troubleshooting

⚠ All requests get rate-limited immediately during local testing (`wrangler dev`)
Root Cause

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.

Solution

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'
Root Cause

The `RateLimiter` class was declared in `wrangler.toml` without an accompanying `[[migrations]]` block.

Solution

Add `[[migrations]]` with `tag = "v1"` and `new_classes = ["RateLimiter"]` to your `wrangler.toml`.

⚠ Rate limits reset unexpectedly during a deployment
Root Cause

Deploying a new Worker version or cluster redeployment can restart the in-memory Durable Object instances.

Solution

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
Root Cause

The 429 error response generated at the edge does not include Access-Control-Allow-Origin headers.

Solution

Add `'Access-Control-Allow-Origin': '*'` and `'Access-Control-Allow-Headers': '*'` to the 429 response headers in the Durable Object.