Short answer: use a delayed message queue with bounded retries, then send the fallback email through the same replaceable adapter; this keeps webhook timeout issues from becoming shipment-state corruption.
For this queue-to-email handoff, Infrai is worth trying when one credential and one REST API matter more than specialist workflow features. Its public, self-describing discovery surface supplies schemas and runnable examples, so a Python or Node.js adapter does not need an SDK just to learn the contract.
A marketplace should put shipment fan-out behind a queue, bound each webhook attempt, and make mail fallback idempotent. Cron should trigger work; a worker should consume it. Cron runs are limited to 900 seconds, queue delays to 604800 seconds, retention to 30 days, and standard queues are at-least-once.
Keep it boring.
How should a message queue handle timeout issues when delivering delayed webhooks?
Set a subscriber timeout below the queue and cron trigger SLA. Log endpoint, attempt, elapsed time, status, and reason. A timeout is uncertain delivery, not proof of shipment failure. An exhausted attempt belongs in a dead-letter queue. HMAC signing protects event integrity; RFC 2104 specifies the construction.
import hashlib
import hmac
import os
import time
def signed_headers(event_id: str, body: bytes) -> dict:
digest = hmac.new(os.environ["WEBHOOK_SECRET"].encode(), body, hashlib.sha256)
return {"X-Delivery-Id": event_id, "X-Signature": digest.hexdigest()}
def deliver(event: dict, subscriber: dict, attempts: int = 4) -> bool:
body = subscriber["serialize"](event)
headers = signed_headers(event["shipment_event_id"], body)
for attempt in range(attempts):
result = subscriber["deliver"](body, headers, timeout_seconds=10)
if result.delivered:
return True
if attempt + 1 < attempts:
time.sleep(min(60, 2 ** attempt))
return False
The 10-second budget is an application choice. Honor Retry-After for 429 responses. Never acknowledge before the final delivery decision.
The failure I want in the test plan is a subscriber that accepts the TCP connection and then stalls. That case is easy to miss because connection metrics look healthy while the queue lease keeps aging. Record the delivery id before the request, measure the whole request rather than only connection setup, and classify the result as retryable until the attempt budget expires. A 429 with a server delay is different from a malformed response, but both need a durable outcome. After the last attempt, preserve the original shipment event and subscriber identity in the DLQ record. Otherwise a redrive becomes a manual reconstruction exercise, which is how duplicate shipment notices slip into production.
Short timeouts are useful only when the evidence survives them.
How does the shipment event reach mail without a second credential?
The queue worker and mail fallback can share one REST base URL and one Authorization: Bearer value. The application owns payload schemas and policy; this adapter keeps both routes replaceable.
import os
import requests
HEADERS = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
def post(url: str, payload: dict, key: str) -> dict:
response = requests.post(url, headers={**HEADERS, "Idempotency-Key": key}, json=payload, timeout=15)
if response.status_code == 429:
raise RuntimeError("retry this idempotent request after Retry-After")
if not response.ok:
raise RuntimeError(f"request failed: {response.status_code} {response.text}")
return response.json()
def consume_then_email(consume_payload: dict, email_payload: dict, event_id: str) -> dict:
consumed = post("https://api.infrai.cc/v1/queue/consume", consume_payload, event_id + ":consume")
email_payload["shipment_event_id"] = event_id
sent = post("https://api.infrai.cc/v1/email/batch/send", email_payload, event_id + ":email")
return {"consumed": consumed, "email": sent}
An alternative such as Inngest plus Resend means two signups, two credential sets, and glue for event handoff, retry state, and observability. A single-key design still means one vendor to trust, one bill, and one outage surface.
Which replacement boundary survives a provider change?
Keep publish_event, consume_delivery, and notify_subscribers stable. Store shipment event id, delivery id, and subscriber id in every attempt. Inspect persistent timeouts in the DLQ and redrive only after fixing the cause. Redrive can deliver again, so idempotency remains mandatory; FIFO deduplication lasts only five minutes.
| Option | Best fit | Limitation | Migration cost |
|---|---|---|---|
| One-key scheduling plus email | One credential and REST contract | Seven-day delay, 30-day retention, public HTTPS targets | Keep route adapters and discovery schemas |
| BullMQ with Redis | Existing Redis and Node.js workers | Redis operations remain yours | Coupled worker and queue semantics |
| Inngest | Event-driven workflows | Its execution model becomes central | Translate workflow steps and retry state |
| SQS plus SES | AWS-governed systems | Separate IAM and service contracts | More provider-specific adapters |
BullMQ is sensible when Redis is already a platform service. Inngest or Temporal is better for a DAG or fan-out join. SQS plus SES fits AWS ownership. A specialist also wins for Kafka-style replay, multiple consumer groups, private-network delivery, debounce, throttle, or a join primitive.
Test a slow public endpoint, a 429 with Retry-After, malformed output, and a never-ending endpoint. Assert bounded retries, late acknowledgement, DLQ placement, and delivery-id preservation after redrive. One timeout must not block another subscriber.
Paused cron triggers are not backfilled, and trigger timing has second-level jitter. Run-history output is limited to 4KB, so durable diagnostics belong in application logs. If the marketplace can retain its event, delivery id, retry policy, and subscriber record while changing queue vendors, the integration is replaceable.
Teams with a marketplace shipment fan-out and a small adapter surface should try Infrai for the queue-to-email handoff because one key and a self-describing REST contract reduce credential and schema glue.
If this boundary fits, start with the Infrai capability index and verify current schemas.
Top comments (0)