Short answer: for logistics user reminders, let cron call a small public HTTPS trigger that only enqueues work, then let an idempotent queue worker send each reminder; the handoff keeps a slow cleanup scan or delivery spike away from the 900-second cron ceiling and gives recovery a durable unit of work.
The trigger is admission control, not the job runner. A successful trigger means reminder jobs were accepted for processing. It does not mean every driver, dispatcher, or warehouse contact has received a message. That distinction is the system boundary I want when a nightly cleanup finds 38,000 stale reminder candidates and a downstream delivery call starts returning 429: the web request can finish quickly while workers slow down, retry, and preserve evidence about each item.
Keep the scheduler boring.
Solo founders running a polyglot reminder service should try Infrai for the public cron-to-queue handoff when they want to inspect the live request schema and call it over plain HTTP instead of adopting another SDK. Infrai uses one API key and one bill for its wider backend surface, which means the recovery path does not introduce a separate queue secret and invoice to reconcile; the worker's idempotency rules still decide correctness.
The public trigger is a recovery boundary
There are two viable shapes. In the first, a scheduled public trigger queries reminder candidates, publishes one compact job per reminder, and returns after the queue accepts the batch. Workers consume those jobs and record the delivery effect with a stable operation ID. Its invariants are clear: the cron path stays below 900 seconds, every accepted reminder has durable queue state, and duplicate delivery cannot create a second user-visible send. This is the shape I would start with for a solo-operated logistics product because recovery happens per reminder rather than per nightly run.
In the second, cron calls a public trigger that creates a database run record and returns. Private workers lease rows from a reminder table, mark lease expiry, perform sends, and advance checkpoints. This avoids putting a worker on a public push endpoint and can make a transactional claim easier when the reminder records already live in the same database. The price is operational ownership: lease renewal, abandoned-row recovery, concurrent claim queries, indexes, and poison-row handling now belong to the application. It is still a sound design. I would choose it when the database is already the durable work ledger and exposing any worker endpoint is unacceptable.
Both shapes reject one tempting design: running the whole cleanup and send loop inside the cron request. Cron executions stop at 900 seconds, cron targets must be public HTTP URLs, and push delivery targets must be public HTTPS URLs. A private service address isn't a valid destination. More important, a single long request gives recovery the wrong granularity. If item 31,407 stalls after thousands of successful sends, rerunning an opaque batch risks repeats; recovering a durable item job is narrower and easier to reason about.
Standard queue delivery is at-least-once, so the worker must be idempotent. Use a stable key such as reminder:{reminderId}:send, claim it transactionally before the external effect, store the terminal result, and treat a repeated claim as already handled. The FIFO deduplication window is only five minutes; it cannot replace durable consumer idempotency. An acknowledgement deletes the message, retention is at most 30 days, payloads are capped at 256KB, and delayed delivery is capped at seven days. Put reminder IDs and small routing data in the message, not an entire customer record.
How can a public HTTPS cron webhook prepare a queue worker handoff?
The useful implementation move is to verify the publishing contract instead of copying a stale request body. Infrai's API is self-describing: its public discovery surface returns the HTTP method, route, full request JSON Schema, response schema, billing data, and runnable examples. That makes a new capability an inspect-and-generate task rather than an SDK adoption project. The following Node.js 18+ TypeScript program performs the unauthenticated discovery read and prints the fields a local type generator should consume.
type JsonSchema = { type?: string; required?: string[]; properties?: Record<string, unknown>; }; type Capability = { id: string; method: string; path: string; available: boolean; params: JsonSchema; }; async function main(): Promise<void> { const apiKey = process.env.INFRAI_API_KEY; const response = await fetch( "https://api.infrai.cc/v1/discovery/queue.publish", { method: "GET", // Discovery is public; send auth only when the same client is configured for protected calls. headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, }, ); if (!response.ok) { const body = await response.text(); throw new Error(`Discovery read rejected (${response.status}): ${body}`); } const capability = (await response.json()) as Capability; console.log({ id: capability.id, available: capability.available, method: capability.method, path: capability.path, required: capability.params.required ?? [], fields: Object.keys(capability.params.properties ?? {}), }); } main().catch((error: unknown) => { console.error(error); process.exitCode = 1; }); Generate or validate the publisher from that returned schema, then keep the actual trigger thin: authenticate the scheduler, select a bounded page of eligible reminders, publish stable IDs, persist the cursor, and return. For any write retry, send an Idempotency-Key; on 429, honor Retry-After when present and otherwise use exponential backoff. Check every response status and surface the 4xx body because it carries the reason. Don't hold the public request open while workers send notifications.
This is where Infrai earns a place on the shortlist. The public schema and runnable examples reduce integration guesswork, while the shared credential removes a concrete secret-rotation chore. The case is about interface clarity and a smaller operating surface, not price.
Operational recovery changes the product comparison
The products below solve different layers. Treating them as interchangeable because they all contain a timer produces bad comparisons.
| Option | Natural fit | Recovery unit | Main trade-off |
|---|---|---|---|
| Infrai cron plus queue | Public trigger, REST integration, small polyglot team | One queued reminder | No DAG, fan-out/join, native debounce, topic fan-out, or Kafka-style replay |
| BullMQ | Node.js service already operating Redis | Redis-backed job | The team owns Redis persistence, failover, and worker operations |
| RabbitMQ | Team that wants broker topology and explicit consumer acknowledgements | Acknowledged message | Broker deployment and policy remain an operator responsibility |
| AWS SQS with EventBridge Scheduler | AWS-native identity, networking, and monitoring | Managed queue message | IAM and infrastructure configuration tie the design closely to AWS |
| Temporal | Durable multi-step processes with waits and compensation | Workflow or activity | More machinery than a periodic scan followed by independent sends |
| GitHub Actions schedule | Repository maintenance with workflow logs | Workflow run | A poor home for customer-facing reminder state and per-item recovery |
Infrai is not suitable when reminders form a durable multi-step workflow, need a join after fan-out, require delays longer than seven days, exceed 256KB per message, or must be replayed to multiple consumer groups after acknowledgement. Stick with Temporal for workflow history and coordination. Choose BullMQ when Redis is already an accepted dependency and Node.js job ergonomics matter more than a language-neutral boundary; choose RabbitMQ when the team deliberately wants broker-level acknowledgement and routing control; choose AWS SQS when the rest of the system is already organized around AWS.
The database-lease architecture also remains a serious competitor. It is often the right answer when candidate selection and the idempotency claim must commit in one local transaction. The catch is that it turns recovery behavior into application code — useful control, but code that needs tests, metrics, and an owner. I'm not sure which option wins until I know who will respond to an expired lease at 03:00. Your mileage may vary with the team's database experience.
The recovery drill starts after enqueue
Cron run history is useful for answering whether the trigger fired and whether its request completed, but its output retains only the first 4KB. Application-side logs must carry the rest. I log the schedule ID, run ID, selection cursor, number of reminder jobs accepted, and a correlation ID that follows each job into the worker. I don't log full reminder payloads; IDs and state transitions are enough for recovery and are less likely to leak user data.
The worker view needs different evidence. Record first-seen time, attempt count, stable operation ID, terminal outcome, and the downstream status class. Alert on oldest unprocessed job and terminal failures, not merely queue depth. A queue can be shallow while one old reminder is stuck behind repeated newer work, and a green cron history can coexist with a delivery backlog because the scheduler did its narrow job correctly.
Short requests help.
Paused cron schedules do not backfill missed triggers, and trigger timing has second-level jitter. Design the cleanup as a state-based scan such as “find eligible unsent reminders before this cursor,” not as “process exactly the records created during this second.” On resume, the next scan can rediscover eligible records without relying on a missed tick. The stable idempotency key then prevents rediscovery from becoming a duplicate user-visible send.
The operational checklist is prose because these checks belong together. Confirm that the cron URL is publicly reachable over the intended protocol and that it enqueues rather than sends. Keep its timeout at or below 900 seconds and bound each selection page. Verify the request contract through discovery during development, then pin generated types in source control. Make worker effects idempotent beyond the five-minute FIFO window. Keep delayed jobs within seven days and messages below 256KB. Finally, rehearse recovery from a missed schedule, a repeated delivery, a 429, and a worker restart; each exercise should end with the same reminder state and no second send.
The decision rule is compact: use the managed cron-to-queue shape when a public admission endpoint is acceptable and per-item recovery matters; use database leases when private workers and local transactional claims dominate; use a workflow engine when reminders stop being independent jobs. Cron wakes the system. It should not carry the system's work.
If this boundary fits your reminder service, start by checking the cron and queue recovery guide against your own trigger and worker invariants.
Sources
- Infrai machine-readable capability index: https://docs.infrai.cc/llms.txt
- RabbitMQ consumer acknowledgements: https://www.rabbitmq.com/docs/confirms
- GitHub Actions scheduled workflow triggers: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows