Rate Limiting Webhooks Outbound
title: "Rate Limiting Outbound Webhook Delivery" description: "Learn how to implement outbound rate limiting for webhook providers to protect consumers from overload and ensure fair delivery across all subscribers." weight: 27 date: 2026-06-28 lastmod: 2026-06-28 tags: ["apis", "webhooks"]
Webhook providers must rate limit outbound delivery to protect consumers from being overwhelmed. A single provider sending thousands of events per second to a consumer can degrade or crash the consumer's system. Outbound rate limiting ensures fair delivery across all subscribers and prevents providers from being blocked by slow consumers.
## What You'll Learn
- Implement per-consumer outbound rate limiting for webhook delivery
- Design fair queuing across multiple subscribers
- Handle consumer backpressure signals (429 responses)
- Monitor delivery latency and adjust rates dynamically
## Why It Matters
Good webhook providers protect their consumers. Overwhelming a consumer with events causes failed deliveries, retries, and eventually DLQ entries. Outbound rate limiting reduces overall network traffic, improves delivery success rates, and builds trust with integration partners.
## Real-World Use
- Stripe limits outbound webhook delivery to one event per second per endpoint by default
- GitHub allows consumers to set a delivery rate limit in webhook settings
- Shopify queues events per store and delivers at a configurable rate
- Message brokers implement consumer prefetch limits to control outbound flow
## Mermaid Flow
```mermaid
graph TD
A[Event Generated] --> B[Queue per Consumer]
B --> C[Rate Limiter]
C --> D{Consumer 429?}
D -->|Yes| E[Reduce Rate / Backoff]
D -->|No| F{Within Rate Limit?}
F -->|Yes| G[Deliver Now]
F -->|No| H[Queue for Later]
H --> I[Scheduled Delivery]
G --> J[Update Metrics]
E --> C
Teacher's Corner
Contrast inbound rate limiting (consumer protects itself) with outbound rate limiting (provider protects consumers). Both are necessary in a healthy webhook ecosystem. Explain that outbound rate limiting is a form of good citizenship that prevents the provider from being labeled as abusive or being blocked by consumers.
Code Examples
Example 1: Per-Consumer Outbound Rate Limiter
import time
import threading
import requests
class OutboundRateLimiter:
def __init__(self):
self.consumers = {}
self.lock = threading.Lock()
def set_rate(self, consumer_id, max_per_second):
with self.lock:
self.consumers[consumer_id] = {
"max_per_second": max_per_second,
"tokens": max_per_second,
"last_refill": time.time()
}
def can_send(self, consumer_id):
with self.lock:
if consumer_id not in self.consumers:
return True
c = self.consumers[consumer_id]
now = time.time()
elapsed = now - c["last_refill"]
c["tokens"] = min(c["max_per_second"],
c["tokens"] + elapsed * c["max_per_second"])
c["last_refill"] = now
if c["tokens"] >= 1:
c["tokens"] -= 1
return True
return False
limiter = OutboundRateLimiter()
limiter.set_rate("consumer-a", max_per_second=5)
def deliver(consumer_id, event):
if limiter.can_send(consumer_id):
print(f"Sending {event} to {consumer_id}")
else:
print(f"Queued {event} for {consumer_id}")
for i in range(10):
deliver("consumer-a", f"event-{i}")
Expected Output: First 5 events send immediately. Events 6-10 are queued. After 1 second, tokens replenish and more events can send.
Example 2: Adaptive Rate Based on Consumer 429 Responses
import time
from collections import defaultdict
class AdaptiveOutboundLimiter:
def __init__(self, initial_rate=10):
self.rates = defaultdict(lambda: initial_rate)
self.backoff_until = {}
def record_response(self, consumer_id, status_code):
if status_code == 429:
self.rates[consumer_id] = max(1, self.rates[consumer_id] // 2)
self.backoff_until[consumer_id] = time.time() + 10
print(f"Reduced rate for {consumer_id} to {self.rates[consumer_id]}")
elif status_code // 100 == 2:
self.rates[consumer_id] = min(100, self.rates[consumer_id] + 1)
def can_deliver(self, consumer_id):
if consumer_id in self.backoff_until:
if time.time() < self.backoff_until[consumer_id]:
return False
del self.backoff_until[consumer_id]
return True
limiter = AdaptiveOutboundLimiter(initial_rate=10)
print(limiter.can_deliver("consumer-x"))
limiter.record_response("consumer-x", 429)
print(limiter.can_deliver("consumer-x"))
time.sleep(11)
print(limiter.can_deliver("consumer-x"))
Expected Output: True, then False (backoff), then True after backoff expires.
Example 3: Fair Queuing Across Multiple Consumers
import time
import heapq
class FairDeliveryQueue:
def __init__(self, global_rate=50):
self.global_rate = global_rate
self.queues = {}
self.last_delivery = time.time()
def enqueue(self, consumer_id, event):
if consumer_id not in self.queues:
self.queues[consumer_id] = []
self.queues[consumer_id].append(event)
def dequeue_next(self):
now = time.time()
if now - self.last_delivery < 1.0 / self.global_rate:
return None
self.last_delivery = now
active = {k: v for k, v in self.queues.items() if v}
if not active:
return None
smallest = min(active, key=lambda k: len(active[k]))
event = self.queues[smallest].pop(0)
return smallest, event
queue = FairDeliveryQueue(global_rate=10)
queue.enqueue("consumer-a", "evt-a1")
queue.enqueue("consumer-a", "evt-a2")
queue.enqueue("consumer-a", "evt-a3")
queue.enqueue("consumer-b", "evt-b1")
for _ in range(4):
result = queue.dequeue_next()
if result:
print(f"Deliver {result[1]} to {result[0]}")
time.sleep(0.05)
Expected Output: evt-b1 (consumer-b has fewest events) then evt-a1, evt-a2, evt-a3.
Common Mistakes
- Applying the same outbound rate to all consumers regardless of their capacity
- Not reducing delivery rate when consumers return 429 responses
- Using FIFO queuing that lets one slow consumer block all others
- Rate limiting at the event production level instead of the delivery level
- Not logging rate limit events, making it hard to debug delivery delays
- Setting static rates that do not adapt to changing consumer capacity
- Forgetting to clean up state for deactivated consumer subscriptions
Practice Questions
- Why is outbound rate limiting important for a webhook provider?
- How should a provider respond when a consumer returns 429?
- What is fair queuing and why does it matter for multi-tenant webhook delivery?
- How would you automatically discover a consumer's optimal delivery rate?
- Challenge: Design an outbound rate limiter that supports per-consumer rate limits, global throughput caps, consumer backpressure detection, and fair queuing. Implement with Redis for distributed rate limiting across multiple provider instances.
Answer Key
1. Outbound rate limiting prevents overwhelming consumers, reduces failed deliveries and retries, ensures fair resource allocation, and maintains the provider's reputation as a good integration partner. 2. Reduce the delivery rate for that consumer, backoff delivery attempts, and log the event. The provider should respect the Retry-After header if provided. 3. Fair queuing ensures that consumers with fewer events are not starved by consumers with many events. It provides proportional delivery time across all active consumers. 4. Start with a conservative rate and increase gradually while monitoring consumer response times and error rates. Use additive increase / multiplicative decrease (AIMD) algorithm. 5. Use Redis sorted sets for per-consumer delivery windows, Lua scripts for atomic rate checking, per-consumer queues in Redis lists, a global counter for total throughput, and a background worker that polls queues using fair scheduling.FAQ
Mini Project
Build an outbound rate-limited webhook provider in Python. Create: (1) a consumer simulator that returns 429 randomly, (2) a provider that delivers events with per-consumer token bucket rate limiting, (3) adaptive rate adjustment based on consumer responses, (4) fair queuing across consumers using a round-robin scheduler, (5) metrics endpoint showing delivery rate per consumer and queue depth, and (6) a simple Grafana dashboard using Prometheus metrics.
What's Next
Now that you can control delivery rates, learn how to monitor webhook delivery with observability tools and dashboards.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro