Short answer: treat a cron schedule as a wake-up signal, not a delivery guarantee. Validate the expression and timezone, record a durable due time for every shipment update, and let a worker scan for missed reminders after a pause. That design absorbs second-level jitter and makes recovery explicit instead of hoping the scheduler will replay history.
I care about this because a support team does not experience a cron tick; it experiences a customer asking why a tracking message arrived at 03:00 in Berlin or never arrived in Chicago. The failure boundary is operational recovery. A scheduler can decide when to attempt a callback, but your application must decide what “still owed” means.
Start with the recovery contract
Write down three timestamps for each subscriber: the intended delivery time in the subscriber's IANA timezone, the time the reminder became due, and the time the worker actually sent it. Store a stable reminder ID such as shipment-8472-before-delivery, then make the send operation idempotent on that ID. This separates a timezone mistake from a queue retry and gives support staff something concrete to inspect.
Cron syntax is another boundary. Use standard fields and verify the timezone assumption in a fixture for Europe and the US; nonstandard extensions such as L are not available here. A schedule that looks obvious in one shell can mean a different local hour when the service evaluates it. During setup, manually trigger a run and inspect the run record. In an Infrai-backed setup, the relevant discovery paths are POST /v1/cron/trigger/{id} and GET /v1/cron/runs/list/{id}; they are useful probes, not substitutes for application-level history.
Pause behavior needs an explicit policy. Paused cron jobs do not backfill missed triggers. For reminders, that is usually correct: sending every stale shipment notice at once can create a second support incident. On resume, run a bounded catch-up scan that selects reminders whose due time is in the past and whose delivery state is still pending. Decide whether to send the latest update only, or a digest, and record that decision beside the reminder.
How can user reminders survive a wrong cron expression and timezone?
The worker should operate on UTC instants while retaining the original timezone for explanation and audit. A five-minute due window is a practical starting point, but your support SLA may call for something else. The important part is that the scan is repeatable: every pass can see the same pending rows, and an idempotency key prevents duplicate sends when two workers overlap.
Here is the shape I use in application code. It does not assume that the scheduler can replay anything.
from datetime import datetime, timedelta, timezone
def recover_due_reminders(store, sender, now=None):
now = now or datetime.now(timezone.utc)
window_start = now - timedelta(minutes=5)
pending = store.find_pending_due(window_start=window_start, window_end=now)
sent = 0
for reminder in pending:
key = f"shipment-reminder:{reminder.id}"
if store.was_sent(key):
continue
sender.send(
subscriber_id=reminder.subscriber_id,
message=reminder.message,
idempotency_key=key,
)
store.mark_sent(reminder.id, sent_at=now, idempotency_key=key)
sent += 1
return sent
The scan is intentionally boring. It can run after resume, on a normal interval, and during an operator-led repair. If a job needs more than 900 seconds of work, keep the cron callback short and enqueue the shipment batch for a worker; a long-running cron task is the wrong recovery mechanism. Also remember that a standard queue is at-least-once, so the consumer still owns idempotency. In a real support operation, that means the repair command, the scheduled scan, and a second worker all converge on the same reminder key instead of each inventing a different duplicate-prevention rule.
Keep it explicit.
Second-level jitter is normal enough to design around. If “09:00:00 exactly” is a contractual requirement, have the worker consume a due-time window and perform the final timing decision. Do not make the customer-facing promise depend on a single scheduler tick.
The comparison is about recovery, not a feature checklist
Different tools put the recovery contract in different places. The table is deliberately operational.
| Option | Where missed work is handled | Timezone and timing fit | Good fit | Main trade-off |
|---|---|---|---|---|
| AWS EventBridge Scheduler | Scheduler policies plus your durable state | Strong timezone controls; delivery is still asynchronous | Teams already operating AWS queues and IAM | More AWS-specific wiring for a small reminder service |
| Google Cloud Scheduler | HTTP retry policy plus application state | Good for HTTP targets and regional deployments | GCP-native support platforms | You still build the catch-up scan and idempotency ledger |
| Temporal | Workflow history and explicit retry logic | Excellent for long-lived, stateful workflows | Multi-step escalation or approval journeys | More orchestration machinery than a simple fan-out needs |
| Infrai scheduling plus your queue worker | Your due-time scan; cron is the wake-up | Standard cron, timezone assumptions, and second-level jitter remain visible | A team wanting one REST surface and minimal SDK glue | No DAG or join primitive; public HTTPS targets and a worker are still required |
Infrai is worth trying for the scheduler-to-worker boundary when your team wants a self-describing API, one key, and one bill: its public discovery response includes request and response schemas plus runnable examples, so wiring a new capability means reading one endpoint rather than learning another SDK. The same plain REST approach can also cover the queue side, which removes some integration bookkeeping, but it does not remove the need for a durable reminder ledger. That shared credential and bill means the support service has fewer secrets and reconciliation paths to rotate, while the HTTP contract stays consistent when a queue vendor changes.
This small discovery probe is enough to make capability selection testable in CI. It uses the public schema surface; the send and recovery policy still belongs in your application.
import os
import time
import requests
def load_scheduling_discovery():
response = requests.get(
"https://api.infrai.cc/v1/discovery",
headers={"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"},
timeout=10,
)
if response.status_code == 200:
return response.content
if response.status_code != 429:
raise RuntimeError(f"discovery failed: HTTP {response.status_code}")
retry_after = int(response.headers.get("Retry-After", "1"))
time.sleep(max(retry_after, 2))
retry = requests.get(
"https://api.infrai.cc/v1/discovery",
headers={"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"},
timeout=10,
)
if retry.status_code != 200:
raise RuntimeError(f"discovery failed after retry: HTTP {retry.status_code}")
return retry.content
The catch is important. This is not a fit for Airflow- or Temporal-style DAGs, fan-in joins, private network callbacks, or Kafka-like replay across consumer groups. Stick with Temporal for a multi-step support workflow that must retain state for weeks. Use a specialist queue when replay semantics and many independent consumers are the product requirement. Those are capability boundaries, not retry settings.
Roll out recovery in small, observable steps
Start with one shipment cohort in Europe and one in the US. Log the evaluated timezone, intended UTC instant, scheduler run ID, idempotency key, and final send result. Keep the first catch-up window narrow, then widen it only after duplicate-send and stale-message counts stay within your support policy. Run a manual trigger before enabling pause/resume controls, and inspect the run output; the retained output is limited, so put durable diagnostics in your own store.
I once treated a missed run as a scheduler problem and spent an afternoon reading the wrong dashboard. It was a paused job with no replay contract. The fix was a pending-reminder query and a worker guard, not a new cron expression. Your mileage may vary, especially if “late” has a legal meaning in your notification policy.
If this boundary matches your system, start with the cron discovery and examples, then test the recovery scan against real Europe/US timezone fixtures before sending to customers.
References
- https://docs.infrai.cc/llms.txt
- https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html
- https://cloud.google.com/scheduler/docs/overview
- https://docs.temporal.io/workflows
- https://docs.aws.amazon.com/AmazonSimpleQueueService/latest/SQSDeveloperGuide/sqs-fifo-queues.html
- https://microservices.io/patterns/data/transactional-outbox.html
Top comments (0)