Implementing Rate Limiting in Edge Functions
Introduction to Edge-Based Rate Limiting
Protecting modern API gateways from distributed denial-of-service (DDoS) attacks, malicious bots, and general resource exhaustion is a primary concern for system architects. Traditional rate-limiting systems operate deep within the backend infrastructure, often situated behind load balancers and application servers. While functional, this design is structurally inefficient.
By the time a request is rate-limited, it has already traversed the network edge, consumed critical bandwidth, initiated TLS handshakes, and utilized backend threads. Implementing rate limiting directly in edge functions changes this paradigm completely. By evaluating and intercepting requests at the closest geographical point of presence (PoP), engineers can drop unauthorized traffic in milliseconds, preserving origin resources and minimizing operational costs.
Selecting the Token Bucket Algorithm
To implement rate limiting at the edge, developers must select an algorithm that balances accuracy, memory utilization, and computational complexity. The three most common patterns are Fixed Window, Sliding Window, and Token Bucket. The Fixed Window algorithm is simple to implement but suffers from traffic bursts at window boundaries.
The Sliding Window approach is more accurate but requires storing individual timestamps for every request, which can quickly exhaust edge memory. The Token Bucket algorithm is widely considered the industry standard for API rate limiting. It models capacity as a bucket that holds a maximum number of tokens. Tokens are continuously added to the bucket at a constant refill rate.
When a request arrives, the rate limiter attempts to draw a token from the bucket. If tokens are available, the request is allowed; if the bucket is empty, the request is rejected with an HTTP 429 Too Many Requests status.
The core engineering challenge of implementing the Token Bucket algorithm at scale is managing state without introducing database bottlenecks. Running a background cron process to refill buckets globally is impractical in a serverless edge environment. Instead, rate limiters calculate bucket capacity dynamically on every incoming request.
By storing only two values—the timestamp of the last processed request and the remaining token count—we can determine the current token level mathematically. The formula calculates the time elapsed since the last request, multiplies it by the refill rate, adds the result to the previous token count (capping it at the bucket's maximum capacity), and then subtracts one token for the current request.
Technical Implementation at the Edge
Let us examine a highly optimized implementation of a dynamic Token Bucket rate limiter designed to run on edge runtimes using a fast key-value store like Upstash Redis. This script reads the client's IP address, fetches the bucket state, performs the math, updates the store, and returns the appropriate headers:
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
export async function handleRequest(request) {
const ip = request.headers.get('cf-connecting-ip') || 'anonymous';
const key = `ratelimit:${ip}`;
const maxCapacity = 10;
const refillRatePerSecond = 0.5; // Refills 1 token every 2 seconds
const now = Date.now() / 1000; // Current time in seconds
// Fetch current state: [tokens, lastUpdatedTimestamp]
let [tokens, lastUpdated] = await redis.hmget(key, 'tokens', 'lastUpdated');
if (tokens === null || lastUpdated === null) {
// First request from this IP: initialize bucket to max capacity
tokens = maxCapacity;
lastUpdated = now;
} else {
tokens = parseFloat(tokens);
lastUpdated = parseFloat(lastUpdated);
// Calculate dynamic refill
const deltaSeconds = Math.max(0, now - lastUpdated);
const refilled = deltaSeconds * refillRatePerSecond;
tokens = Math.min(maxCapacity, tokens + refilled);
}
// Check if we have enough tokens to process this request
if (tokens >= 1) {
tokens -= 1;
lastUpdated = now;
// Persist new state back to the edge Redis instance with a TTL
await redis.hmset(key, { tokens, lastUpdated });
await redis.expire(key, 60); // Clean up inactive keys after 60 seconds
return new Response('Access granted', {
status: 200,
headers: {
'X-RateLimit-Limit': maxCapacity.toString(),
'X-RateLimit-Remaining': Math.floor(tokens).toString()
}
});
} else {
// Request blocked
return new Response('Too Many Requests', {
status: 429,
headers: {
'Retry-After': Math.ceil((1 - tokens) / refillRatePerSecond).toString(),
'X-RateLimit-Limit': maxCapacity.toString(),
'X-RateLimit-Remaining': '0'
}
});
}
}
HTTP Headers and State Reconciliation
A production-ready edge rate limiter must communicate its status back to the client using standardized HTTP headers:
- X-RateLimit-Limit: Indicates the maximum number of requests the client is allowed to make within a given time window.
- X-RateLimit-Remaining: Displays the number of tokens or requests remaining in the client's current quota.
- Retry-After: Injected during HTTP 429 responses to tell the client the exact number of seconds to wait before attempting another request.
- X-RateLimit-Reset: Specifies the epoch timestamp indicating when the current rate limit window will reset or refill completely.
Deploying this configuration globally introduces synchronicity trade-offs. If the edge functions query a single, centralized database, the rate-limiting step will add considerable latency to every API call, negating the performance benefits of edge computing.
To solve this, developers use globally replicated KV stores or localized caching. However, global replication introduces eventual consistency. A client could potentially bypass the rate limit by spreading requests across different geographical PoPs before the state propagates.
For standard APIs, this minor window of inconsistency is an acceptable trade-off for sub-millisecond execution times. For high-security endpoints (like login or payment APIs), developers configure the edge function to connect to a centralized, transaction-safe storage engine like Cloudflare Durable Objects, which enforces strong consistency for specific keys globally.
Furthermore, implementing rate limiting at the edge enables developers to construct dynamic, context-aware traffic management policies. Because edge functions run in a full programming environment, they can read headers, cookies, and request bodies to adjust limits on the fly.
For instance, authenticated enterprise users can be granted larger buckets with faster refill rates, while anonymous traffic or requests with suspected bot signatures can be throttled aggressively. Additionally, headers can be injected to communicate rate limits back to downstream application servers, allowing them to coordinate their load shedding and internal caching policies dynamically.
Edge Traffic Controls and Security at Bramsley
Mitigating traffic surges and DDoS vectors before they impact your origin requires a unified edge defense. Here is how Bramsley Digital Studio secures and accelerates enterprise APIs at the network periphery:
- Sub-Millisecond Execution: Deploying stateless rate limiters in optimized WebAssembly workers.
- Dynamic Policy Evaluation: Checking client headers, cookies, and tokens in real time to adjust bucket sizes.
- Distributed State Management: Replicating rate-limit quotas globally using edge-peered KV and cache clusters.
Our systems architects build customized, zero-latency security layers tailored to your application's concurrency needs. Contact us at bramsley.studio to fortify your edge infrastructure today.