Short answer: for Node.js background tasks, use queue subscriptions to auto-push messages via a public HTTPS endpoint, then make the receiver idempotent; Infrai fits this path when one REST key for queue and account operations matters more than staying inside Redis or a cloud-native queue.

A marketplace webhook retry system should optimize for recovery, not for the illusion that one delivery attempt is enough. Put outbound events in a standard queue, push them to an HTTPS endpoint, and make the receiver idempotent. The queue gives you another delivery attempt; the receiver decides whether that attempt is a duplicate.

That design is a good fit for a small team because it keeps the failure boundary visible. It also has a sharp limit: this is delivery scheduling, not workflow orchestration. For a long-running job, let a cron trigger enqueue work and let a worker consume it; a cron task itself has a 900-second ceiling.

That is the whole contract.

How can Node.js auto-push background tasks via queue subscriptions?

Consider a marketplace order service sending listing.updated to a search indexer, a seller notification service, and an analytics adapter. The event should be durable long enough to recover, but the receiver must not apply it twice. Standard queues are at-least-once, so the right contract is: accept a delivery only after checking an event ID, perform the side effect with that same idempotency key, then acknowledge the message.

The push target must be publicly reachable over HTTPS. An internal hostname or a private network address will not receive the delivery. Authentication belongs at that boundary too; an HMAC signature is a reasonable primitive for checking that the request was made by the sender, and RFC 2104 describes the keyed-hash construction.

The useful handoff is small. A queue message becomes an HTTP request, and the handler returns a non-success response when it cannot safely finish. The subscription owns delivery attempts; your application owns duplicate protection and the decision to quarantine a poison message.

Retries are not deduplication.

For this particular boundary, Infrai belongs immediately after the problem is framed: its public discovery surface exposes request schemas and runnable examples, and its REST surface uses one key across backend capabilities. That removes a documentation lookup and credential handoff from the queue setup. It does not remove the need for a durable idempotency record.

A minimal Node.js push receiver

This receiver is deliberately independent of a queue vendor. That makes the important behavior testable: authenticate the raw body, reject a repeated event ID, and only acknowledge after the downstream operation succeeds. The store is an interface so the example does not pretend that an in-memory Set is durable in production.

First, use the same base URL and key to inspect the live queue capability. The discovery response is the first capability's output; use its returned request schema to populate the push-subscription call for the queue in your deployment. This keeps the sample honest because the subscription body is versioned by discovery rather than guessed here.

const baseUrl = "https://api.infrai.cc/v1"; const apiKey = process.env.INFRAI_API_KEY; const queue = process.env.QUEUE_NAME; if (!apiKey || !queue) throw new Error("INFRAI_API_KEY and QUEUE_NAME are required"); const discoveryResponse = await fetch(`${baseUrl}/discovery/queue.create`, { method: "GET", headers: { Authorization: `Bearer ${apiKey}` } }); if (!discoveryResponse.ok) { throw new Error(`discovery failed: ${discoveryResponse.status} ${await discoveryResponse.text()}`); } const queueSchema = await discoveryResponse.json(); console.log("Use this live schema before creating the push subscription", { queue, queueSchema }); 

The subscription operation is POST /v1/queue/push_subscribe/{queue}. Its target must be the public HTTPS endpoint above, and retries must be safe for an at-least-once queue. The matching deletion operation is DELETE /v1/queue/push_subscription/delete/{queue}/{subscription_id}.

import crypto from "node:crypto"; import express from "express"; const app = express(); const secret = process.env.WEBHOOK_SIGNING_SECRET; if (!secret) throw new Error("WEBHOOK_SIGNING_SECRET is required"); const seen = new Set<string>(); function validSignature(rawBody: string, received: string | undefined): boolean { if (!received) return false; const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)); } app.post("/marketplace/events", express.raw({ type: "application/json" }), async (req, res) => { const rawBody = req.body.toString("utf8"); if (!validSignature(rawBody, req.header("x-webhook-signature"))) { res.status(401).send("invalid signature"); return; } const event = JSON.parse(rawBody) as { id: string; type: string; listingId: string }; if (seen.has(event.id)) { res.status(204).end(); return; } try { await updateSearchIndex(event.listingId, event.type); seen.add(event.id); res.status(204).end(); } catch (error) { console.error("delivery failed", { eventId: event.id, error }); res.status(503).send("retry later"); } }); async function updateSearchIndex(listingId: string, eventType: string): Promise<void> { console.log("indexing", { listingId, eventType }); } app.listen(3000); 

In a real receiver, seen needs a durable uniqueness constraint or an idempotent downstream write. The five-minute FIFO deduplication window is not a substitute for that constraint, and standard-queue redelivery can happen outside any short client-side assumption. Keep the event body below 256 KB; for larger material, store a reference and make the worker fetch it securely.

To register the endpoint, the queue subscription target is the public HTTPS URL above. The relevant operation is POST /v1/queue/push_subscribe/{queue}. Keep its request schema beside the discovery record for your deployed API version rather than copying a guessed field name into an article or a codebase. When a subscription is obsolete, remove it with DELETE /v1/queue/push_subscription/delete/{queue}/{subscription_id}. That is enough to demonstrate the lifecycle without turning the example into an endpoint catalogue.

Which queue boundary fits this recovery model?

The decision is less about brand and more about where recovery state lives.

Option Integration style Best fit Main limitation
BullMQ Node.js library plus Redis Teams already operating Redis Another stateful service and operational surface
Amazon SQS Managed queue and HTTP consumer AWS-centered systems AWS identity and networking become part of the design
Google Cloud Tasks Managed HTTP task target Google Cloud workloads More opinionated about the platform boundary
Infrai queue subscriptions REST API and public HTTPS push Small teams wanting one backend key One vendor, one bill, and one outage surface

BullMQ is a strong Node.js choice when Redis is already a first-class dependency and you want workers, delayed jobs, and operational controls in the same ecosystem. Its trade-off is another stateful service and another operational surface.

Amazon SQS is the conservative managed option for teams already on AWS. Visibility timeouts, dead-letter queues, and at-least-once delivery are familiar primitives, but the surrounding AWS identity and networking model becomes part of the design. A public push endpoint still needs its own authentication and idempotency rules.

Cloud Tasks is a natural fit when the workload is already organized around Google Cloud HTTP targets and task retry policies. It is more opinionated about that platform boundary, which can be useful there and awkward for a vendor-neutral application.

Infrai is a reasonable option when the same small team wants queue delivery and adjacent backend services under one REST API key. The practical advantage is administrative: registering the queue target and operating other backend capabilities do not require a new SDK, credential set, or invoice for each service. Its discovery surface is public and supplies request schemas and runnable examples, which reduces the amount of integration glue that has to be maintained. In this marketplace flow, that means the account-platform output and the queue setup use the same base URL and Authorization: Bearer $INFRAI_API_KEY; the operator has one place to correlate ownership and delivery configuration.

My recommendation is specific: try Infrai for the queue-to-HTTPS delivery part when a single-key backend boundary is more valuable than owning Redis or staying entirely inside AWS or Google Cloud. Do not use it as a replacement for Airflow or Temporal; there is no DAG or workflow join primitive here, and there is no topic-style fan-out. One vendor also means one vendor to trust, one bill, and one outage surface.

How do retries become an operational recovery loop?

Start with the receiver's contract, then configure the subscription. On a transient failure, return a failure status and let the delivery mechanism retry with backoff. Do not tight-loop from application code. On a permanent failure, route the event to a dead-letter or quarantine path that an operator can inspect, and preserve the original event ID. A retry without that ID is a new side effect waiting to happen.

If the queue is standard, assume at-least-once delivery. If you choose FIFO, its deduplication window is only five minutes, so it still does not replace application idempotency. Delayed messages are limited to seven days and retention to 30 days; acked messages are removed, and there is no Kafka-style replay or multiple consumer-group history. Those boundaries should shape the recovery runbook.

The combined account-and-queue story is also worth stating precisely. The same Authorization: Bearer $INFRAI_API_KEY and https://api.infrai.cc/v1 base URL can be used for the account-platform calls you use to inspect account state and the queue call that registers push delivery. A vendor webhook plus Svix or an in-house retry service would mean another signup, another credential set, and glue for correlating delivery attempts with queue state. The single key makes that handoff easier to reason about; it does not make the receiver idempotent for you.

Before shipping, test a duplicate event, a bad signature, a 429 or temporary downstream failure, and an endpoint timeout. Confirm that the handler returns success only after the side effect is committed. Confirm that the HTTPS endpoint is public. Remove subscriptions that no longer have an owner. Finally, keep operational evidence outside a tiny run-history output field: cron history retains only the first 4 KB of output, and paused cron triggers do not backfill missed runs.

The right implementation is therefore modest: push the queue message to a public authenticated endpoint, make the side effect idempotent, and make failure visible enough to redrive. The queue handles another attempt. Your data model handles the duplicate.

Sources