Skip to content
use-cases / the-webhook-fan-out-you-didnt-have-to-build / hero
PIPE · HTTP FAN-OUT

The webhook fan-out you didn't have to build

Stripe POSTs the event body to a pipe path with ?n=12. Twelve subscribers GET the same path with ?n=12. The pipe holds the message until everyone is connected, then streams to all twelve at once. No broker, no consumer group, no DLQ.

Read the pipe docs
use-cases / the-webhook-fan-out-you-didnt-have-to-build / mechanism

One URL. Twelve readers. Backpressure per connection.

Hoody Pipe is HTTP streaming with a multi-receiver mode. Append ?n=N to both the sender and the receiver URLs and the pipe waits until N readers attach, then mirrors the body to all of them simultaneously. The slow ones throttle their own connection — the fast ones keep flowing.

01STEP 01 · INTAKE

Stripe POSTs to the pipe

Your Stripe endpoint forwards the event body straight to /api/v1/pipe/billing?n=12. The connection blocks while the pipe waits for the twelve receivers to assemble.

02STEP 02 · ASSEMBLE

Twelve subscribers attach

Each subscriber is a curl loop in a container, GETing the same path with ?n=12. The pipe holds the body in memory until the twelfth reader connects — no on-disk queue, nothing to flush.

03STEP 03 · STREAM

Fan-out, then forget

Once everyone is connected, the body streams to all twelve at once. A slow reader applies backpressure to its own socket; the others keep going. When the last reader disconnects, the pipe forgets the message.

stripe-webhook.sh
# Sender side — your Stripe webhook handler.
# Pipe holds the body until 12 readers are attached.
curl -X POST "https://api.hoody.com/api/v1/pipe/billing?n=12" \
  -H "Authorization: Bearer $HOODY_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary "@stripe-event.json"

# Receiver side — one of twelve subscriber containers.
# Same path, same n. Streams the event body when fan-out is ready.
curl -N "https://api.hoody.com/api/v1/pipe/billing?n=12" \
  -H "Authorization: Bearer $HOODY_TOKEN" | ./billing-handler.sh

# n must match on both ends — mismatch returns 400.
# Default n=1 is point-to-point. n=12 is fan-out for twelve.

Same URL, same query string, both ends. The sender's connection is the broker; the readers' connections are the consumer group. Backpressure is per-socket, not per-topic, because there is no topic — there is one in-flight body and twelve sockets pulling on it.

use-cases / the-webhook-fan-out-you-didnt-have-to-build / fleet

Subscribers come and go like processes

Adding a subscriber is one more curl in one more container. Removing one is killing the curl. There is no broker config, no consumer-group rebalancing, no DLQ to drain — the cluster topology is just whatever processes happen to be running right now.

subscribers diff · last 5 minutesno rebalancing required
  • + ADDED
    WHKAUD
    two new containers spun up
  • − REMOVED
    MIX
    container killed, curl exited
  • = STABLE
    BILANLLOGFRDSUCSLACRMCDNNOT
    still on the same n=12 path

The pipe doesn't track who is subscribed. It just waits for n connections per event. Bump n on both ends and the next event waits for the new headcount. There is no membership state to corrupt because there is no membership.

use-cases / the-webhook-fan-out-you-didnt-have-to-build / advantages

What you don't have to operate anymore

Every line below is a category of work that disappears when the broker is the protocol. No infrastructure to provision, no abstractions to learn — just curl and ?n.

  • No queue to provision

    No SQS, no Kafka cluster, no RabbitMQ exchange. The pipe is the queue, and it lives for one message at a time.

  • No consumer-group bookkeeping

    No offsets, no commits, no rebalances. The pipe holds the body until n sockets attach — that is the entire coordination model.

  • No DLQ to drain

    If the sender disconnects without n readers ready, the pipe times out (5-min TTL) and Stripe retries. No poison-message bucket to babysit.

  • No SDK per language

    Subscribers are curl loops. Senders are curl. The protocol is HTTP. Anything that can hit a URL can join the cluster.

  • Per-socket backpressure

    A slow reader throttles its own connection. The other eleven keep streaming at full speed. There is no head-of-line blocking across the fleet.

  • Forgetful by default

    The body is gone the moment the last reader disconnects. No retention policy to set, no log-compaction to schedule, no GDPR delete-on-request job to write.

use-cases / the-webhook-fan-out-you-didnt-have-to-build / punchline

Twelve subscribers, one URL, no broker.

The brokers below are the things you used to install before HTTP fan-out was a query parameter. The right side is what replaces them — one path, one n, and twelve curl processes that don't know they're a cluster.

WHAT YOU DON'T RUN
  • AWS Lambda + SQS/SNS
  • Apache Kafka
  • RabbitMQ exchanges
  • Custom webhook routers
WHAT YOU CALL INSTEAD
POST /api/v1/pipe/billing?n=12

Twelve receivers GET the same path with the same n. The pipe is the broker — and forgets the message the second the last reader hangs up.

use-cases / the-webhook-fan-out-you-didnt-have-to-build / replaces

What this replaces

If you reach for any of these to broadcast a webhook to N consumers, the pipe model is doing the same job in two curl invocations — sender on one side, receivers on the other.

  • AWS Lambda + SQS/SNSBroker per topic, IAM per subscriber
  • Apache KafkaCluster, ZooKeeper/KRaft, consumer groups
  • RabbitMQ exchangesBindings, vhosts, mgmt UI per env
  • Custom webhook routersService to maintain, retries to invent
  • Pub/Sub servicesPer-message billing, GCP/AWS lock-in
  • HTTP fan-out pluginsGateway plugin per stack, no backpressure
use-cases / the-webhook-fan-out-you-didnt-have-to-build / cta

The fan-out you used to write a sprint for is now a query parameter. Append ?n=12 and ship.

Read the pipe docs
use-cases / the-webhook-fan-out-you-didnt-have-to-build / related

Read the others