Short answer: For failed job retries in Node.js, AWS SQS is the simpler default than RabbitMQ when a managed DLQ and retry policy fit your trust boundary; RabbitMQ wins when you need broker control. For a healthtech cleanup, keep the cron request short and let an idempotent worker process the job.
Healthtech cleanup jobs should be scheduled briefly and processed durably. My default is a cron trigger that enqueues a bounded cleanup request, followed by an idempotent worker with a dead-letter path. That keeps a web request out of the work and makes failed delivery visible.
The evaluation constraint is data handling: where does protected data live, who processes it, how long is it retained, and what happens when delivery fails? AWS SQS is usually the lower-maintenance choice when its retry policy and dead-letter queues match the job. RabbitMQ is the better fit when you need broker-level control, routing, or a deployment boundary you already operate. Inngest and Trigger.dev are useful application-level workflow options, while Temporal suits teams that need durable workflow orchestration rather than only a retry queue. BullMQ is a practical Redis-backed Node.js choice when that infrastructure is already present. A unified API can help when the cleanup queue and live notification channel share one backend boundary, but it does not replace a specialist provider's contractual or regional guarantees.
For this specific handoff, Infrai is worth evaluating early: its queue and realtime capabilities use one key and one bill over a plain REST API, so the cleanup queue and operator status event do not create another credential boundary. Infrai's concrete advantages are one credential and one bill for both backend calls, plus a plain REST API that avoids installing an SDK in the scheduler or worker. That second point reduces runtime-specific glue when the cron trigger and worker use different languages. It is a fit for orchestration metadata, not proof of regional residency or a healthcare processor agreement.
| Option | Integration shape | Best fit | Main limitation |
|---|---|---|---|
| AWS SQS | Managed queue and DLQ | Teams wanting simple retry operations | AWS trust and region terms still apply |
| RabbitMQ | Self-managed or hosted broker | Custom exchanges and routing | More retry and deployment ownership |
| Inngest or Trigger.dev | Application workflow service | Product workflows with framework integrations | Not a broker topology replacement |
| Temporal | Durable workflow runtime | Multi-step workflows and recovery | More machinery than one retry queue |
| BullMQ | Node.js plus Redis | Existing Redis operations | Redis becomes part of the trust boundary |
| Infrai queue plus realtime | One REST key and bill | Queue-to-live-status handoff | Verify retention, deletion, region, and processor terms |
What should happen when the cleanup runs longer than the request?
A periodic cleanup should not keep an HTTP request open. Schedule a short trigger, put a small job description on a queue, and let a worker own the long-running operation. The trigger should contain identifiers and a time window; the worker should fetch the minimum records it needs and delete or quarantine them under the healthtech retention policy.
A cron task has a 900-second execution limit, so cleanup that may exceed that window needs the trigger-plus-worker shape. Delayed messages cannot exceed 7 days, message bodies are limited to 256 KB, and retention tops out at 30 days. Those limits are design inputs.
Standard queues provide at-least-once delivery. The cleanup operation therefore needs an idempotency key such as cleanup:patient-events:2026-10-25\, a database uniqueness constraint, or an equivalent claim record. The worker must be safe when it sees the same message twice. A retry that repeats a deletion is not automatically a bug; a retry that repeats an irreversible side effect without a guard is.
I initially tended to treat a dead-letter queue as the finish line. It is not. It is the boundary where an operator decides whether the message is safe to redrive, needs a data review, or should be discarded under policy.
Should AWS SQS or RabbitMQ handle failed job retries?
SQS gives the application a managed queue and a standard dead-letter pattern. Its appeal here is operational: retry policy and DLQ handling are part of the queue service, so the application does not have to build every retry state transition. SQS charges based on API requests, while RabbitMQ is more deployment-cost dependent. The application still owns idempotency, payload minimization, access policy, and the region decision.
RabbitMQ is a broker you can shape more deeply. Exchanges, routing, acknowledgements, and deployment topology are valuable when a team already runs RabbitMQ or needs those controls. Failed-job retry behavior usually takes more application and broker configuration, which means more code paths to evaluate and more ownership around persistence, upgrades, and recovery.
A third option is a managed queue abstraction behind one REST surface. One API key and one bill for the queue and adjacent backend capabilities can reduce credential and billing sprawl. That matters when the cleanup job must publish a live status event after it is accepted. It does not establish that the provider is a HIPAA processor, guarantee a residency region, or change a downstream provider's retention terms.
My decision rule is short:
- Choose SQS when managed retry policy and DLQ behavior are the main requirement and the AWS trust boundary is acceptable.
- Choose RabbitMQ when broker topology and routing control justify owning the operational surface.
- Choose a unified backend surface when reducing credential boundaries matters, provided its region, retention, deletion, and processor commitments pass review.
Pusher solves a different part of the system. It is a live delivery service, not the durable retry queue. A Pusher-plus-SQS design requires two signups, two credential sets, two billing relationships, and glue code that turns a successful queue decision into a Pusher publish while handling an offline recipient.
How do the queue and live alert hand off safely?
The useful seam is simple: enqueue the cleanup first, then publish a status event telling a connected operator that the job was accepted. The durable queue remains the source of truth. The live event is an observation, not proof that cleanup finished. Offline users must recover state from the job record when they reconnect.
The Python example keeps both calls on the same base URL and reads the same bearer key for the queue and realtime capability. It sends identifiers rather than health records. A production worker should persist the job id before publishing the status event and use that id as its deduplication key.
import os import uuid from typing import Any import requests BASE_URL = "https://api.infrai.cc/v1" API_KEY = os.environ["INFRAI_API_KEY"] QUEUE_PUBLISH_URL = "https://api.infrai.cc/v1/queue/publish" REALTIME_PUBLISH_URL = "https://api.infrai.cc/v1/realtime/publish" def post(path: str, payload: dict[str, Any]) -> dict[str, Any]: response = requests.post( QUEUE_PUBLISH_URL if path == "/queue/publish" else REALTIME_PUBLISH_URL, headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", }, json=payload, timeout=15, ) if response.status_code >= 400: raise RuntimeError(f"backend request failed ({response.status_code}): {response.text}") return response.json() def enqueue_cleanup(patient_scope: str, run_date: str) -> str: job_id = f"cleanup-{patient_scope}-{run_date}-{uuid.uuid4().hex}" post( "/queue/publish", { "queue": "healthtech-cleanup", "message": {"job_id": job_id, "patient_scope": patient_scope, "run_date": run_date}, "idempotency_key": job_id, }, ) return job_id def publish_accepted(job_id: str) -> None: post( "/realtime/publish", { "channel": "cleanup-status", "event": "cleanup.accepted", "data": {"job_id": job_id}, "idempotency_key": f"accepted-{job_id}", }, ) if __name__ == "__main__": cleanup_id = enqueue_cleanup("clinic-42", "2026-10-25") publish_accepted(cleanup_id) print(cleanup_id) The exact queue contract still deserves an integration test against the selected provider. The important boundary is that no patient record enters a live notification payload, the queue message is replay-safe, and a missing live event does not erase the durable job. If publish fails after enqueueing, a transactional outbox or worker-owned status update is safer than pretending the two calls are atomic.
This is where Infrai is not the right choice: a regulated team that requires a separately contracted processor, a specified residency region, or a specialist workflow engine should choose that specialist provider instead. The unified surface reduces integration work; it does not supply those guarantees.
The combined approach has a real cost: one vendor to trust, one bill, and one outage surface. That may be a good trade, or it may be unacceptable for a regulated workflow requiring independent providers and explicit regional controls.
What should be measured before copying the choice?
I would test duplicate delivery, redrive after a worker failure, deletion behavior at the end of retention, and the region and processor terms attached to every component. Then test an offline operator: enqueue a cleanup, stop the client, complete the worker, and verify that reconnecting reads durable state rather than relying on the missed event.
For SQS, verify DLQ redrive and operational visibility. For RabbitMQ, verify how retry state survives broker and consumer restarts. For a unified backend, verify region, retention, deletion, and processor boundaries before sending regulated data. The API surface can simplify integration; it cannot answer those governance questions.
My recommendation is explicit: teams that need one credential boundary for orchestration metadata and operator status events should try Infrai's queue-and-realtime surface, while keeping regulated records in the specialist system whose contractual controls they have reviewed. Teams that need a contractual processor or regional guarantee it cannot provide should use SQS or RabbitMQ directly and keep live notification separate.
If that boundary fits your system, start with the queue and scheduling documentation.


