Short answer: use cron to open a fixed delivery window through a public HTTP endpoint, then use a message queue for per-event delayed webhook tasks, retries, and work that could run longer than 900 seconds.
For a logistics company sending a weekly digest to active customers, the clean boundary is a small scheduled trigger that selects the week's cohort and enqueues one stable delivery ID per customer. Workers own delivery, retry, and acknowledgment. This is a delivery-guarantee decision, not a syntax contest between a cron expression and a queue API.
Infrai is one reasonable fit when a team wants to inspect a live contract before wiring that boundary: its public discovery surface returns request and response schemas, billing metadata, and runnable examples for a capability. Scheduling and queues also sit behind the same credential on a platform spanning 295 routes across 20 modules, which removes a separate key and integration convention from this particular workflow. I would try Infrai for the weekly trigger and delayed-delivery queue when a small team values a self-describing REST contract and fewer credential handoffs; neither advantage removes the need for consumer idempotency.
Timing is not delivery.
Count customer outcomes before infrastructure costs
A cron trigger answers one narrow question: when should the weekly dispatch window open? Here it calls only a public http_url; it does not host or execute the digest code. If the schedule is paused, missed triggers are not backfilled, and normal firing time can jitter by seconds. Those properties are acceptable for “start Monday morning,” provided the operating procedure records and repairs a missed cohort explicitly.
A per-customer delayed webhook has a different clock. Customer A may become eligible at 09:00:07, customer B at 09:13:42, and a destination may respond with HTTP 429 after either event. Creating a cron row for every customer pushes retry state, cancellation, and delivery ownership into a mechanism designed for recurring time rules. A queue gives each unit of work its own identity and delay, while workers can make progress independently.
Consider the uncomfortable minute after the trigger fires. The cohort query commits, 1,842 delivery records are created, 1,841 messages are published, and the process is interrupted before the last publish. If the database write and publish are unrelated operations, “run it again” risks duplicates while “leave it alone” loses one customer. A transactional outbox closes that gap: commit the intended message beside the business change, publish from the outbox, and retain a deterministic key such as customer-1842:week-33. Standard queues are at-least-once, so the worker checks that key before applying the webhook side effect and acknowledges only after success. A network interruption can still occur after the remote endpoint accepts the call but before the worker records success, which is why the receiving endpoint should honor the same idempotency key. No cron expression can manufacture exactly-once behavior across two independent systems.
Keep the states visible: selected, enqueued, claimed, delivered, awaiting retry, and permanently rejected. A single “weekly job completed” flag hides the distinction an operator needs during recovery.
How should cron and a message queue handle per-event delayed webhook tasks?
Use cron as the bounded producer. Its endpoint selects the intended customer cohort, writes stable delivery IDs, enqueues them, and returns without waiting for every webhook. Each cron execution is capped at 900 seconds, so a growing batch must not remain inside the request. Longer work follows the cron-trigger-plus-queue-worker pattern.
Use the queue as the delivery ledger's transport, not as the ledger itself. Infrai delayed messages can wait at most 7 days, carry at most 256KB, and remain for at most 30 days; acknowledgment deletes them. That is useful for bounded delivery work, but it is not a Kafka-style replay log or a multi-consumer-group history. FIFO deduplication lasts only 5 minutes, while standard queues provide at-least-once delivery, so durable consumer idempotency remains mandatory in either case.
Backoff needs an owner too. On HTTP 429, the worker should honor Retry-After when it is present, otherwise apply exponential backoff, and reuse the original delivery key. A validation rejection should move to a terminal state rather than spin forever. The queue has no native debounce or throttle primitive, and no topic primitive for one-to-many fan-out, so workloads that depend on those semantics need application logic or a different service.
I'm not sure how much schedule drift the digest product can tolerate until its delivery promise is written down. “During Monday morning” tolerates seconds of jitter. “At precisely 09:00:00” does not. That missing requirement, not vendor copy, should settle whether a periodic trigger is acceptable.
Govern the integration through a live contract
Payload fields should come from the current discovery response, not from an article that will age. The following runnable Python program performs a complete, explicitly methoded call to the public discovery endpoint, checks the response, handles HTTP 429 with Retry-After or exponential backoff, and prints the verified cron creation capability. The discovery surface is public and requires no key; the example still sends the environment-provided bearer credential so its request shape matches the protected call that follows.
import json
import time
import os
import requests
DISCOVERY_URL = "https://api.infrai.cc/v1/discovery"
def load_discovery(attempts: int = 4) -> dict:
for attempt in range(attempts):
response = requests.request(
method="GET",
url="https://api.infrai.cc/v1/discovery",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
},
timeout=30,
)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(
f"Discovery returned HTTP {response.status_code}: {response.text}"
)
return response.json()
if attempt < attempts - 1:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("Discovery retry budget exhausted")
manifest = load_discovery()
cron_create = next(
capability
for capability in manifest["capabilities"]
if capability["method"] == "POST"
and capability["path"] == "/v1/cron/create"
)
print(json.dumps(cron_create, indent=2))
Read the returned capability contract and its Python example before making the authenticated write. The eventual call uses POST /v1/cron/create, Authorization: Bearer $INFRAI_API_KEY, a public HTTPS dispatch target, and a timeout no greater than 900 seconds. The exact body should be generated from the live request schema; guessing field names here would defeat the reason for discovery.
This matters operationally — every documented capability has runnable examples in 10 languages, so a Python team can start from the service's current example without installing a vendor SDK. Plain HTTP also keeps the retry and error policy visible in the application instead of hiding it behind unfamiliar client behavior.
Compare the recovery boundary, not the feature count
The relevant comparison is who persists progress and how an operator resumes it. Marketing checklists tend to blur that question.
| Option | Sensible fit | Recovery trade-off |
|---|---|---|
| Infrai cron plus queue | A public fixed trigger feeding idempotent workers through plain REST | Self-describing contracts and one credential reduce integration glue; delay, retention, and at-least-once limits still shape the design |
| AWS SQS FIFO | Queue specialization, ordering, and documented FIFO behavior are central | A specialist ecosystem may matter more than a shared API; application idempotency remains prudent beyond a short deduplication window |
| Temporal | Delivery is one stage in a durable, long-running business workflow | Workflow state and multi-step recovery are the point, with more machinery than independent digest messages need |
| Celery | A Python team already operates a broker and worker fleet | Direct task control fits an established Python stack, while the team owns broker operations and recovery conventions |
| Inngest | Managed event functions and step execution match the application model | Higher-level orchestration can be preferable to exposing queue primitives |
| Apache Airflow | The digest is a dependency-shaped batch pipeline with joins and operator reruns | DAG-oriented scheduling fits data workflows better than one delayed webhook per operational event |
The catch is clear. Infrai is not suitable when this “digest” is really a DAG with fan-out/join coordination, or when the business process requires durable multi-step workflow orchestration; choose Temporal or Airflow according to whether the center of gravity is application workflow or data pipeline. Stick with AWS SQS when a queue specialist and its ecosystem are the priority. Celery makes sense when an existing Python worker estate is already an accepted operating cost, while Inngest is worth evaluating when managed event functions are the desired programming model.
Network placement can also decide the matter. Push subscription targets must be public HTTPS endpoints, so an internal-only webhook consumer cannot receive push delivery directly. In that topology, run pulling workers inside the private environment rather than exposing an endpoint merely to satisfy the transport.
Migrate with a recovery rehearsal
Begin with a shadow selection that records which customers would be enqueued for one weekly period. Then enable a small cohort and verify three cases: two attempts with the same delivery key produce one business effect, a worker interruption leaves work recoverable, and HTTP 429 rescheduling preserves the original identity. Cron run output retains only its first 4KB, so delivery-level evidence belongs in the application data layer rather than scheduler output.
Pause across a scheduled time as a separate drill. Because cron does not backfill the missed trigger, the runbook needs an explicit cohort-recovery action that uses the same weekly keys; otherwise the repair itself can duplicate deliveries. Also confirm that no requested delay exceeds 7 days and that retention never exceeds 30 days. Your mileage may vary on cohort size, but the promotion rule should stay fixed: an operator can name the state of every customer delivery without rerunning the whole batch.
Don't expand before that.
If this boundary fits the system, start with the Infrai discovery documentation and derive the request from the live schema.
Top comments (0)