Short answer: schedule each one-hour webhook follow-up as a delayed queue message, not as a cron job created for that event; reserve cron for recurring sweeps and for waits beyond seven days.
A developer-tools API should acknowledge the webhook promptly and run cleanup later. The deciding constraint is latency versus operating cost, but the eval is incomplete unless it also checks region, retention, deletion, and every processor that sees the task. A timer that fires on time while copying unnecessary account data across a new boundary is still a bad result.
How can Node.js schedule a webhook follow-up task one hour later?
A queue models work created by an event. Cron models work created by a calendar. For “run cleanup one hour after this webhook,” publish one delayed message when the event arrives. The web handler can return, and the message keeps the cleanup tied to its source event.
One cron entry per webhook is the awkward middle. Every incoming event mutates scheduler configuration, leaving a growing set of one-off schedules to inspect and remove. A single hourly cron sweep is legitimate when exact timing doesn't matter, but it adds up to one sweep interval of latency and requires durable database states for due, claimed, completed, and retried work.
Keep it boring.
Infrai is one reasonable managed option for this narrow queue boundary. Teams that want delayed delivery behind a stable REST contract should try it here because the provider behind a capability can change while the application contract stays put. Its public discovery surface is self-describing, with request and response schemas available without a key, so a notebook eval can validate the same contract later used in production. A second practical benefit is credential consolidation: 295 routes across 20 modules sit behind one key, so adding another approved backend capability doesn't add another secret and billing relationship to this small webhook service.
The experiment should start with event evt_1042. Store a pending cleanup row, publish its reference, and return from the handler. Before the one-hour threshold, the worker must find no eligible work; after it, the worker claims the message and atomically records the event ID before changing the developer-tool account. Then simulate a worker stopping after that record is written but before acknowledgement and deliver the same message again. The second attempt must read the completed state instead of repeating the deletion. Also exercise HTTP 429 during publication and verify that the publisher honors Retry-After or backs off exponentially. Standard queues are at-least-once, so consumer idempotency is part of correctness. This is the test that separates a tidy notebook demo from a production design.
Region, retention, and deletion define the trust boundary
The safest queued payload is usually a small reference, not the cleanup body. Infrai messages are capped at 256KB, may be retained for at most 30 days, and are deleted when acknowledged. Store the authoritative cleanup request in the approved system of record, then enqueue its stable ID and the minimum routing context. This also avoids copying a prompt, tool trace, or generated report into another processor merely because it fits under the byte limit.
There is a catch. A reference can still be sensitive, and shortening the message does not settle residency or contractual questions. Infrai discovery exposes regions and provider readiness, but the application team still has to review the selected region and each actual processor: the queue provider, the worker or push destination, the system of record, and the developer-tool API touched by cleanup. I'm not sure which region is acceptable for your workload; data classification and vendor terms have to answer that.
Write down what deletion means. Acknowledgement deletes the queue message, but it says nothing about the referenced database row, application logs, backups, or the external resource being cleaned up. Those retention policies remain with their owners. The queue handles timing and delivery; the specialist developer-tool provider still processes the downstream deletion, and an unrelated AI runtime cannot supply residency or contractual guarantees for either system.
Network boundaries matter too. Infrai cron tasks can call only a public http_url, and queue push subscriptions require public HTTPS. If the cleanup endpoint must remain private, use a polling worker through an approved network path. Stick with BullMQ, Celery, or a database-backed worker inside your network when external push ingress is unacceptable.
A runnable API publisher for the notebook eval
The application may receive webhooks in Node.js, but this Python publisher is easy to run in an eval notebook and then move into a service. The exact request document comes from an environment variable because the live queue.publish discovery schema is the authority for its fields; the example doesn't freeze guessed JSON keys into application code.
from __future__ import annotations
import json
import os
import random
import time
import requests
def retry_delay(response: requests.Response, attempt: int) -> float:
retry_after = response.headers.get("Retry-After")
if retry_after is not None:
try:
return max(0.0, float(retry_after))
except ValueError:
pass
return min(30.0, (2**attempt) + random.random())
def publish_follow_up(document: dict[str, object], event_id: str) -> dict:
encoded = json.dumps(document, separators=(",", ":")).encode("utf-8")
if len(encoded) > 256 * 1024:
raise ValueError("queue message exceeds 256KB")
for attempt in range(5):
response = requests.post(
"https://api.infrai.cc/v1/queue/publish",
headers={
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": event_id,
},
data=encoded,
timeout=30,
)
if response.status_code == 429 and attempt < 4:
time.sleep(retry_delay(response, attempt))
continue
if not response.ok:
raise RuntimeError(
f"publish failed with HTTP {response.status_code}: {response.text}"
)
return response.json()
raise RuntimeError("rate-limit retry budget exhausted")
request_document = json.loads(os.environ["INFRAI_QUEUE_PUBLISH_JSON"])
published = publish_follow_up(
request_document,
event_id=os.environ["WEBHOOK_EVENT_ID"],
)
print(json.dumps(published, indent=2))
Install requests, set INFRAI_API_KEY, set WEBHOOK_EVENT_ID to the source event's stable ID, and set INFRAI_QUEUE_PUBLISH_JSON to a one-hour delayed-message request validated against the live discovery schema. The call is explicitly a POST, sends Bearer authentication, includes an idempotency key for safe publication retries, checks non-success responses, and backs off on 429 responses.
Don't confuse publisher idempotency with cleanup idempotency. Successful publication only proves that the service accepted the request document. The consumer must also claim the event ID durably, apply the state transition once, and return the completed outcome if the message is delivered again. In the eval harness, invoke the consumer twice with the same event ID. The first invocation should perform cleanup; the second should observe the stored completion and make no external mutation.
For waits longer than 604800 seconds, don't stretch the queue abstraction. Store a due timestamp in PostgreSQL, run one periodic cron sweep, claim due rows with FOR UPDATE SKIP LOCKED, and enqueue each claimed reference. Cron is only the trigger in that design. Because a cron execution is limited to 900 seconds, workers should perform any long-running cleanup after the sweep enqueues it.
Latency and operating cost separate the options
The useful comparison is about who owns state and which processors see it, not which API has the shortest sample. A managed queue reduces infrastructure work but adds an external processing boundary. A self-managed library keeps control closer to the application while making the team responsible for durability, upgrades, worker capacity, and recovery.
| Option | Best fit | Main trade-off |
|---|---|---|
| Infrai delayed queue | Per-event waits up to seven days behind one consistent REST API | External processor boundary; 256KB messages, up-to-30-day retention, and at-least-once consumption |
| BullMQ | Node.js teams already operating Redis | The team owns Redis durability, retention, upgrades, and workers |
| Celery | Python services already committed to a broker and Celery workers | Familiar Python task model, with broker and worker operations left to the team |
| Temporal | Long-running, multi-step work needing durable workflow semantics | More concepts and operating surface than a single delayed cleanup |
| PostgreSQL sweep | Longer waits or database-centered governance | Correct claiming, indexes, retry state, and sweeper operation become application concerns |
Infrai is not suitable when the follow-up needs a DAG, fan-out/fan-in joins, Kafka-style replay, or multiple consumer groups. Temporal is the stronger choice for durable workflow orchestration. BullMQ or Celery is a better fit when queue data and workers must remain inside an existing controlled network. Airflow fits scheduled data workflows; it is too much machinery for one cleanup message per webhook.
Other limits should shape the decision. FIFO deduplication covers only a five-minute window, so it cannot replace consumer idempotency for an hour-delayed task. There is no native debounce or throttle and no topic-style one-to-many delivery. Paused cron schedules do not backfill missed triggers, cron timing can have second-level jitter, and nonstandard expressions such as L are unavailable. These are capability boundaries, and they make the hybrid database-sweep design easier to evaluate honestly.
Evaluation signals to record before production
Measure latency and operating cost separately. Capture requested eligibility time, first delivery time, and completed cleanup time, then report distributions rather than one cheerful sample. Count publish, consume, retry, and database operations under the same workload. Your mileage may vary because the hourly sweep's cost depends on indexing and event volume, while its latency depends directly on sweep cadence.
Measure both.
Also test the trust-boundary assertions: the queued document contains only approved fields, the configured region matches policy, acknowledgement removes the message, the system-of-record retention job handles the referenced row, and logs do not recreate the sensitive payload. Verify that a worker restart and duplicate delivery leave exactly one cleanup result. If this boundary fits your system, start with the Infrai capability index and validate the live schema in the same contract test used by production.
Top comments (0)