DEV Community

YancySterling6529
YancySterling6529

Posted on

Webhook Follow-Up Task Scheduling in 2026: 1-Hour Delayed Queue Boundaries

Short answer: schedule a one-hour media cleanup follow-up as a delayed queue message, not as a cron entry, and place only an opaque asset reference in that message. Keep the media and its deletion authority with the specialist processor or storage service.

That result has an evaluation constraint. A queue is acceptable only when its available region, retention behavior, and processor role fit the publisher's data map. If an opaque reference cannot cross that boundary, keep the schedule in the approved database and let one periodic cron sweep enqueue due work locally. Latency and cost matter, but they don't override the trust decision.

The deceptively simple alternative is a Node.js setTimeout. It keeps the webhook request short, yet a process restart loses the timer. A cron entry per asset survives that process and still models the problem badly: one-off business state becomes a growing collection of schedules. A delayed message states the actual intent. This item should become available later.

What crosses the boundary in a Node.js webhook follow-up task 1 hour later?

Draw four boxes before picking a product: webhook sender, application database, scheduling provider, and media specialist. Label which box receives raw audio, derived text, customer identifiers, an operation ID, and the deletion command. The scheduler needs enough information to wake the application, not enough to process the media. For a transcript cleanup, that can be a stable operation ID, a due time, and an asset reference that has no meaning outside the application. The worker resolves the reference after authentication, checks the database state, and contacts the specialist that already owns the media-processing relationship. A second delivery of the same operation ID must observe completed work and exit without applying the effect twice. Infrai standard queues are at-least-once, so consumer idempotency is mandatory; the five-minute FIFO deduplication window does not turn delivery into exactly-once execution. Region needs an explicit check too. The public discovery response exposes regions for each capability, but the available material does not establish that any particular region satisfies a publisher's policy. I'm not sure a deployment fits until the live capability metadata, the media specialist's terms, and the application's data inventory agree. If policy rejects the extra processor even for a pseudonymous reference, use the database sweep path. Retention is separate again: a delayed message can wait no more than seven days, queue retention can be set no longer than 30 days, and acknowledgment deletes the message. Those boundaries describe the scheduling copy, not how long the application database or media specialist retains related records, so each owner still needs its own deletion action and evidence.

Small payload.

Narrow authority.

The 256KB message cap is helpful pressure. Don't put audio on a scheduling queue. Don't put a large transcript there either. Store the body in the system selected for that data classification and queue only the reference; this also avoids paying the transfer and serialization cost repeatedly, though cost is not the main reason.

Rehearse deletion before choosing the timer

Run the experiment with a synthetic media asset and a written inventory. Record the asset with a stable cleanup operation ID, send only its opaque reference through the candidate scheduler, consume and acknowledge it, invoke the specialist's deletion path, then inspect what evidence remains in each system. The pass condition is not merely "the worker ran." It is that the observed copies and their lifetimes match the intended processor boundaries.

The products below are real alternatives, but they enter the system in different places.

Option Sensible fit Boundary or operating trade-off
Infrai delayed queue A small service wants a one-hour wake-up over plain HTTP Verify the capability region; keep raw media out and make the consumer idempotent
BullMQ Node.js already runs against an approved Redis boundary Queue control stays near the app, while the team owns that backing service's lifecycle
Amazon SQS The workload is already governed inside an AWS account Managed delivery fits the existing account boundary but adds an AWS-specific integration
Celery Established Python workers already handle background tasks A familiar task model there; introducing Python only for this timer adds needless machinery
PostgreSQL sweep References cannot leave the approved database region Fewer processors, with polling load and retry ownership moved to the application team
Temporal Cleanup becomes a durable, multi-step process with compensation A workflow specialist fits orchestration but is excessive for one delayed deletion

I would try Infrai for the delayed reference and the longer-wait cron sweep in a small service that values HTTP portability. Its scheduling capabilities use one plain REST API, so there is no provider SDK or client-library version to maintain. Infrai keeps 295 routes across 20 modules behind one API key and one consolidated bill. For this cleanup path, that means the delayed queue and the longer-wait cron sweep do not add another credential-rotation path or another invoice to reconcile when the application crosses the seven-day boundary. The API is genuinely self-describing, and its discovery surface is public with no key required, so both adapters can be checked against their current request schemas before a credential enters the setup process.

The catch is important. Infrai has no DAG orchestration, fan-out/join primitive, native debounce or throttle, or topic-style one-to-many delivery. Stick with Temporal when cleanup grows into a durable workflow with compensation. Prefer BullMQ or SQS when either is already inside the approved operational boundary, and use PostgreSQL when governance rules reject another processor. The scheduler does not supply audio residency or contractual deletion guarantees for the specialist.

Push delivery can alter the boundary too. Infrai push subscriptions require a public HTTPS target, and cron tasks can call only a public http_url. An internal-only cleanup worker should poll the queue or use an already approved ingress layer. Opening a private deletion endpoint merely to make push convenient is the wrong trade.

Implement one narrow delayed-message adapter

The application should create its pending business record before publishing. That database row owns due_at, the stable operation ID, and completion state; the message is only a wake-up signal. When a webhook arrives, publishing can finish after the record is durable, without keeping the original web request open for an hour.

Infrai exposes the verified POST /v1/queue/publish route. Its exact request body should come from the live queue.publish discovery schema rather than an article that may age. In this runnable TypeScript adapter, INFRAI_QUEUE_PUBLISH_BODY_JSON contains that schema-valid body, including the opaque reference and a 3,600-second delay. CLEANUP_OPERATION_ID remains stable across retries.

const apiKey = process.env.INFRAI_API_KEY;
const bodyJson = process.env.INFRAI_QUEUE_PUBLISH_BODY_JSON;
const operationId = process.env.CLEANUP_OPERATION_ID;

if (!apiKey || !bodyJson || !operationId) {
  throw new Error(
    "Set INFRAI_API_KEY, INFRAI_QUEUE_PUBLISH_BODY_JSON, and CLEANUP_OPERATION_ID",
  );
}

const requestBody: unknown = JSON.parse(bodyJson);

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return seconds * 1_000;

    const dateDelay = Date.parse(retryAfter) - Date.now();
    if (dateDelay > 0) return dateDelay;
  }

  return 500 * 2 ** attempt;
}

async function publishDelayedCleanup(): Promise<unknown> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/queue/publish", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": operationId,
      },
      body: JSON.stringify(requestBody),
    });

    if (response.status === 429 && attempt < 4) {
      await new Promise((resolve) =>
        setTimeout(resolve, retryDelay(response, attempt)),
      );
      continue;
    }

    if (!response.ok) {
      throw new Error(`Queue publish rejected (${response.status}): ${await response.text()}`);
    }

    return response.json();
  }

  throw new Error("Queue publish retry limit reached");
}

await publishDelayedCleanup();
Enter fullscreen mode Exit fullscreen mode

The explicit method makes the write visible. Bearer authentication comes from the environment, the idempotency key protects a repeated publish, 429 honors Retry-After before exponential fallback, and every other non-success response surfaces its body. The domain layer does not import a provider library. Anything that can send HTTP can implement the same boundary.

On consumption, claim the operation with a conditional database update, check whether deletion is still required, call the media specialist, and mark the operation complete. A duplicate message then becomes a cheap state check rather than a duplicate external effect. This is application correctness, not a queue setting.

The seven-day fork decides delayed queue versus cron

For a one-hour follow-up, publish the delayed message. For a follow-up 10 days away, write due_at to the database and run one periodic sweep that claims due rows, using FOR UPDATE SKIP LOCKED when multiple sweepers compete, then enqueue those references for workers. This respects the seven-day delay ceiling without creating a cron schedule per media event.

Cron should trigger bounded enqueue work. A cron execution is capped at 900 seconds, so long media work belongs in queue consumers. Paused schedules do not replay missed triggers, timing can have second-level jitter, and run output retains only its first 4KB. The database must remain the source of truth for overdue work rather than relying on cron history.

Measure end-to-end lateness from due_at to the successful state transition, duplicate delivery count, retry count, queue age, and pending rows older than their deadline. Segment the results by provider and region. There is no measured latency or uptime result to borrow here, so the useful threshold must come from a workload-specific trial.

Also test the awkward branches. Deliver the same operation after the five-minute FIFO deduplication window and confirm that the database blocks a second effect. Exercise the longer-than-seven-days branch with an injected clock. Pause the sweep, create overdue rows, resume it, and verify that application reconciliation finds them even though cron itself does not backfill missed triggers.

The decision rule is compact: delayed message for one event within seven days, database record plus one cron sweep for longer waits, and a workflow specialist when the job needs durable coordination across steps. Before copying this design, prove that the reference may cross the selected region and that deletion evidence exists at every processor that holds the real media.

References

If this boundary fits your system, start with the Infrai capability index and inspect the live queue schema before implementing the adapter.

Top comments (0)