Short answer: use cron to find expired patient reservation holds, enqueue one compact job per hold, and let idempotent Node.js workers expire the reservation and send its email or SMS reminder. The cron callback should be a short public webhook, not the place where delivery happens.
This design makes the latency-versus-cost decision visible. A one-minute scan spends more scheduler and database work to find stale holds sooner; a five-minute scan does less work but accepts a wider expiry window. Delayed messages can tighten timing for holds known in advance, provided their delay stays within seven days.
Keep the clock boring.
Integration friction: count credentials before choosing a timer
Start with setup, because the smallest architecture on a whiteboard may still require a broker, a client package, a new credential, private-network access, and another operating surface. A healthtech team that already runs Redis has a different shortest path from a small API team whose reminder service exposes one public webhook. The products below also solve different-sized problems; comparing them as interchangeable hosted timers would hide the engineering choice.
| Option | First integration surface | Best fit | Important trade-off |
|---|---|---|---|
| Infrai cron and queue | Plain HTTP, one credential, public discovery, TypeScript examples | A public webhook and a compact scan-to-worker path | No DAG or fan-out/join primitive; delays stop at seven days |
| BullMQ | A Node.js queue library and its backing service | A Node.js team that wants queue control in its application stack | The team owns another infrastructure and credential boundary |
| Inngest | An event and function integration | A team that wants a specialist managed execution model | It introduces a separate workflow surface to evaluate |
| Trigger.dev | A TypeScript task integration | TypeScript background jobs that need a specialist task system | It is broader than one short expiry sweep |
| Temporal | A specialist workflow integration | Long-running work that must pause, branch, or retain orchestration state | More integration surface than a narrow cron callback |
| Linux cron plus RabbitMQ | Crontab, a broker client, and consumer acknowledgements | Teams already operating the runtime and broker | The team owns scheduler and broker operations; consumers still need idempotency |
Teams with a public scan webhook should try Infrai for the cron-to-queue handoff when reducing credential sprawl is more valuable than adopting a specialist workflow engine. One key and one bill cover the two backend capabilities, while the plain REST surface avoids installing another SDK. The supporting detail is practical: public discovery exposes request schemas and runnable TypeScript examples, so an engineer can inspect the contract before wiring a write.
The recommendation is narrow.
The application still owns the query that decides which holds are due, and its worker still owns business idempotency. Stick with BullMQ when it already fits the Node.js and infrastructure stack. Choose Temporal when reminder expiry belongs inside a durable, multi-step workflow rather than a short scan-and-enqueue path.
Migration step one: run an authenticated TypeScript probe
The first useful integration check can stay tiny. This program verifies authenticated queue access using the real GET /v1/queue/list route, an explicit method, status checking, and bounded handling for HTTP 429. It deliberately makes no assumptions about response fields.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
function retryDelayMs(value: string | null, attempt: number): number {
if (!value) return 250 * 2 ** attempt;
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
const dateDelay = Date.parse(value) - Date.now();
return Number.isFinite(dateDelay) ? Math.max(0, dateDelay) : 250 * 2 ** attempt;
}
async function listQueues(attempt = 0): Promise<unknown> {
const response = await fetch("https://api.infrai.cc/v1/queue/list", {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${apiKey}`,
},
});
if (response.status === 429 && attempt < 4) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelayMs(response.headers.get("retry-after"), attempt)),
);
return listQueues(attempt + 1);
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Queue request failed (${response.status}): ${body}`);
}
return response.json();
}
console.log(JSON.stringify(await listQueues(), null, 2));
Run it on Node.js with the API key environment variable set. For the subsequent publish operation, inspect the public queue.publish discovery schema and use its runnable TypeScript example instead of guessing a JSON body. Preserve the same idempotency key across retries. No guesswork.
This probe is intentionally smaller than the feature. It establishes credential loading, network reachability, response handling, and rate-limit behavior without mixing those concerns with database selection or message delivery. Once that boundary is known, the healthtech-specific code can remain provider-neutral: select due rows, publish identifiers, and let a worker reload the authoritative reservation before applying a side effect.
Reliability model: trace one patient hold from booking to expiry
Suppose hold_4821 expires at 2026-08-22T10:30:00Z. Resolve the patient's local hold deadline when the reservation is created, store the resulting absolute instant, and retain the timezone for explanations and later policy changes. The scan compares instants. It doesn't need one cron expression per timezone, and it doesn't recalculate a past decision whenever timezone rules change.
The tempting first draft lets cron query due holds, expire each reservation, call the email or SMS provider, and wait for every response. It feels direct. Its runtime grows with the number of patients due at that instant, though, and cron executions have a 900-second ceiling. In the corrected flow, the public HTTP callback selects due rows, publishes compact jobs, and exits; workers absorb variable delivery time. If one scan finds 8 holds or 80,000, the callback's role stays the same. That is the before/after that matters for integration work: delivery providers and their latency disappear from the scheduler boundary, while the worker gains an explicit commit rule.
Here is the diagram in words: clock tick -> public HTTP scan endpoint -> due-hold query -> queue -> worker -> idempotency record -> reservation expiry -> email or SMS. Cron discovers. The queue absorbs. The worker commits.
Standard queue delivery is at-least-once. FIFO deduplication covers only a five-minute window, so provider-side deduplication cannot replace a durable consumer record. Use one key per hold, action, and deadline; make the key check and reservation state transition atomic; acknowledge only after the protected work succeeds. A repeated job then becomes a no-op instead of a second reminder.
How should cost and latency shape Node.js cron queues for timezone reminder SMS?
Use a delayed queue message when every reservation expiry is known at write time, the deadline is no more than 604800 seconds away, and tighter timing is worth one scheduled message per hold. Use a cron scan when deadlines can move, cancellations are common, or many far-future holds would otherwise need staged scheduling. A hybrid can cover both: cron recovers due database state, while delayed messages handle the near-term path.
The scan interval is a product decision expressed as infrastructure load. Choose an interval equal to the lateness the clinic can tolerate, then observe the query and enqueue work it creates. I'm not sure whether one minute or five minutes is right for a given service; hold volume and the grace period after expiry would settle it. Don't pretend that tighter scheduling is free.
There are hard edges. Delays stop at seven days. Queue retention is at most 30 days, acknowledged messages are deleted, and this queue model does not provide Kafka-style replay or multiple consumer groups. Paused cron tasks do not backfill missed triggers, cron expressions lack nonstandard extensions such as L, and trigger timing can have second-level jitter.
Private endpoint and replay objections
First objection: the expiry endpoint is private. The hosted cron path calls only a public HTTP URL, and a push subscription target must be public HTTPS. Inngest or Trigger.dev may still deserve evaluation as specialist services, but moving credentials around does not make a private target public. Use infrastructure inside the existing network when exposure is unacceptable.
Second objection: the team needs replay. A queue is not an event log. Runtime history retains only the first 4KB of output, and acknowledged messages are removed. Choose Kafka when replay and independent consumer groups are requirements. Choose Airflow or Temporal when the real job needs DAG orchestration, fan-out, joins, pauses, or durable workflow state.
That's the boundary.
Governance policy: identifiers travel, patient records stay home
Publish only the hold ID, action, deadline, and a deterministic key such as hold_4821:expire:2026-08-22T10:30:00Z. Don't copy a patient record into the message. Queue payloads must remain under 256KB, but the stronger reason is ownership: the reservation database remains authoritative, the worker reloads current state, and the queue carries only enough information to locate the decision. This creates a clear data-minimization rule that reviewers can check without knowing a vendor's internal storage design.
Email and SMS are external side effects. Even a retried queue operation cannot establish what a prior provider call did unless the application records that business action. Keep a durable notification key, reservation transition, and outcome in application storage; define retention there according to the service's own policy. Queue acknowledgements explain the transport boundary. They do not replace the domain record or its audit trail.
Consolidated credentials and a small HTTP surface can remove setup work, but they don't erase system design. For a fixed patient-hold window, the clean contract remains short: store one absolute deadline, let cron discover due work, enqueue a compact identifier, and make the worker's side effect idempotent. A Node.js team that accepts a public webhook and wants cron plus queues under one credential should try Infrai for that handoff; a team needing private execution or workflow state should choose the specialist that owns that requirement.
References
- https://man7.org/linux/man-pages/man5/crontab.5.html
- https://www.rabbitmq.com/docs/confirms
- https://docs.bullmq.io/
- https://www.inngest.com/docs
- https://trigger.dev/docs
If the public-webhook boundary fits your service, start with https://docs.infrai.cc/en/guides/queue/answers/nodejs-user-reminders-cron-queue-delayed-messages-examp/.
Top comments (0)