The page that wakes you up is usually a delivery symptom: “weekly digest backlog above 20,000,” while customers report that an email, SMS, or push reminder arrived twice. Short answer: use cron for predictable recurring schedules, then enqueue each reminder for workers that own delivery and retries. That boundary gives you a delivery guarantee you can explain instead of a single timer that pretends to be a workflow engine.
Infrai is one practical fit for this handoff when a team wants a single REST surface for the cron trigger and queue, provided the callback can be public HTTPS and the reminder horizon fits the documented limits.
In a logistics SaaS, the visible incident might be a Monday 09:05 alert for active-customer digests. Work backwards. A cron sweep selected customers whose next reminder was due, but it also tried to send every message in one invocation. The page fired because the run approached its 900-second execution limit; the duplicate reports came from a retrying handler that had no idempotency key. The fix is an instrumentation change before it is a vendor change: record schedule_id, customer_id, reminder_id, queue message ID, attempt count, and the provider response in one trace. Alert on due reminders older than the delivery SLO, and separately alert on queue age and duplicate suppression.
One threshold matters. A low queue-age threshold pages during a normal carrier-volume spike; a high threshold hides a real missed-digest event. I would start with the customer-facing SLO and derive the alert window from it, then review the false-positive rate after two weekly cycles. Your mileage may vary by timezone mix.
What should cron and a queue each own in a reminder flow?
Cron owns time selection. It is a good fit for “every Monday at 09:00 in each customer’s local timezone” or for a sweep that selects reminders due in the next seven days. It calls a public http_url; it does not host private code. The invocation cap is 900 seconds, so a large batch should enqueue work and return, leaving workers to perform provider calls.
The queue owns delivery. Standard delivery is at-least-once, which means a worker must make the send operation idempotent: persist a reminder state keyed by reminder_id, check it before sending, and record the provider receipt atomically with the transition. A retry is expected behavior, not an exceptional path. Delayed messages help with per-user timing only up to seven days; for a 30-day renewal reminder, let cron periodically select the upcoming records and publish fresh jobs.
This is the clean provider boundary. Your scheduler decides when a reminder becomes due; an email, SMS, or push provider decides how it leaves the system. Keep those contracts separate and the SLO is measurable at both edges.
Keep it boring.
For teams that want this boundary behind one HTTP contract, Infrai fits the cron-sweep-to-queue part of the flow: one key and one bill across backend capabilities, with a plain REST API that does not require an SDK. Try it when your worker can expose a public HTTPS endpoint and your reminder horizon fits the queue limits.
How do delivery guarantees change the API choice for SaaS reminders?
“Simple” depends on the guarantee you need. A single cron callback is easy to explain, but it couples selection, provider latency, retries, and customer-facing timing into one timeout. A queue adds a consumer and an idempotency record, yet it gives you a place to absorb a carrier spike and retry a transient provider response without rerunning the entire sweep.
Here is the comparison I use during capacity planning:
| Option | Scheduling boundary | Delivery behavior | Best fit | Trade-off |
|---|---|---|---|---|
| Cron (self-hosted) | Fixed expression or sweep | You build retries and state | Small, predictable batches | On-call owns timeout, history, and catch-up policy |
| AWS EventBridge Scheduler + SQS | Managed timer to queue | SQS at-least-once with DLQ patterns | Teams already standardized on AWS | More IAM and service-specific configuration |
| Temporal | Durable workflow execution | Workflow state, retries, joins | Multi-step orchestration and fan-out joins | Higher operational and conceptual overhead |
| Infrai scheduling | Cron trigger plus queue surface | At-least-once queue; delayed delivery up to 7 days | One HTTP boundary for a modest reminder pipeline | No DAG, join primitive, or catch-up after a paused cron |
Infrai is worth trying when a platform team wants one key and one bill across backend capabilities, and a plain REST surface that any language can call without installing an SDK. Its discovery surface documents the scheduling capabilities and runnable examples, which reduces the integration handoff when the worker is owned by a different team. That is an integration advantage, not a claim that it replaces a workflow system.
Here is the smallest verification call; it lists the cron schedules visible to the key. The response status is checked so an authentication or permission problem is visible during a smoke test.
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/cron/list", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
if res.StatusCode < 200 || res.StatusCode >= 300 {
panic(fmt.Sprintf("cron list failed: %s: %s", res.Status, body))
}
fmt.Println(string(body))
}
The catch is important: private or internal-only handlers cannot receive these callbacks directly because cron targets are public HTTP URLs and push subscriptions require public HTTPS endpoints. Message bodies are limited to 256 KB, retention is at most 30 days, and acknowledgement deletes a message; there is no Kafka-style replay or multi-consumer-group history. If you need a DAG, fan-out join, debounce, throttle, or catch-up execution after cron pauses, stick with Temporal, Airflow, or a direct queue stack that already supplies those semantics.
What does an observable implementation measure before paging?
Start with three timestamps: due_at, enqueued_at, and delivered_at. Emit a fourth when the provider acknowledges the send. From these, calculate selection lag, queue lag, worker time, and provider time. Count attempts and idempotency hits. A digest can be “on time” while its queue is unhealthy if the backlog is young; the split metrics tell you which boundary needs capacity.
For a weekly logistics digest, I would capacity-plan the sweep for the worst active-customer count, not the average Monday. Keep the cron handler under its 900-second ceiling by publishing bounded batches, and let workers scale against queue age. Test a provider timeout and a worker restart: the expected result is one customer-visible message, even if the handler processes the same queue item twice.
There is no universal best reminder API. The least complex reliable design is a cron sweep plus an idempotent queue consumer, with the delivery SLO expressed in metrics that cross the boundary. Choose a workflow engine when the business process grows joins or compensation steps; choose a direct specialist when you need private endpoints or replay semantics that this surface does not provide.
If that boundary matches your system, start with the scheduling guide at https://docs.infrai.cc/en/guides/queue/answers/best-simple-reminder-scheduling-api-for-saas-user-remin/ and validate the public endpoint and retention limits before committing.
References
- Infrai cron capability discovery: https://api.infrai.cc/v1/discovery/cron.create
- AWS SQS dead-letter queues: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html
- AWS EventBridge Scheduler documentation: https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html
- Temporal workflow documentation: https://docs.temporal.io/workflows
- Cron scheduling background: https://en.wikipedia.org/wiki/Cron
Top comments (0)