Skip to main content

How to fan out webhooks to multiple services

To fan out a webhook to multiple services, receive it once at a single endpoint, verify the signature, return a 2xx immediately, and write the event to a queue. A dispatcher then expands it into one delivery per destination, each with its own timeout, retries, and failure log, so one slow service never blocks the others.

Sending webhooks?
Svix is the enterprise-ready webhook sending service. It handles signing, retries, and delivery observability, so you can ship a reliable webhook platform in minutes instead of months. Start sending webhooks with Svix.

This guide covers the receiving side: a provider such as Stripe or GitHub sends one webhook, and several of your systems need it, say billing, analytics, a Slack alert, and a data warehouse. The sending side, where your product delivers one event to every customer endpoint subscribed to it, has the same shape with different constraints, and webhook fan-out covers that.

Why forwarding from the handler does not work​

The first version everyone writes receives the webhook and, inside the same request, POSTs it to each downstream service in a loop. It works in testing and fails the first time one of those services is slow. Providers wait a few seconds for your 2xx (GitHub allows 10, Shopify 5) and count anything slower as a failure. Stripe then retries for up to three days, so every service that already accepted its copy receives the event again. GitHub does not retry repository webhooks at all, so the event is simply gone. Either way, the provider’s view of your endpoint now depends on the health of every service behind it, and an outage in analytics turns into missing payment events.

Receive once and acknowledge fast​

Give the provider exactly one webhook endpoint, and make it do three things before responding. Verify the signature on the raw body. Check the provider’s delivery ID, X-GitHub-Delivery for GitHub or the event id for Stripe, against the IDs you have already stored, so a provider retry is not fanned out twice. Persist the event. Then return 200. Everything else happens after the response. If you receive from many providers, verification is where most of the code lives, since each one signs differently; the receiving best practices page covers the handler itself.

Expand the event into one job per destination​

The dispatcher reads the stored event and looks up which destinations want it. Keep that as a routing table rather than code: a destination URL, the event types it subscribes to, and an optional filter on the payload. A payment_intent.succeeded might go to billing and the warehouse but not to the on-call channel. For each match, enqueue one message on a background queue carrying the event ID and the destination ID. The expansion is cheap and happens once. The deliveries are the expensive part, and they now run independently of each other.

Deliver each copy on its own terms​

Each worker takes one message and makes one HTTP request. Set a timeout in seconds rather than trusting the client default. On failure, retry with exponential backoff, a common schedule being eight attempts spread over about 24 hours, and send what still fails to a dead-letter queue for that destination so you can replay it later. A per-destination rate limit protects services that cannot absorb a burst. Because retries can duplicate and reorder deliveries, every destination should be idempotent on the event ID, which is the same rule it would follow if it received from the provider directly.

Destinations cannot verify the provider’s original signature once you have changed the payload, and they should not have to carry every provider’s verification code. Sign your outgoing copies with a secret per destination, in practice HMAC-SHA256 with webhook-id, webhook-timestamp, and webhook-signature headers following the Standard Webhooks specification, so every internal service verifies one scheme. If you forward the payload byte for byte, pass the original headers through as well.

Transform per destination when you need to​

A Slack channel wants a short message, not a Stripe event object. Put payload transformation at the destination level, applied by the worker just before it sends, so the stored event stays the raw original and a bad transform breaks one destination instead of all of them. The same hook handles the format mismatches behind errors like a GitHub to Discord 400.

Build it or use a gateway​

Written out, the parts are an endpoint with signature verification, an event store, a routing table, a queue, workers with retries and dead-lettering, and a log of every attempt. Any queue works for the middle: SQS, RabbitMQ, and Redis streams all fit, and the DLQ redrive guide covers getting failed deliveries back out. A webhook gateway is this same architecture packaged as a product. Svix Ingest gives you one URL per source, verifies signatures for providers like Stripe, GitHub, and Shopify, stores every event, and filters, transforms, and fans out to multiple destinations with retries, so the fan-out becomes configuration rather than a service you run.

Frequently asked questions

Can I fan out a webhook by forwarding it from my handler?

Only for a prototype. The provider waits a few seconds for your response, so any slow destination fails the whole delivery, and a provider retry re-sends to destinations that already succeeded. Acknowledge first, queue the event, and deliver each copy from a worker.

Do I need to sign fanned-out webhooks?

Yes, whenever destinations are reachable over a network. Sign each copy with a secret per destination using HMAC-SHA256 and the Standard Webhooks headers, so internal services verify one scheme instead of one per provider.

How is fan-out different from a webhook proxy?

A webhook proxy forwards each request to one destination, usually adding verification and buffering. Fan-out expands one received event into independent deliveries to several destinations, each with its own retries, rate limit, and delivery log.