Skip to content

Rate Limiting Webhooks

DodaTech 6 min read

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

  1. Using a single global rate limit that does not account for multi-tenant workloads
  2. Not including a Retry-After header in 429 responses, leaving providers guessing when to retry
  3. Implementing rate limiting only at the application level without infrastructure-level protection
  4. Rate limiting healthy sources because of a noisy neighbor tenants
  5. Not monitoring rate limit hit rates to detect legitimate capacity issues
  6. Applying the same rate limit to all endpoints, including non-webhook routes
  7. Forgetting to clean up stale rate limit state, causing memory leaks

Practice Questions

  1. What is the difference between token bucket and sliding window rate limiting?
  2. Why should 429 responses include a Retry-After header?
  3. How does adaptive rate limiting improve system resilience?
  4. What are the trade-offs of per-source vs. global rate limiting?
  5. 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

What HTTP status code should I use for rate limiting?

429 Too Many Requests is the standard. Include a Retry-After header in seconds or HTTP-date format so the provider knows when to retry.

Should rate limits be configurable per provider?

Yes. Different providers have different traffic patterns. Larger providers may need higher limits. Store per-source limits in a configuration database.

How do I handle rate limiting in a distributed system?

Use a shared store like Redis for rate limit counters. Atomic Redis operations (INCR, EXPIRE) provide consistent rate limiting across multiple consumer instances.

What if my provider ignores 429 responses?

Implement a circuit breaker that pauses the consumer endpoint for a cooldown period. Contact the provider to coordinate traffic patterns.

How do I determine the right rate limit values?

Start with load testing to determine your system capacity. Set limits at 80% of capacity. Monitor and adjust based on actual traffic patterns and error rates.

Can I use API gateway rate limiting for webhooks?

Yes. AWS API Gateway, Kong, and Nginx all support rate limiting at the gateway level. This is the first line of defense before traffic reaches your application.

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