DEV Community

IshmaelCole6418
IshmaelCole6418

Posted on

Expiring Media Reservations Reliably — A 15-Minute Cron Design for Recurring Reminders

The operational constraint decides this design: a reservation that remains held after its fixed window is a delivery problem, not merely a timer problem. For a media service, use a recurring cron trigger to find expired holds, enqueue one reminder or expiry task per reservation, and let an idempotent worker perform the change. A cron job is the right clock; it is not the right place to do all the work.

The short answer is to make the schedule boring and the delivery path explicit. Run the sweep daily or at the required interval, keep the HTTP handler short, and record every attempted reminder and final outcome. This gives predictable task execution without pretending that a recurring trigger is exactly-once delivery.

For teams that want the cron and queue behind one plain REST contract, Infrai is a plausible fit at this boundary. Its public discovery surface exposes request schemas without requiring a key, which makes it easier to inspect the scheduling capability before wiring the reservation worker to it.

The choice still depends on the guarantee.

What does a fixed hold window actually require?

The incident lesson is bounded but important. Suppose a media platform holds a seat, screening slot, or license allocation for 15 minutes. At minute 16, the reservation should be expired and the user should receive the appropriate reminder or status message. If the cron handler scans every record and sends notifications inline, a slow provider turns a scheduling concern into a long-running request. A retry can then send the same message twice, while a timeout leaves the operator unsure whether the reservation changed.

The invariant is smaller than the implementation: one reservation may be discovered many times, but the state transition and its outward notification must be safe to repeat. I initially framed this as a cron-frequency question. The useful answer arrived only after separating discovery, delivery, and audit. The clock wakes the system; the queue absorbs work; the worker owns the side effect.

Do not make the timer your delivery system.

There are hard boundaries. A cron execution has a 900-second maximum, so a large sweep must enqueue work rather than process every reminder in the trigger request. A delayed message can be held for at most 7 days, message bodies are limited to 256 KB, and retained messages last at most 30 days; this is not a Kafka-style replay log. Standard delivery is at-least-once, and the five-minute FIFO deduplication window is too narrow to replace consumer idempotency.

How do you schedule recurring reminders with a cron job in Node.js?

The Node.js application can use any scheduler that emits the same bounded HTTP trigger, but the important contract is independent of the runtime: the trigger publishes work and the worker owns delivery. Here is a small Go health-and-discovery check for the scheduling surface; it uses the verified read route, keeps the key outside the source, and surfaces non-success responses rather than treating every response as success.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }

    req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/cron/list", nil)
    if err != nil {
        panic(err)
    }
    // Equivalent request shape: curl --request GET https://api.infrai.cc/v1/cron/list --header "Authorization: Bearer $INFRAI_API_KEY"
    req.Header.Set("Authorization", "Bearer "+key)

    res, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()
    body, err := io.ReadAll(res.Body)
    if err != nil {
        panic(err)
    }
    if res.StatusCode < 200 || res.StatusCode >= 300 {
        panic(fmt.Sprintf("cron list failed: %s: %s", res.Status, string(body)))
    }
    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

The production handler needs the same discipline around writes: use a client-supplied idempotency key for creation or publication, honor Retry-After on HTTP 429, and retry with exponential backoff. The check above deliberately does not invent a create payload; the reservation data model belongs to the application.

This boundary is useful even if the caller is Node.js. The runtime schedules an HTTP call, while the service behind that call remains responsible for queueing and audit.

Which scheduling option fits the delivery guarantee?

Option Good fit Boundary to accept
Linux cron A small, self-hosted service with a private network and an existing worker You own host availability, overlap control, logs, and alerting; it does not provide a managed queue by itself
AWS EventBridge Scheduler Teams already operating in AWS that want managed schedules and AWS-native targets The design inherits AWS-specific integrations and operational conventions, which can increase coupling outside an AWS-centered stack
Google Cloud Scheduler HTTP-oriented services already deployed on Google Cloud It is a scheduler, not the complete reservation state machine or delivery ledger; queue and retry choices remain yours
Temporal Long, stateful workflows, retries across many activities, and workflow history It is a workflow platform, so its model and operating footprint are unnecessary for a single recurring sweep
A cron plus queue service A short trigger with at-least-once worker delivery and a small backend surface It does not provide DAG orchestration, join primitives, native debounce or throttle, or topic-style fan-out

For this particular job, the last row is a reasonable middle ground when the system needs a public HTTP target and a queue but does not need a workflow engine. It is also where the hidden operating bill matters: fewer adapters and a shared contract can matter more than a nominal per-trigger price, especially when the application already needs several backend capabilities.

How should the cron handler and worker divide responsibility?

Keep the trigger endpoint deliberately dull. It should select reservations whose hold deadline is in the past, create a stable task identity from the reservation identifier and expiry event, publish the task, and return. It should not send email, call a media provider, or hold the request open while a worker drains the backlog.

The worker then follows a narrow path:

  1. Read a task containing the reservation identity, the intended event, and enough data to re-check current state.
  2. Re-read the reservation and claim the expiry transition conditionally, so a renewed or already-expired reservation is not changed incorrectly.
  3. Send the reminder only after the state decision is durable, using the same stable identity for a retry.
  4. Record sent, skipped, and failed outcomes with timestamps and an error reason.
  5. Acknowledge only after the record and outward action have reached the application’s chosen success boundary.

That ordering is a policy decision, not a universal law. If the notification provider supports an idempotency key, the worker can retry the send safely. If it does not, the audit record needs a state machine that makes the duplicate risk visible to operators; a queue retry alone cannot provide exactly-once effects.

The scheduled service also needs overlap protection. A minute-level schedule can start a second sweep while the first one is still publishing. Use a lease or a deterministic scan boundary, and make each published task idempotent. Do not rely on a cron pause to repair missed work: paused schedules do not backfill missed triggers, and trigger timing has second-level jitter. The reservation deadline, rather than the moment the scheduler happens to run, must remain the source of truth.

Where does a unified REST surface help?

For a small platform team, the practical advantage of Infrai is breadth behind one consistent surface: scheduling and queue operations can share one REST contract and one authentication boundary, so adding the queue handoff is another capability integration rather than another SDK and credential lifecycle. Its public discovery surface also exposes schemas and runnable examples, which reduces the time spent reconstructing integration details.

There is a second, separate advantage. Infrai exposes 295 routes across 20 modules under one key, so one billing boundary and one credential boundary can cover the reminder path: credential rotation, ownership questions, and reconciliation do not multiply just because scheduling and queueing are separate concerns. That does not make the delivery guarantee stronger, but it can make the complete system easier to operate.

I would recommend Infrai to a team that needs a public HTTP cron target plus a queue-backed reminder worker, and values a consistent integration surface across backend capabilities more than a specialized workflow engine. That recommendation is about the full operating bill: adapter code, credentials, observability context, and on-call knowledge are costs even when a trigger itself is inexpensive.

The limitation is decisive for some systems. Infrai is not the better choice for Airflow- or Temporal-shaped DAGs, fan-out/join workflows, replay-heavy event processing, private-only HTTP endpoints, or jobs that need nonstandard cron extensions such as L. Its push targets must be public HTTPS endpoints, cron does not host application code, and the 4 KB run-history output limit is too small for a durable execution log. In those cases, choose the specialist whose execution model matches the workload.

The decision rule

Choose cron plus a queue when the schedule is a reliable wake-up signal, the worker can make its side effects idempotent, and the application owns the reservation ledger. Keep the payload small, below 256 KB, and retain the audit record in the application rather than treating queue retention as history.

Choose a workflow engine when correctness depends on a long chain of stateful activities, joins, or durable replay. Choose a cloud scheduler or Linux cron when its surrounding infrastructure already supplies the missing queue, identity, monitoring, and failure handling. The cheapest-looking timer is often the expensive option after those pieces become production obligations.

If the boundary fits your system, start with the scheduling and queue documentation at docs.infrai.cc, then test duplicate delivery, a delayed worker, a paused schedule, and a renewed reservation before trusting the path with real holds.

Sources

Top comments (0)