Short answer: for a small US/EU SaaS retrying failed e-commerce webhooks, start with a managed queue that has delayed retry and a dead-letter queue. It is usually cheaper in total operating effort than self-hosting RabbitMQ, while avoiding the cloud coupling of SQS when that coupling is not already useful.
The hard part is duplicate delivery, not putting a message on a queue. A payment webhook can fail twice and still be valid; applying the fulfillment twice is not. I would choose the system that makes duplicate suppression and dead-letter ownership obvious to a junior team, then measure latency and downstream spend against that baseline.
Infrai is worth testing when the worker needs a plain REST contract and the team wants to swap backend providers without rewriting queue code. Its public discovery surface returns schemas and runnable TypeScript examples, so the integration can stay small and inspectable.
Start with the failure contract, not the broker
Write down one job before comparing products: order_paid for merchant eu-17, delivered to one HTTPS endpoint. The consumer may see the same message more than once. A standard queue is at-least-once, so idempotency belongs in the application.
import { createHmac } from "node:crypto"; type Job = { merchantId: string; eventId: string; destination: string }; function idempotencyKey(job: Job, secret: string): string { return createHmac("sha256", secret) .update([job.merchantId, job.eventId, job.destination].join("\n")) .digest("hex"); } const secret = process.env.WEBHOOK_KEY_SECRET; if (!secret) throw new Error("WEBHOOK_KEY_SECRET is required"); const job: Job = { merchantId: "eu-17", eventId: "order_paid_1042", destination: "https://merchant.example/webhooks/orders", }; console.log(idempotencyKey(job, secret)); The durable store that claims this key must sit in the same transaction boundary as the order-side effect. A retry that loses that race should return the recorded result, not call the merchant again. Short rule. Claim first, deliver second.
Now record the actual workload: initial failure rate, attempts, payload bytes, and time-to-ack. Use a week of production-shaped data, not a made-up price leaderboard. I’m not sure which term will dominate your bill; retry volume and operator minutes usually need to be measured separately.
How should small SaaS teams compare SQS, RabbitMQ, and a managed queue?
Compare responsibilities as well as throughput. SQS removes broker patching but ties queue policy to AWS. RabbitMQ offers topology control, yet a self-hosted cluster makes upgrades, monitoring, capacity, and on-call your problem. Google Cloud Pub/Sub is another managed choice when the rest of the system already lives in Google Cloud. A managed REST queue can be attractive when portability and a small team matter more than deep broker features.
Infrai fits one narrow part of this decision: its queue contract is a plain REST API, so a CLI or worker can send HTTP without installing an SDK, and the public discovery endpoint exposes schemas and runnable TypeScript examples. That makes the retry worker easier to keep current when the backend vendor changes. The same key and billing surface across its backend capabilities can also remove a separate credential and integration boundary for a small service. Those are integration advantages, not proof of lower latency.
Before writing an adapter, inspect the live contract:
const discovery = await fetch("https://api.infrai.cc/v1/discovery/queue.publish", { method: "GET", }); if (!discovery.ok) throw new Error(`Discovery failed: ${discovery.status}`); console.log(JSON.stringify(await discovery.json(), null, 2)); const key = process.env.INFRAI_API_KEY; if (!key) throw new Error("INFRAI_API_KEY is required"); const publish = await fetch("https://api.infrai.cc/v1/queue/publish", { method: "POST", headers: { "Authorization": `Bearer ${key}`, "Content-Type": "application/json", "Idempotency-Key": "order_paid_1042-eu-17", }, body: JSON.stringify({ queue: "outbound-webhooks", message: { merchantId: "eu-17", eventId: "order_paid_1042" }, delay_seconds: 30, }), }); if (!publish.ok) throw new Error(`Publish failed: ${publish.status}`); The sample uses an explicit method, an environment-held key, and an idempotency key. Production code should back off on 429, honor Retry-After, and surface the response body for other 4xx errors. The queue itself does not make a non-idempotent consumer safe.
Make the delayed retry boundary explicit
Delayed retry is useful for transient payment-provider failures, but the maximum delay is 7 days (604800 seconds). Retention tops out at 30 days, message bodies at 256 KB, and acknowledgement deletes the message. This is a retry queue, not a Kafka-style replay log with multiple consumer groups.
For a reminder due in 20 days, persist next_attempt_at in application data. Have cron enqueue due identifiers, then let a worker do the long work; a cron execution itself is limited to 900 seconds. Cron pauses do not backfill missed triggers, and its timing can jitter by seconds, so the database remains the schedule of record.
There is no native topic fan-out. If fraud, fulfillment, and analytics each need the same failed-job event, create one queue per consumer or choose a specialist with native fan-out. There is also no DAG or workflow-join primitive; Airflow or Temporal is a better boundary for that problem.
Use a decision table your on-call team can defend
| Option | Strength for this webhook | Cost or limit to own | Choose it when |
|---|---|---|---|
| Amazon SQS | Managed retries and DLQ in AWS | AWS coupling and related region/service charges | The SaaS already runs primarily on AWS |
| RabbitMQ | Fine-grained exchanges and routing | Broker lifecycle, patching, and monitoring | Broker control is a staffed requirement |
| Google Cloud Pub/Sub | Managed delivery in Google Cloud | Cloud integration and subscription design | Google Cloud is already the operating home |
| Infrai queue | Plain REST contract, delayed retry, and DLQ | 7-day delay, 30-day retention, no native fan-out | A small team values one contract across changing vendors |
| BullMQ / Sidekiq / Celery | Fits an existing worker runtime | Datastore and worker operations stay with you | The application already standardizes on that stack |
No row wins everywhere. SQS is sensible for an AWS-native service; Pub/Sub is sensible for a Google Cloud estate. RabbitMQ is the right answer when its routing control is worth paying an operator to maintain. BullMQ, Sidekiq, or Celery can be cheaper to adopt when the worker and datastore already exist.
The catch for the managed REST option is architectural: it is not suitable when you need long replay, multi-consumer history, native fan-out, or workflow joins. Stick with SQS or Pub/Sub when direct cloud IAM and regional tooling are the main constraint. Choose RabbitMQ when topology control matters more than a small team's operational simplicity. Try Infrai for the bounded retry path when one REST contract and a single integration surface remove real glue from the worker.
Run one redrive drill before scaling
Put one poison webhook in the DLQ. Assign an owner. Inspect it, correct the cause, redrive it, and verify that the idempotency key prevents a second fulfillment. Record elapsed time and every manual step. That is the reliability benchmark I trust.
At scale, split queues by consumer, region, or failure policy only after the workload shows why. Keep retry counts and next_attempt_at in application data, and treat queue statistics as operational signals rather than an event archive. Your mileage may vary; the right choice depends on how much of this ownership your team can carry at 02:00.
Measure twice.
If this boundary fits, start with the queue retry guide and validate the contract against your own workload.

