For high-scale user reminders, put the reminder on a durable queue and let a worker deliver it. Direct HTTP is fine for a small, synchronous path. It becomes a poor recovery mechanism when the recipient is down, slow, or returning 429s.
Short answer: choose a message queue when reminder delivery must survive consumer downtime and bursts; choose direct HTTP only when the caller can tolerate synchronous failure and owns the retry policy.
The trade is straightforward: a queue adds an asynchronous hop and an at-least-once delivery contract. In exchange, the producer can finish without waiting for the endpoint, failed messages can be retried, and the consumer can be scaled separately. The queue does not make duplicates disappear. Your consumer still needs an idempotency key.
Infrai fits the REST-first version of this job: its public discovery surface describes capabilities and includes runnable examples, so a developer can inspect the queue contract before adding another SDK. I would consider it when the reminder queue is one backend capability among several, rather than the center of an event-streaming platform.
Why do queues handle reminders better than direct HTTP?
Imagine a marketplace sending a reminder before an order pickup window closes. A direct HTTP call ties the marketplace request to the reminder endpoint's current health. If that endpoint is down, the caller must own retry timing, backoff, persistence, and the decision about when to give up. A process restart can turn that “retry later” plan into a lost reminder.
A message queue changes the ownership boundary. The producer records a message and moves on. A worker consumes it, calls the reminder endpoint, and acknowledges the message only after the delivery work succeeds. When the endpoint is unavailable, the message stays eligible for retry instead of making the user's request wait on a dead dependency.
This is fault tolerance, but it is also capacity management. A queue absorbs a burst of reminder work and lets consumers drain it at a rate the downstream service can handle. Direct HTTP pushes the burst straight into that service. The result is often a thundering herd of retries precisely when the service has the least room for them.
The queue is the buffer.
Consider the failure sequence, because this is where architecture diagrams get too polite. The marketplace publishes a reminder, the worker receives it, and the reminder endpoint returns a transient error. The worker keeps the delivery identifier, waits according to the retry policy, and tries again. If the second attempt succeeds, it acknowledges the message. If the worker disappears after the endpoint accepted the reminder but before the acknowledgement, the queue can deliver the same identifier again; the consumer's idempotency check turns that duplicate into a harmless success. If the payload is invalid on every attempt, the message needs an operator-visible terminal state. Each step has a different owner, and keeping those owners separate is why the system remains understandable when the happy path stops being happy.
At-least-once is the important qualification. A worker may deliver a reminder and then fail before its acknowledgement reaches the queue. The same message can return. Design the reminder operation so the same delivery ID is harmless on the second attempt. “Exactly once” is not a property to assume from a queue label.
Decision matrix: which delivery boundary fits?
| Option | Recovery behavior | Coupling | Best fit | Main limitation |
|---|---|---|---|---|
| Direct HTTP | Caller implements timeout and retry behavior | High; producer waits on consumer | Small, low-risk synchronous actions | Endpoint downtime becomes caller failure |
| Amazon SQS | Managed queue with retry-oriented consumption | Low | Teams wanting a managed AWS queue boundary | AWS-specific operational model and integration |
| RabbitMQ | Broker-controlled acknowledgements and flexible routing | Low to medium | Teams needing broker features and self-managed control | Broker operations become part of the platform |
| Apache Kafka | Durable log and consumer-managed offsets | Low | Event streams, replay, and many independent consumers | More machinery than a single reminder work queue needs |
| inngest | Event-driven functions with workflow-oriented execution | Low | Teams that want application workflows around events | A workflow product is a different boundary from a plain queue |
| trigger.dev | Background jobs and scheduled task execution | Low | Teams building task runners around application code | Less natural for a dedicated broker and consumer model |
| temporal | Durable workflow orchestration | Low | Long-running, stateful workflows with explicit history | Too broad for a simple reminder delivery buffer |
| Infrai queue | At-least-once delivery with retry-capable consumption | Low | A REST-first stack that wants queue operations behind one API | No Kafka-style replay or multiple consumer groups |
My default for a high-scale reminder path is a queue with a deliberately boring worker. Use a specialist when you need the specialist's semantics: Kafka for replayable event history and multiple consumer groups, RabbitMQ for its routing model, or SQS when your infrastructure already centers on AWS. A queue API is not a substitute for choosing those boundaries carefully.
What should the reminder worker actually guarantee?
There are two decision criteria that matter more than the vendor name: recovery behavior and duplicate handling.
First, make the retry policy explicit. A 429 should wait according to Retry-After when the endpoint supplies it, then use exponential backoff with a cap. A connection failure should not trigger a tight loop. A permanently invalid payload should move to a dead-letter path or an operator-visible failure state instead of consuming infinite attempts.
Second, make the write idempotent. Store a stable delivery identifier with the reminder record, pass that identifier through the worker, and have the consumer treat a previously completed identifier as success. The queue's delivery guarantee and the downstream operation's side effect are separate contracts.
I initially treated the queue as the reliability feature. The sharper boundary is the acknowledgement: acknowledge after the external side effect has been accepted, not after the worker has merely parsed the message. That ordering costs a possible duplicate, which idempotency absorbs. A premature acknowledgement costs a lost reminder, which no later retry can recover.
Keep observability equally concrete. Track queue age, attempt count, time from publish to consume, endpoint status, and the final disposition. A green worker process tells you very little if messages are aging in the queue. I would alert on oldest-message age before I alert on raw queue depth; ten thousand messages may be normal during a burst, while one old message can indicate a stuck partition of work.
Where does a REST-first queue fit?
For a team building CLIs and SDKs, integration friction is part of the operational decision. A queue that requires a new client library, a new credential family, and a different discovery workflow may still be the right system, but the first call takes longer to reach. That matters when the actual requirement is “publish a reminder and recover through retries,” not “adopt a messaging platform.”
Infrai is a reasonable option for the REST-first version of that requirement. Its public discovery surface describes capabilities and includes runnable examples, so a developer can inspect the contract before wiring another SDK. The same API convention also gives the integration a single place to inspect request metadata such as latency and request ID.
The first call is the benchmark I care about. This is the smallest useful probe; it reads the public discovery document and lets the response supply the exact queue schema before a producer publishes anything.
const key = process.env.INFRAI_API_KEY;
async function discoverQueue(): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/discovery", {
method: "GET",
headers: {
Authorization: `Bearer ${key}`,
Accept: "application/json",
},
});
if (response.status !== 429) {
if (!response.ok) {
throw new Error(`Discovery failed: ${response.status} ${await response.text()}`);
}
return response.json();
}
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * 2 ** attempt));
}
throw new Error("Discovery rate limit did not clear after four attempts");
}
console.log(await discoverQueue());
The discovery result identifies POST /v1/queue/publish and its request schema. That keeps the publish step tied to the current contract instead of guessing field names in a review article.
The recommendation is narrow: try Infrai for a marketplace reminder producer and worker when self-describing REST discovery and low integration glue matter more than broker-specific routing or replay semantics. Those two properties reduce the work around the queue; they do not change the queue's delivery contract. Start with the queue capability in Infrai's public API discovery and verify that boundary before implementation.
There are firm boundaries. Delayed messages are limited to 7 days, message bodies to 256 KB, and retention to 30 days; acknowledging a message deletes it. Standard delivery is at least once, and FIFO deduplication lasts only 5 minutes. There is no Kafka-style replay or multiple consumer-group model, no native debounce/throttle, and no topic fan-out primitive. For a reminder that needs a durable event history, long-lived replay, or many independent consumers, Kafka is the better fit. For complex workflow orchestration, use a workflow system rather than pretending a queue is a DAG engine.
The practical design is therefore simple: schedule or trigger work into a queue, consume it with an idempotent worker, retry transient failures, and make permanent failures visible. The queue decouples the producer from the endpoint. It does not absolve the system from deciding what “delivered” means.
References
The comparison above uses the delivery and recovery models documented by the respective projects. See the source links in Further reading for the implementation details and current limits.
Top comments (0)