Rate Limiting Webhooks
title: "Rate Limiting Webhook Ingestion" description: "Learn how to implement inbound rate limiting for webhook consumers to prevent overload, ensure fair resource allocation, and maintain system stability." weight: 26 date: 2026-06-28 lastmod: 2026-06-28 tags: ["apis", "webhooks"]
Webhook consumers face unpredictable traffic patterns. A provider may send a burst of events, or multiple providers may send simultaneously. Inbound rate limiting protects consumer infrastructure from overload, ensures fair processing across tenants, and maintains system stability under high load.
## What You'll Learn
- Implement token bucket and sliding window rate limiting for webhook ingestion
- Configure per-source and global rate limits
- Handle rate-limited requests with proper backpressure signaling
- Monitor and adjust rate limits based on system capacity
## Why It Matters
Without inbound rate limiting, a traffic spike from a single provider can overwhelm your webhook consumer, causing cascading failures across all integrations. Rate limiting provides predictable resource usage, fair multi-tenant isolation, and graceful degradation under load.
## Real-World Use
- API gateways (Kong, AWS API Gateway) apply rate limits to webhook endpoints per source IP
- Stripe suggests consumers return 429 Too Many Requests if processing capacity is exceeded
- GitHub webhook documentation recommends consumers respond with 429 to signal backpressure
- Message brokers use consumer prefetch limits to control inbound message flow
## Mermaid Flow
```mermaid
graph LR
A[Incoming Webhook] --> B{Source Identified}
B --> C[Check Rate Limit]
C --> D{Within Limit?}
D -->|Yes| E[Process Event]
D -->|No| F[Return 429]
F --> G[Retry-After Header]
E --> H[Return 200]
G --> I[Provider Retries Later]
Teacher's Corner
Explain that rate limiting is not just about protection, it is also about signaling. The 429 status code with a Retry-After header tells the provider exactly when to retry, reducing overall system load. Compare token bucket (allows bursts) vs. fixed window (simpler but allows double bursts at boundaries).
Code Examples
Example 1: Token Bucket Rate Limiter
import time
import threading
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate
self.capacity = capacity
self.tokens = capacity
self.last_refill = time.time()
self.lock = threading.Lock()
def consume(self, tokens=1):
with self.lock:
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.capacity,
self.tokens + elapsed * self.rate)
self.last_refill = now
if self.tokens >= tokens:
self.tokens -= tokens
return True
return False
bucket = TokenBucket(rate=10, capacity=20)
for i in range(25):
allowed = bucket.consume()
print(f"Request {i+1}: {'Allowed' if allowed else 'Rate limited'}")
if (i + 1) % 5 == 0:
time.sleep(0.5)
Expected Output: First 20 requests allowed, then rate limited until tokens replenish at 10 per second.
Example 2: Per-Source Rate Limiting Middleware
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
class PerSourceRateLimiter:
def __init__(self, default_limit=100, window=60):
self.limits = {}
self.default_limit = default_limit
self.window = window
def check(self, key):
now = time.time()
if key not in self.limits:
self.limits[key] = []
self.limits[key] = [
t for t in self.limits[key]
if now - t < self.window
]
if len(self.limits[key]) >= self.default_limit:
return False
self.limits[key].append(now)
return True
limiter = PerSourceRateLimiter(default_limit=5, window=10)
@app.route("/webhook", methods=["POST"])
def webhook():
source = request.headers.get("X-Source", request.remote_addr)
if not limiter.check(source):
return jsonify({
"error": "rate limit exceeded",
"retry_after": 10
}), 429
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(port=5000)
Expected Output: After 5 requests from the same source within 10 seconds, returns 429 with Retry-After.
Example 3: Adaptive Rate Limiting Based on System Load
import psutil
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
class AdaptiveRateLimiter:
def __init__(self, base_limit=100, cpu_threshold=80, mem_threshold=85):
self.base_limit = base_limit
self.cpu_threshold = cpu_threshold
self.mem_threshold = mem_threshold
self.requests = {}
def get_current_limit(self):
cpu = psutil.cpu_percent(interval=0.1)
mem = psutil.virtual_memory().percent
if cpu > self.cpu_threshold or mem > self.mem_threshold:
return int(self.base_limit * 0.5)
return self.base_limit
def check(self, key):
limit = self.get_current_limit()
now = time.time()
if key not in self.requests:
self.requests[key] = []
self.requests[key] = [
t for t in self.requests[key]
if now - t < 60
]
if len(self.requests[key]) >= limit:
return False, limit
self.requests[key].append(now)
return True, limit
limiter = AdaptiveRateLimiter(base_limit=100)
@app.route("/webhook", methods=["POST"])
def webhook():
allowed, limit = limiter.check(request.remote_addr)
if not allowed:
return jsonify({
"error": "rate limited",
"limit": limit
}), 429
return jsonify({"status": "ok"}), 200
Expected Output: Under normal CPU/memory, limit is 100 req/min. Under high load, limit drops to 50 req/min.
Common Mistakes
- Using a single global rate limit that does not account for multi-tenant workloads
- Not including a Retry-After header in 429 responses, leaving providers guessing when to retry
- Implementing rate limiting only at the application level without infrastructure-level protection
- Rate limiting healthy sources because of a noisy neighbor tenants
- Not monitoring rate limit hit rates to detect legitimate capacity issues
- Applying the same rate limit to all endpoints, including non-webhook routes
- Forgetting to clean up stale rate limit state, causing memory leaks
Practice Questions
- What is the difference between token bucket and sliding window rate limiting?
- Why should 429 responses include a Retry-After header?
- How does adaptive rate limiting improve system resilience?
- What are the trade-offs of per-source vs. global rate limiting?
- Challenge: Implement a two-tier rate limiter for a webhook consumer: a fast token bucket for burst protection (100 req/s burst, 50 req/s sustained) and a sliding window for daily quota (100,000 req/day per source). Return appropriate headers showing remaining quota.
Answer Key
1. Token bucket allows bursts up to capacity and refills at a steady rate. Sliding window counts requests in a rolling time window and is simpler but can allow double bursts at boundaries. 2. Retry-After tells the provider the exact duration to wait before retrying, preventing immediate retry storms and coordinating backoff across all providers. 3. Adaptive rate limiting automatically reduces limits during high system load, protecting system stability without manual intervention during traffic spikes. 4. Per-source isolation prevents one aggressive provider from starving others. Global limiting is simpler but allows a single source to consume all capacity. 5. Use a token bucket per source for short-term control and a Redis-based sorted set for the daily sliding window counter. Return `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers with each response.FAQ
Mini Project
Build a rate-limited webhook consumer in Python. Use Flask to create a webhook endpoint. Implement a Redis-backed sliding window rate limiter that enforces per-source limits. Store rate limit configuration in a YAML file. Expose current rate limit status at /status endpoint. Use Locust or Apache Bench to load test the endpoint and verify rate limiting behavior.
What's Next
Now that you understand inbound rate limiting, learn about outbound rate limiting for webhook providers sending events to consumers.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro