Skip to content

Rate Limiting Webhooks Outbound

DodaTech 6 min read

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

  1. Applying the same outbound rate to all consumers regardless of their capacity
  2. Not reducing delivery rate when consumers return 429 responses
  3. Using FIFO queuing that lets one slow consumer block all others
  4. Rate limiting at the event production level instead of the delivery level
  5. Not logging rate limit events, making it hard to debug delivery delays
  6. Setting static rates that do not adapt to changing consumer capacity
  7. Forgetting to clean up state for deactivated consumer subscriptions

Practice Questions

  1. Why is outbound rate limiting important for a webhook provider?
  2. How should a provider respond when a consumer returns 429?
  3. What is fair queuing and why does it matter for multi-tenant webhook delivery?
  4. How would you automatically discover a consumer's optimal delivery rate?
  5. 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

What is a good default outbound rate limit per consumer?

Start with 1-10 requests per second depending on the event complexity. Monitor consumer response times and adjust. Most consumers can handle 5-50 webhooks per second.

Should I batch events sent to the same consumer?

Yes, if your events are small and frequent. Batch multiple events into a single HTTP request with an array payload. This reduces HTTP overhead and consumer processing load.

How do I handle consumers with different capacity requirements?

Allow consumers to set their preferred rate limit during subscription. Provide a default rate and let consumers request adjustments via API or dashboard.

What happens when all consumers are rate limited?

The provider should queue events and deliver them as capacity becomes available. Monitor queue depth and alert if it grows too large, indicating systemic capacity issues.

How do I test outbound rate limiting?

Create a test consumer that returns 429 after N requests. Verify that the provider reduces its rate and eventually stops sending. Test recovery when the consumer starts accepting again.

Does outbound rate limiting affect event ordering?

Yes, rate limiting can reorder events if multiple events are queued for the same consumer. Use sequence numbers in the payload so consumers can reorder if needed.

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