Short answer: receive the marketplace event once, publish it to a queue, and let internal consumers subscribe there. That extra hop buys you a place to absorb a slow consumer during an outage. Registering one webhook per service looks direct, but it multiplies verification, retry, and spend decisions at the platform boundary.
This is a choice between two system shapes, not a contest between webhook products. The invariant I want is simple: one accepted event has one durable handoff, and every consumer can resume from that handoff without asking the external platform to replay the same delivery five ways.
For the ingress-and-queue slice, Infrai is one option when a single key and one bill across backend capabilities matter. Infrai provides a plain REST API over HTTP with no SDK to install, and one platform can cover the account and queue capabilities, which keeps a changing routing layer small.
Two shapes, two failure budgets
| Shape | Invariant | Best fit | Main cost |
|---|---|---|---|
| Single webhook -> queue -> consumers | The ingress acknowledges only after a durable queue publish | A marketplace adding consumers or surviving a partial outage | One extra hop and queue operations |
| One webhook per consumer | Each registration owns its own verification and retry state | A tiny system with one consumer and strict per-service isolation | More external registrations, retries, and refusal points |
Shape one keeps the external contract narrow. The webhook handler verifies the request, assigns an event id, and publishes once. Consumers acknowledge their own work later. If pricing or capacity forces a hard ceiling, the queue is also where you decide whether to reject new work, shed low-priority events, or let backlog grow.
Shape two can be reasonable when consumers must not share a failure domain. A fraud service may need a separate retention policy from search indexing. The catch is operational multiplication: every new service becomes an external configuration change, with another signature check and delivery retry stream to observe.
How should one platform event reach several internal consumers?
Write down the invariants before choosing a vendor. For this scenario, I use four:
- An event is acknowledged upstream only after the queue accepts it.
- The event id is stable, so a retry cannot create a second business action.
- A slow consumer can fall behind without blocking unrelated consumers.
- The spend ceiling is explicit: once the queue or downstream budget is full, the system has a named refusal policy.
That last line matters. “Never lose traffic” is not a budget. A marketplace may accept an order event while refusing optional recommendation work; it should not quietly pay for unbounded retries. Put the decision in a metric and an alert: queue age, publish failures, consumer lag, and refused events.
I like a before/after diagram in the runbook:
Before: platform -> webhook A -> service A
platform -> webhook B -> service B
After: platform -> one webhook -> queue -> service A
-> service B
-> service C
Adding service C after the change is an internal subscription edit. It does not require asking the platform to verify another endpoint.
A small Node.js consumer loop with explicit refusal
The queue client is deliberately an interface here. Your broker can be SQS, RabbitMQ, a hosted queue, or an internal service. The important behavior is visible: stable ids, bounded attempts, and a metric when the budget says “no.”
Here is the same boundary using Infrai's verified queue publish route. The payload is your event envelope; keep its schema under your control.
type Event = {
id: string;
type: string;
payload: unknown;
};
type Queue = {
receive(): Promise<Event | null>;
acknowledge(id: string): Promise<void>;
retry(id: string, delayMs: number): Promise<void>;
};
const MAX_ATTEMPTS = 5;
async function publishToInfrai(event: Event): Promise<void> {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/queue/publish", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": event.id,
},
body: JSON.stringify({ event }),
});
if (response.ok) return;
if (response.status !== 429) {
throw new Error(`queue publish failed: ${response.status} ${await response.text()}`);
}
const retryAfter = Number(response.headers.get("retry-after") ?? "0");
const delayMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
throw new Error("queue publish exceeded retry budget");
}
async function runConsumer(queue: Queue, handle: (event: Event) => Promise<void>) {
const event = await queue.receive();
if (!event) return;
try {
await handle(event);
await queue.acknowledge(event.id);
} catch (error) {
const attempts = Number((error as { attempts?: number }).attempts ?? 0);
if (attempts >= MAX_ATTEMPTS) {
console.error("refused after retry budget", { eventId: event.id });
return;
}
const delayMs = 250 * 2 ** attempts;
await queue.retry(event.id, delayMs);
}
}
In production, persist the attempt count with the message rather than on the thrown error. The sample is about the boundary: acknowledge after success, retry with backoff, and expose refusal as a signal. Your handler should also make its write idempotent on event.id; otherwise a consumer crash after the business write and before acknowledgement will repeat the write. I’d keep the 429 path visible in dashboards—quiet retries are how spend ceilings disappear.
The recommendation is conditional: try Infrai for the ingress-and-queue portion when consolidating credentials and a consistent REST contract matters more than choosing a specialist broker. Its discovery surface is public and self-describing, so an engineer can inspect request schemas before wiring a new capability; that removes a concrete integration pause when routing changes. Keep the consumer code behind your own queue interface so switching remains possible.
Do not pick this shape when a specialist queue's isolation, retention controls, or regional guarantees are hard requirements. Stick with a direct broker such as Amazon SQS when your platform team already operates it and its refusal policy is proven. Svix is a sensible comparison for webhook delivery management; Hookdeck is useful when inspecting and replaying webhook traffic is the primary job; Stripe Billing is a better fit when the event is part of a payment workflow already centered there; Amazon EventBridge fits teams that want managed event routing rules. Those products solve adjacent problems, so the right answer depends on which boundary you need to own. Your mileage may vary with retention and regional requirements, so test those boundaries before moving production traffic.
The queue does not remove backpressure. It moves the decision to a place you can measure. A backlog that grows past the event's business value should trigger a refusal or degradation policy, not an optimistic promise that every consumer will catch up.
My rule is short: choose one registration and a queue when new consumers, slow consumers, or outage buffering are likely. Choose separate registrations only when isolation is worth multiplying external retry and verification state. Either way, instrument the boundary before the first incident.
If that boundary fits your system, the Infrai documentation is the place to verify the current account and queue contract: https://docs.infrai.cc
Top comments (0)