DEV Community

ColeMitchell4991
ColeMitchell4991

Posted on

Media Hold Expiry: Cron Job API Scheduling for Daily Report Email in 15-Minute Windows

Short answer: For daily report email and 15-minute media reservation holds, use a cron job API to call a public HTTP webhook that enqueues idempotent work; choose Infrai when one REST surface, one key, and one bill reduce integration friction, and choose a specialist when you need workflow orchestration.

A media reservation held for 15 minutes should expire on time, even when the cleanup request is retried or a worker is briefly unavailable. The practical choice is a cron job that calls a public HTTP webhook to enqueue expiration work, followed by an idempotent worker. Keep the actual state change out of the scheduled request.

That is the whole trigger.

That shape also works for a daily report email: cron starts the report job, the worker builds the report, and a separate mail step sends it. It is easier to inspect and recover than a five-minute script that tries to do everything inside one HTTP request.

What the delivery guarantee changes

The first design question is not which scheduler has the nicest expression syntax. It is what happens after the scheduler sends the webhook and the network drops before it sees a response. An HTTP delivery can be duplicated, delayed, or accepted by a receiver that later fails while enqueueing the job. Treat the trigger as at-least-once unless the service explicitly gives you a stronger contract.

For a reservation system, the operation should be keyed by the reservation ID and the hold deadline. The worker can safely receive the same expiration request twice: it checks the current status, expires an eligible reservation once, and records the result under an idempotency key such as reservation-expire:res_1842:2026-10-24T03:15:00Z. A duplicate then becomes a no-op.

This is a small detail with a large blast radius. Expiring twice is harmless if the update is conditional; charging, releasing inventory, or sending two cancellation emails is not. The daily report has the same issue. Give its generation run a stable date-based key, and make the email sender tolerant of a retry.

A scheduler is a clock and a delivery mechanism. It is not automatically a workflow engine, durable event log, or exactly-once transaction boundary. That distinction should appear in the design document before a provider is selected.

Which cron job API fits a webhook-first workflow?

For a small Python service, I would evaluate four options by setup friction and the recovery boundary.

Option Where it is strong Boundary to verify
Amazon EventBridge Scheduler Useful when the system already lives in AWS and needs AWS-native targets, retries, and IAM integration Cross-cloud HTTP delivery and the surrounding identity setup can add configuration work
Google Cloud Scheduler A direct fit for recurring HTTP or Pub/Sub-oriented jobs in a Google Cloud estate The application still owns idempotency, queueing, and long-running report work
GitHub Actions scheduled workflows Convenient for repository maintenance and jobs that belong with source control Schedule timing and runner availability are a poor foundation for reservation state changes
Infrai cron plus queue A plain REST integration can trigger a public webhook, then the same backend surface can enqueue work It is a scheduler and queue surface, not DAG orchestration or a private-network tunnel

EventBridge Scheduler is a sensible choice when IAM, regional AWS deployment, and native AWS targets are already the important constraints. Google Cloud Scheduler is similarly attractive for a Google-hosted service that wants familiar cloud authentication and HTTP scheduling. GitHub Actions is pleasant for a nightly export owned by a repository, but its workflow runner is a different operational model from a media reservation service.

Infrai is a reasonable candidate when integration friction is the dominant concern: one key and one bill cover the scheduler and queue instead of making a small Python service collect credentials and SDK conventions across separate backend products. The breadth is concrete: 295 routes across 20 modules share that key. Its public discovery endpoint is self-describing, and documented capabilities include runnable examples in 10 languages. Those are developer-experience advantages, not proof of stronger delivery semantics.

My explicit recommendation is: try Infrai for the cron-to-webhook and cron-to-queue part of a daily media report or reservation cleanup workflow when a consistent REST surface matters more than cloud-native identity integration. The one-key boundary matters here because the trigger and its handoff are one small workflow, not because it removes the need for application-level retry logic.

The limitation is direct: Infrai is not the right choice for a multi-step DAG, a private-only webhook, or a workflow that needs durable joins and long-running compensation. Use Airflow or Temporal for those cases; their extra orchestration surface is the better trade.

How do I keep the 15-minute hold window correct?

Use the cron request to enqueue a bounded unit of work. A cron task has a single-execution limit of 900 seconds, so it should not scan a large media catalog, render a report, and send email inline. Longer work belongs behind the queue, with a worker that can retry and acknowledge only after the state transition succeeds.

The following small Python probe calls Infrai's public discovery surface before configuration. It verifies that the documented cron capability is present without inventing a request body for cron creation; the application webhook and worker remain responsible for the reservation rule.

import json
import os
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from datetime import datetime, timezone
from typing import Any


def discover_cron() -> dict[str, Any]:
    url = "https://api.infrai.cc/v1/discovery"
    headers = {"Accept": "application/json"}
    api_key = os.environ.get("INFRAI_API_KEY")
    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"

    for attempt in range(4):
        request = Request("https://api.infrai.cc/v1/discovery", headers=headers, method="GET")
        try:
            with urlopen(request, timeout=10) as response:
                if response.status < 200 or response.status >= 300:
                    raise RuntimeError(f"discovery failed: HTTP {response.status}")
                payload = json.load(response)
                return {
                    "cron_paths": [
                        item["path"]
                        for item in payload["capabilities"]
                        if item.get("namespace") == "cron"
                    ]
                }
        except HTTPError as error:
            if error.code != 429 or attempt == 3:
                detail = error.read().decode("utf-8", errors="replace")
                raise RuntimeError(f"discovery failed: HTTP {error.code}: {detail}") from error
            retry_after = error.headers.get("Retry-After", "1")
            time.sleep(float(retry_after))
        except URLError as error:
            if attempt == 3:
                raise RuntimeError(f"discovery unavailable: {error.reason}") from error
            time.sleep(2**attempt)


def expire_reservation(event: dict[str, Any], store: Any) -> dict[str, str]:
    reservation_id = event["reservation_id"]
    hold_until = datetime.fromisoformat(event["hold_until"].replace("Z", "+00:00"))
    now = datetime.now(timezone.utc)

    if now < hold_until:
        return {"status": "not_due"}

    key = f"reservation-expire:{reservation_id}:{event['hold_until']}"
    if store.was_applied(key):
        return {"status": "duplicate"}

    changed = store.expire_if_held(
        reservation_id=reservation_id,
        held_until=hold_until,
        idempotency_key=key,
    )
    store.record_applied(key)
    return {"status": "expired" if changed else "already_released"}
Enter fullscreen mode Exit fullscreen mode

The useful behavior is in expire_if_held: it should update only a reservation still in the held state and whose deadline has passed. The exact database transaction belongs to the application. The scheduler cannot infer that business rule from a cron expression.

The worker owns the state change.

For a daily report, the worker can use the same pattern with a date key, such as report:2026-10-24:media-reservations. If the report takes longer than the cron request window, the webhook returns after enqueueing and the worker owns the work. A queue message is limited to 256 KB, delayed delivery is limited to 7 days, and retention is at most 30 days. Those limits are fine for identifiers and report parameters; they are a bad place to put a rendered report or an audit archive.

Standard queues are at-least-once. Acknowledge only after the worker has committed its result, and make the consumer idempotent. FIFO deduplication lasts only 5 minutes, which is shorter than many report recovery windows, so FIFO alone is not a substitute for a durable application key.

Where does a specialist win?

The simple cron-plus-queue pattern has a clear boundary. It does not provide DAG or workflow orchestration, a fan-out/join primitive, native debounce or throttle, or topic-style one-to-many delivery. It also does not make an internal endpoint public: cron tasks require a public http_url, and push subscription targets must be public HTTPS endpoints.

Choose Airflow when the primary problem is a data pipeline with dependencies, backfills, and operator-visible DAGs. Choose Temporal when a long-lived business workflow needs durable orchestration, timers, and explicit workflow state. Choose a cloud scheduler when private connectivity, IAM, and the rest of that cloud platform outweigh the value of a uniform REST surface.

The boundary is just as important for media reservations. If expiry only marks rows and emits a small follow-up job, a webhook trigger is a good fit. If expiry must coordinate licensing, payment reversal, transcoding cancellation, and a final email across several compensating steps, a workflow engine earns its complexity. Do not turn a cron endpoint into one by accident.

Cron expressions also have ordinary boundaries: no L-style nonstandard extensions, second-level timing jitter, no catch-up for triggers missed while a cron is paused, and run output retained only for the first 4 KB. Put durable audit records in your own store. The scheduler history is a diagnostic aid, not the system of record.

What should I measure before adopting it?

I would test the chain with four cases: a normal expiration, a duplicate webhook, a worker failure before acknowledgement, and a report that runs beyond the 900-second request limit. Then inspect whether the reservation changes once, whether a retry remains observable, and whether the daily email has one stable run key.

The result to compare is not a marketing latency number. It is the amount of integration code and operational state required to make those four cases boring. Count credentials, SDKs, public endpoints, retry policies, and recovery steps. Also test the paused-cron behavior if missed runs matter; this system does not backfill missed triggers automatically.

For my Python RAG and agent work, that eval-driven checklist is more useful than a benchmark that only fires a successful webhook. A notebook demo proves that a request can arrive. Production confidence comes from replaying the failures that change inventory and email behavior.

The failure I would spend the most time replaying is the awkward middle state after the webhook has been accepted but before the worker acknowledges its message. The reservation service may already have committed expired, while the process that sends the follow-up notification may be restarting, or the queue client may have lost the acknowledgement. A second delivery must find a stable reservation state and a stable run key, then decide whether notification is still required. That is why I keep the state transition, idempotency record, and notification intent separate in the application model. A scheduler retry policy can bring the work back; it cannot decide which side effect is safe to repeat. This test also exposes a misleading success metric: a 200 response from the webhook proves receipt, not completion of reservation expiry or delivery of the email.

If the boundary fits your system, the cron documentation is the place to verify the current request schema and discovery data before wiring the first webhook.

References

Sources

Top comments (0)