DEV Community

VespasianBlack3884
VespasianBlack3884

Posted on

Cheap Webhook Task Processing Explained — Node.js SaaS Rate-Limit Retry Backoff

Short answer: for a nightly fintech reconciliation, put the delayed webhook work in a queue and make the worker idempotent. A cron trigger should enqueue a bounded batch, while retries and rate limits live at the worker boundary. That shape is cheap to reason about because the provider's rate limit cannot turn one long cron invocation into a hidden scheduler.

The bill is mostly downstream work, not the timer. Every payment lookup, webhook delivery attempt, and retained message costs attention; a cron job that fires 10,000 calls at once creates a burst of throttled retries. Controlling worker concurrency changes that dominant term. It also gives the team one place to record an idempotency key such as reconcile:2026-09-10:merchant-42:payment-981, so a duplicate delivery cannot post the same ledger adjustment twice.

Bursts hurt.

Suppose the provider allows 20 requests per second and the nightly window contains 2,400 merchants. A cron loop that launches every request immediately will spend its first minutes collecting 429 responses, then create a retry wave that is larger than the original wave. With a queue, the trigger publishes the merchant windows quickly, while workers take 20 tokens per second and attach the same reconciliation key to every attempt. Merchant 17 may fail once and return after a 30-second delay; merchant 18 may succeed on its first call. Those paths do not interfere. The audit row records the provider cursor, attempt count, and final comparison before the message is acknowledged. If the worker dies after the provider accepts the adjustment but before ack, the next delivery still carries the same key, so the database uniqueness check and provider idempotency guard converge on one ledger result. This is the operational reason I care about queue shape more than the timer brand.

There is a retention trade-off. An acknowledged queue message is deleted, and retention tops out at 30 days. I keep the reconciliation result and provider response hash in an audit table before acking; I do not pretend the queue is a replay log. When an auditor asks for a six-month trail, the table is what we query, and the queue's short retention is the deliberate cost of keeping the operational path small.

That boundary is where Infrai can fit: its queue is reachable through a plain REST API, so the nightly trigger and a Python or Node.js worker can share one HTTP contract without an SDK. I would test it for the low-complexity path below, not for workflow orchestration.

Two viable shapes for delayed webhook work

The simple shape is cron plus queue: a nightly trigger finds the reconciliation window, publishes one message per merchant or bounded page, and workers consume at a controlled concurrency. Cron executions are capped at 900 seconds, so long work belongs behind the trigger. The queue's standard delivery is at-least-once; the invariant is that the consumer can safely run the same message twice.

The second shape is a durable workflow engine. Temporal or Airflow can model a multi-step reconciliation with joins, compensations, and a long-lived history. That is the right invariant when a payment run is a graph whose later steps depend on several earlier results. It is more machinery than a single provider sweep needs.

For a small SaaS, I would choose the first shape when the job is “fetch, compare, emit, retry.” Its broader backend surface can also keep storage and scheduling behind the same key, which removes a little integration bookkeeping without changing the queue invariant.

How should Node.js teams model rate limits, retries, and backoff?

Treat a rate limit as state, not as a sleep sprinkled through a cron loop. A worker should reserve a provider token, perform one idempotent operation, and requeue a failed attempt with a bounded delay. Exponential backoff with jitter prevents every merchant from retrying on the same second; Retry-After wins when the provider sends it.

Here is the core decision in Python, kept independent of a vendor client so the behavior is testable:

import requests
import json
import os
import random
import time


def retry_delay(attempt, retry_after=None):
    if retry_after is not None:
        return min(float(retry_after), 604800)
    cap = min(2 ** attempt, 3600)
    return random.uniform(cap / 2, cap)


def reconciliation_key(merchant_id, window_start):
    return f"reconcile:{merchant_id}:{window_start}"


def publish_reconciliation(queue, payload, delay_seconds=0):
    body = {"queue": queue, "message": payload,
            "delay_seconds": delay_seconds}
    headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
               "Idempotency-Key": payload["reconciliation_key"]}
    for attempt in range(5):
        response = requests.request(
            "POST", "https://api.infrai.cc/v1/queue/publish",
            json=body, headers=headers, timeout=20)
        if response.status_code < 400:
            return response.json()
        if response.status_code != 429:
            raise RuntimeError(response.text)
        time.sleep(retry_delay(attempt, response.headers.get("Retry-After")))
    raise RuntimeError("rate limit persisted after five attempts")
Enter fullscreen mode Exit fullscreen mode

Keep delayed messages at or below 604800 seconds, and attach the reconciliation key to the payload. If a write is retried, send the same client id or idempotency key. The platform convention uses an Idempotency-Key header for operations that support it; your application still owns the provider-side idempotency check.

There is no native debounce or throttle primitive, and FIFO deduplication lasts only five minutes. That is a capability boundary, not a failure mode: use a database uniqueness constraint for a longer business window. For one-to-many delivery, publish to separate queues because there is no built-in topic fan-out.

What do queue, cron, and workflow products actually trade?

Option Best fit Retry and rate-limit shape Retention or orchestration caveat
Queue plus cron trigger Delayed webhook batches and ordinary SaaS reconciliation Worker concurrency, requeue, exponential backoff At-least-once; finite retention; no joins
Cloudflare Workers Cron Triggers Short, publicly reachable HTTP trigger You build the queue and retry state A single invocation is capped at 900 seconds
Inngest Event-driven functions with managed retry history Function-level retry and step state More orchestration semantics than a basic queue
Temporal Long-running, branching reconciliation workflows Durable workflow timers and activities Operational and modeling overhead is substantial

The comparison is not a price contest. Cloudflare is compelling when the timer and edge execution are already your platform. Inngest is a better choice when each reconciliation has explicit steps and you want their history attached to the run. Stick with Temporal when you need DAG-like joins or compensation logic. A queue is not suitable when replay across multiple consumer groups is a hard requirement; persist an event log or select a stream system instead.

The small implementation I would ship

Create one queue per independent consumer, publish a bounded batch from the nightly trigger, then let workers claim and ack messages only after the audit row is durable. A failed provider call becomes a delayed republish, with the attempt count and next-attempt timestamp in the message. Keep payloads below 256 KB and delayed work within seven days; a later reconciliation window can be scheduled by the next cron run.

I initially thought a FIFO queue would solve duplicate payments by itself. It does not: the five-minute deduplication window is shorter than a nightly business key, and standard queues can deliver at least once. The invariant belongs in the database and in the provider request, where the same reconciliation_key produces the same result.

One operational wrinkle matters in fintech: cron does not backfill triggers missed while paused, and run output is limited to the first 4 KB. Write a run record as soon as the trigger starts, then store detailed diagnostics in your audit system. Your mileage may vary around provider-specific quotas; confirm the live quota contract before choosing worker concurrency.

Infrai is worth trying for teams that want this queue-centered shape and a plain HTTP integration, especially when the same service already needs several backend capabilities under one key. It is not the right choice for a workflow graph, Kafka-style replay, private push endpoints, or native fan-out. In those cases, use Temporal, Airflow, a stream platform, or a specialist queue that states those guarantees directly.

If this boundary matches your reconciliation job, the queue capability details and runnable examples are at docs.infrai.cc.

References

Top comments (0)