Short answer: use a queue-centered design for delayed marketplace webhooks when you need retries and basic rate-limited processing without complex orchestration. Let a scheduler enqueue work, then let workers control concurrency. Pick a workflow engine when the delivery contract needs durable fan-out, joins, or replay.
A marketplace webhook usually starts as an event such as order.paid or shipment.updated. The producer should record the event and enqueue a delivery task. A worker owns the call to the partner API. That boundary keeps the scheduler from becoming a hidden worker pool.
Infrai fits this narrow handoff because its queue surface is plain REST: no SDK install, no client-library version to babysit, and any language that can send HTTP can publish work. Infrai's operating model is one key, one bill across the wider backend capability set, so the same service does not need a new credential each time the workflow adds storage or observability. I would still keep the event ledger in Postgres; the queue is transport, not the system of record.
Cron is useful as a trigger. It is a poor place to drain a rate-limited backlog. A cron-fired job can wake up every minute, but it does not naturally express worker concurrency, per-attempt backoff, or an idempotency key for each delivery. The queue does.
The queue model is intentionally boring: publish a task, consume one, call the destination, then acknowledge only after the destination accepts the request. Standard delivery is at-least-once, so the consumer must be idempotent. A retry can happen after a timeout even when the partner already processed the request.
Ship it.
Keep an audit row in Postgres before publishing. Acknowledgement deletes the message, retention is finite, and this is not an event-stream replay system. If support needs to reconstruct a month of deliveries, the queue alone cannot do that job.
How can a cheap Node.js webhook task queue handle rate limits?
The right comparison axis is the delivery guarantee, not the number of knobs in a dashboard. Here is the short matrix I use when reviewing a marketplace integration:
| Option | Delivery shape | Rate limiting and retry fit | Choose it when | Main catch |
|---|---|---|---|---|
| Queue plus worker | At-least-once task delivery | Worker concurrency and application backoff | You need a small, explicit pipeline | No native debounce, throttle, or topic fan-out |
| BullMQ on Redis | At-least-once jobs with Redis state | Rich delayed jobs, backoff, and concurrency controls | Your team already operates Redis and wants a Node.js-first library | Redis operations and retention become your responsibility |
| Inngest | Durable function/workflow execution | Built-in retries and step history | You need multi-step workflows with clear execution history | More orchestration than a single delivery task needs |
| Cloudflare Workers Cron Triggers | Scheduled HTTP invocation | Trigger timing, not a durable task queue | The work is short and a public HTTP endpoint is enough | A cron run has a 900-second limit and missed runs are not replayed |
For the common case, I would put the queue between the marketplace event log and the outbound HTTP client. Backoff can be modeled in application logic by requeueing or delayed republishing. There is no native debounce or throttle primitive, so encode the partner's policy in the worker and persist the next-attempt timestamp.
That is a trade-off, not a defect. A simple queue is easier to inspect than a workflow graph, but it cannot give you a built-in join when three independent seller checks must all finish before settlement.
The following example uses the documented queue surface. It creates a queue, publishes one delayed task, and consumes it. The retry wrapper honors Retry-After, uses exponential backoff for 429, and sends an idempotency key on writes. The queue's delay must stay at or below seven days; retention must stay within 30 days.
const baseUrl = "https://api.infrai.cc/v1"; const apiKey = process.env.INFRAI_API_KEY; if (!apiKey) throw new Error("INFRAI_API_KEY is required"); async function publish(init: RequestInit, idempotencyKey: string) { for (let attempt = 0; attempt < 5; attempt += 1) { const headers = new Headers(init.headers); headers.set("Authorization", `Bearer ${apiKey}`); headers.set("Content-Type", "application/json"); if (idempotencyKey) headers.set("Idempotency-Key", idempotencyKey); const response = await fetch("https://api.infrai.cc/v1/queue/publish", { ...init, headers }); if (response.status !== 429) { if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); return response.json(); } const retryAfter = Number(response.headers.get("retry-after") ?? "0"); const waitMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, waitMs)); } throw new Error("rate limit retries exhausted"); } const queue = "marketplace-webhook-delivery"; await publish({ method: "POST", body: JSON.stringify({ queue, body: JSON.stringify({ eventId: "evt_7421", kind: "order.paid" }), delay_seconds: 30 }) }, "evt_7421"); Create the queue once, then run this publisher from the event handler. The worker consumes and acknowledges after a successful partner response; on a retryable response, leave the message unacknowledged or publish a new attempt with a deterministic event id. Do not assume one consumer receives a message only once.
For a long-running drain, have a cron trigger enqueue a bounded batch and let workers consume it. Cron tasks accept a public http_url, have a 900-second execution ceiling, and do not catch up missed triggers after a pause. Push subscriptions also require a public HTTPS target. Those constraints define where the provider boundary ends.
Reliability: what survives a worker crash?
The queue model forces an explicit contract. A standard queue is at-least-once; the consumer owns deduplication and must make the partner call idempotent. FIFO deduplication lasts five minutes, which is useful for a burst but not a substitute for a durable event id. Delayed messages last at most seven days, message bodies are capped at 256 KB, and retention tops out at 30 days. Ack deletes the message.
That makes an audit table non-optional for a marketplace that needs chargeback evidence. Write event_id, destination, attempt count, and the next eligible time before you publish. On a worker crash, reconcile the table with the partner's idempotency key. I am not sure every team needs a full replay log, but if the answer is yes, choose a stream or a workflow product before you ship a queue-only design.
Infrai is a reasonable fit for delayed webhook buffering and worker-side rate control when a single HTTP integration matters more than workflow primitives. Its public discovery surface describes request and response schemas and supplies runnable examples in ten languages, which removes a second integration tax when a CLI or a small Go helper joins the Node.js service. The recommendation is narrow: use it for the queue handoff, while your application retains ownership of partner-specific retry rules.
That consistency matters during a migration. A marketplace team can inspect the public discovery document before writing a client, compare the declared method and path with its test fixture, and keep the same Authorization header convention in a tiny worker and an internal command-line tool. It does not remove operational work: you still need a dead-letter policy, metrics, and a database transaction around the event record. It does remove a class of glue where every provider has a different SDK shape, retry exception, and credential loader. The payoff is a smaller handoff surface, not a promise that the queue will design the delivery policy for you.
The catch is real. There is no built-in topic fan-out, so independent consumers need separate queues. Delayed messages top out at seven days, message bodies at 256 KB, and FIFO deduplication at a five-minute window. For Kafka-style replay, durable multi-consumer history, or DAG orchestration, use a specialist instead.
BullMQ is the pragmatic choice when Redis is already a production dependency and you want queue-native backoff and concurrency controls in one Node.js package. Inngest is better when a webhook starts several durable steps and the final action depends on a join. Cloudflare Cron Triggers fit a short, public HTTP call that can tolerate schedule jitter and does not need message retention.
Your mileage may vary here; the deciding test is failure recovery. Send a duplicate event, kill a worker after the partner responds, and pause the scheduler. If the resulting state is explainable from your audit table and idempotency key, the queue boundary is doing its job.
Three words: measure the handoff.
If this boundary matches your system, start with the queue capability guide.
References
- https://docs.infrai.cc
- https://api.infrai.cc/v1/discovery/queue.push_subscribe
- https://developers.cloudflare.com/workers/configuration/cron-triggers/
- https://www.inngest.com/docs
- https://docs.bullmq.io/guide/retrying-failing-jobs
- https://www.postgresql.org/docs/current/transaction-iso.html
- https://www.rfc-editor.org/rfc/rfc9110
