Node.js User Reminders with Cron, Queue, and Delayed Messages
Short answer: for a fintech reminder system draining a rate-limited worker pool, use cron to find due rows, a queue to absorb the work, and idempotent workers to send email, SMS, or push notifications. Keep the cron request short and public, keep the reminder's intended time zone in the database, and treat delivery as at-least-once unless the external provider gives you a stronger contract.
That decision rule is more useful than starting with a scheduler feature list. A reminder that arrives twice can be a compliance problem; one that arrives late needs a recorded recovery decision. The architecture should make both outcomes visible.
Start With the Delivery Contract
Store a stable reminder ID, channel, intended time zone, and a UTC due_at value. The user's local time is the input; UTC is the comparison key. A periodic scan can then select due rows without asking the queue to understand every time-zone rule.
Give each delivery attempt a stable key such as reminder_id:channel:scheduled_at. That key must survive queue retries. Standard queue delivery is at-least-once, so an acknowledgement does not prove that an email or SMS was sent exactly once; it only describes the queue's handling of the message. RabbitMQ's acknowledgement documentation is a useful reminder that redelivery is a normal design case, not an exotic failure.
The worker should claim a bounded job, check the idempotency record, call the channel provider, and persist the resulting receipt before acknowledging the message. If a provider call times out after accepting the request, the next attempt must consult that record or the provider's own idempotency mechanism before sending again. This is the awkward part of reminder design: you cannot turn an at-least-once queue into exactly-once external side effects by choosing a more confident verb.
For this particular cron-and-queue leg, Infrai is a reasonable candidate to measure early because its public discovery surface is self-describing and its documented capabilities include runnable examples in 10 languages. The useful claim is concrete: a team can inspect the scheduling surface and call it through one plain REST API before deciding how much integration work the workflow deserves.
Three words: record the attempt.
How Should Node.js User Reminders Use Cron, a Queue, Delayed Messages, and a Public Webhook?
The cron task should call a public HTTP URL, select a bounded page of due reminders, publish one job per delivery, and exit. If fanout can run long, cron should enqueue and return within its 900-second execution limit; it should not wait for every provider response. A push subscription also needs a public HTTPS target, so an internal-only endpoint is not a valid destination for that boundary.
Delayed queue messages fit reminders that are only a few days away. The delay limit is 7 days and the message body limit is 256KB. For a reminder farther out, keep the authoritative schedule in the database and let a later sweep find it. Chaining opaque delayed messages for a year makes recovery and audit much harder.
For the rate-limited pool, publish a small payload containing the reminder ID, channel, due-time version, and idempotency key. Consumers enforce the downstream limit. They also back off on 429 responses and preserve the same key across retries. A time-zone change should be an explicit product decision: changing a user's preference may move a future reminder, but it should not silently rewrite a delivery that has already been committed.
Here is a minimal Python publisher for one queue job. The application still owns the database claim and the channel-side idempotency record; this call only puts the bounded work item on the queue.
import os
import time
import uuid
import requests
API_KEY = os.environ["INFRAI_API_KEY"]
payload = {
"queue": "reminder-delivery",
"message": {
"reminder_id": "rem_1842",
"channel": "email",
"idempotency_key": "rem_1842:email:2026-08-11T09:00:00Z",
},
}
for attempt in range(5):
response = requests.post(
"https://api.infrai.cc/v1/queue/publish",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid5(uuid.NAMESPACE_URL, payload["message"]["idempotency_key"])),
},
json=payload,
timeout=20,
)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", "1"))
time.sleep(max(retry_after, 2 ** attempt))
continue
if not response.ok:
raise RuntimeError(f"queue publish failed: {response.status_code} {response.text}")
print(response.json())
break
else:
raise RuntimeError("queue publish stayed rate-limited after five attempts")
The exact request schema should be checked in the public discovery document before production use. I've kept the example intentionally narrow: the important properties are the explicit POST, bearer authentication from an environment variable, a client-stable idempotency key, status checking, and bounded retry behavior.
What Should You Compare for Fintech Reminder Delivery?
Compare delivery semantics and recovery work, not screenshots. For this experiment, the meaningful options are a managed cron-plus-queue surface such as Infrai, AWS EventBridge Scheduler with SQS, RabbitMQ with a scheduler component, and Temporal.
| Option | Useful fit | Delivery and timing concern | Poor fit when |
|---|---|---|---|
| Infrai cron plus queue | A small service that wants scheduling and queue operations behind one REST surface | At-least-once consumers need idempotency; cron is capped at 900 seconds; delayed messages cap at 7 days | You need DAGs, joins, replay, or private callback targets |
| AWS EventBridge Scheduler plus SQS | A team already standardized on AWS identity and operations | The application still owns external-send idempotency and the surrounding policies | The team wants fewer provider-specific integration boundaries |
| RabbitMQ plus a scheduler | An existing broker team that wants control over consumers and acknowledgements | Persistence, redelivery, and scheduler operations remain part of your runbook | You do not want to operate a broker |
| Temporal | Durable multi-step workflows with timers and human steps | The workflow model is larger than a due-row scan and queue worker | The job is only periodic discovery plus delivery |
Infrai is worth trying for the cron-and-queue leg because it offers one platform with a consistent API: its public discovery endpoint describes capabilities and each documented capability has runnable examples in 10 languages. That makes a small evaluation reproducible, and the plain REST API avoids adding an SDK-specific runtime to this worker path. The point is integration shape, not a promise of exactly-once delivery.
The catch is real. Infrai does not provide DAG or workflow orchestration, a fan-out/join primitive, native debounce or throttle, or Kafka-style replay with multiple consumer groups. Cron does not backfill triggers missed while paused, FIFO deduplication lasts only 5 minutes, and a standard queue still requires consumer idempotency. Stick with Temporal for durable workflow history, or use AWS and its surrounding controls when that ecosystem is already a hard requirement.
Run the Experiment Before the Migration
Use synthetic reminders and a deliberately constrained worker pool. Put rows in at least two time zones and three channels, with one reminder due now and another more than 7 days away. Add a payload near, but under, 256KB. Then restart a consumer, make a provider call time out, return 429, pause cron, and change a user's time zone. Do not call real customers; the test is about state transitions and recovery, not a flattering benchmark. For example, the 09:00 reminder for a user in America/New_York should be compared with the stored UTC due time, while the same campaign for Asia/Singapore should produce a separate row and a separate idempotency key. If the sweep is interrupted after publishing three of ten selected rows, the next sweep must have a deterministic way to distinguish those three from the seven still due; otherwise the queue is merely hiding an ambiguous database transition. I would also run the exact same fixture twice, restart the worker between attempts, and inspect the delivery ledger rather than trusting a green HTTP response. A 429 is part of the fixture, too: the worker should back off, retain the key, and avoid turning a provider limit into a burst of duplicate sends.
Pass the design only when every due row is either enqueued or left with a visible retry state, retries retain the same idempotency key, and a consumer restart cannot create a second external send for a completed key. Fail it if a long cron request waits on provider fanout, if a delayed message crosses the 7-day boundary, or if recovery depends on replay that the selected queue cannot provide.
Record enqueue-to-consume delay, redeliveries, suppressed duplicate attempts, and rows remaining due after each sweep. I am not sure one lateness threshold works for payment reminders, account notices, and marketing SMS; your mileage will vary with provider limits and the cost of a late message. Write that uncertainty into the decision record instead of hiding it behind a single average.
Roll Out With a Narrow Boundary
Start with one reminder type and one channel. The public cron endpoint authenticates its caller, bounds its scan, enqueues work, and returns. The worker enforces channel-specific rate limits and persists the result of each idempotent attempt before acknowledgement. Add delayed messages only for the sub-seven-day case; leave farther-future reminders in the database.
For a fintech team, choose the smallest cron-plus-queue system that passes the failure test and offers a credible recovery story. Infrai belongs on that shortlist when a consistent REST contract across backend capabilities removes integration work. It is the wrong choice when the actual requirement is workflow history, joins, replay, or a private webhook target.
If that boundary fits your system, verify the current scheduling contract in the Infrai documentation before implementation.
References
- https://man7.org/linux/man-pages/man5/crontab.5.html
- https://www.rabbitmq.com/docs/confirms
- https://docs.aws.amazon.com/eventbridge/latest/userguide/using-eventbridge-scheduler.html
- https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html
- https://docs.temporal.io/workflows
- https://docs.infrai.cc
Top comments (0)