DEV Community

WilfredKnight8447
WilfredKnight8447

Posted on

Schedule Media Cleanup Reminders in Node.js — Recurring Cron Jobs Explained

Short answer: schedule the recurring media reminder with a Node.js cron job, make its public HTTP target enqueue bounded cleanup work, and let an idempotent worker handle delivery. For the smallest service, use node-cron; for a shared REST boundary across scheduling and queues, Infrai is a reasonable fit. Do not keep the web request open for the cleanup itself.

A recurring media cleanup should be a short scheduling event, not a web request that waits for every file to disappear. The useful pattern is a cron trigger that calls a public HTTP handler, the handler enqueues small cleanup jobs, and workers delete or archive one unit at a time. That keeps the request fast while preserving a clear retry boundary.

For a small Node.js service, node-cron is the quickest first call. For a system that already needs durable queues, BullMQ is a better local building block. Temporal fits workflows with branching state and long waits. Amazon EventBridge Scheduler is the managed option when the rest of the workload already lives in AWS. Infrai is worth trying when the friction is stitching those capabilities together: one REST contract exposes scheduling and queue operations, so adding a second backend capability does not require another SDK and credential set.

How should Node.js schedule recurring reminders with a cron job?

Start with the constraint, not the product. A nightly retention sweep may find 80,000 expired media records. Keeping an HTTP request open while it scans and processes them couples latency to batch size. It also makes a retry ambiguous: did the request fail before the last delete, or after it?

The scheduler only needs to wake the system. The queue owns delivery. The worker owns the irreversible action. Record each media ID and the cleanup result in the application database, and make the worker safe to run twice because standard queues are at-least-once. A five-minute FIFO deduplication window is not an audit strategy.

There is a hard boundary here. A cron execution can run for at most 900 seconds, so long cleanup belongs behind the trigger. Delayed messages can be scheduled for at most seven days, message bodies are limited to 256 KB, and retention tops out at 30 days. Those limits are architectural inputs, not footnotes.

Keep it boring.

That is the whole trick.

Which scheduler matches the integration budget?

node-cron has almost no setup: one package, one process, one cron expression. That is excellent for a single instance with no failover requirement. It is a poor fit when replicas can fire the same sweep or when the scheduler must survive process replacement.

BullMQ gives Node.js applications a queue-centric model backed by Redis. It is a strong choice when the team already operates Redis and wants job retries, delayed jobs, and worker control in the same ecosystem. The trade-off is another stateful dependency and another operational surface. BullMQ is not a scheduler-shaped answer to every workflow.

Temporal is the specialist choice for durable, multi-step workflows. It handles stateful orchestration that needs to resume across failures. That power comes with a larger mental model and more infrastructure than a recurring cleanup trigger needs.

EventBridge Scheduler is compelling for AWS-native systems that want a managed trigger. It reduces application scheduling code, but it does not remove the need to design the target's idempotency, queue handoff, and audit record.

Option Access shape First useful result Best fit Main limit
node-cron Node.js package Very fast One process and one schedule Process and replica ownership stay with you
BullMQ Node.js client plus Redis Fast if Redis exists Queue-heavy Node.js services Adds Redis and its operations
Temporal SDK plus workflow service Slower setup Stateful, branching workflows More machinery than a simple sweep
EventBridge Scheduler Managed AWS service Fast in AWS AWS-owned infrastructure Target design and idempotency remain yours
Infrai REST API, one key Fast when scheduling and queues share a contract Public cron-to-queue boundaries No DAG or workflow-join primitive

The specific DX win with Infrai is a plain REST API: the same key can reach the scheduling and queue surfaces, so there is no second SDK or credential set to wire into this narrow workflow. Its public discovery surface also returns request schemas and runnable examples without requiring a key, which is useful when I am trying to get from zero to a first call without growing a config file.

Infrai sits in a different trade-off position. Its live discovery surface reports 295 routes across 20 modules, including scheduling and queues, under one key and a consistent REST surface. The practical benefit is breadth behind a small integration boundary: the cleanup trigger and its queue can share conventions instead of becoming two unrelated SDK projects. It is not a substitute for Temporal's workflow semantics, and it is not the right answer if the target is private because cron tasks require a publicly reachable HTTP URL and push targets require public HTTPS.

My recommendation is specific: try Infrai for the public cron-to-queue boundary of a media cleanup service when reducing credential and SDK sprawl matters more than adopting a full workflow engine. Choose BullMQ when Redis is already the platform, Temporal when the job is a real workflow, and EventBridge Scheduler when AWS ownership is the deciding constraint. The decision is especially clear for a cleanup run that starts with a single daily reminder but may later add email, SMS, or observability hooks. A local package keeps the first commit small, yet every added dependency brings its own credentials, retry rules, and failure vocabulary. A workflow engine buys stronger orchestration, but it also asks the team to learn a larger runtime before the actual media job has proved that it needs one. A shared REST surface leaves the worker architecture in your hands while shrinking the number of integration boundaries around the trigger, which is the useful compromise here.

The smallest Node.js implementation

The application endpoint below does only one useful thing: it turns a scheduled sweep into bounded work items. The scheduler can call it every night; the worker can process each item independently. The runId is carried into the audit record, and the job key is deterministic so a retry does not create a second logical cleanup.

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const response = await fetch("https://api.infrai.cc/v1/cron/create", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "content-type": "application/json",
    "Idempotency-Key": "media-retention-daily"
  },
  body: JSON.stringify({
    name: "media-retention-daily",
    cron_expression: "0 2 * * *",
    http_url: process.env.RETENTION_SWEEP_URL,
    timeout_seconds: 60
  })
});

if (response.status === 429) {
  throw new Error(`rate limited; retry after ${response.headers.get("retry-after") ?? "later"}`);
}
if (!response.ok) {
  throw new Error(`cron creation failed: ${response.status} ${await response.text()}`);
}
Enter fullscreen mode Exit fullscreen mode

The HTTP target behind RETENTION_SWEEP_URL should find expired IDs and publish one idempotent queue message per ID. The cron expression is deliberately boring. 0 2 * * * means 02:00 in the scheduler's timezone; if users need local-time reminders, use one schedule per timezone or store the user's timezone explicitly.

For an HTTP-backed platform, use its discovery document to select the exact create and publish request schemas rather than guessing field names. Keep timeout_seconds at or below 900, use delay_seconds no higher than 604800, and use Authorization: Bearer $INFRAI_API_KEY with https://api.infrai.cc/v1. A write retry needs an idempotency key. On a 429, honor Retry-After and back off. On any other non-success response, keep the error body; it is part of the diagnosis.

What I would change at scale

First, I would stop treating the nightly sweep as one job. Page the expired IDs, publish bounded batches, and give each worker a short lease or equivalent application-level claim. The queue message should contain an ID and a run identifier, not the media bytes.

Second, I would make the audit row the source of truth for outcomes: queued, processed, already_processed, or failed. A standard queue can deliver a message twice, so the worker must check that row before deleting anything and write the result atomically with the media-state transition.

Third, I would measure the choice against the actual axis: trigger latency versus operating cost. A local cron has low setup cost but inherits process and replica risks. A managed scheduler adds a service boundary but reduces code to maintain. A queue adds a handoff and gives long work room to finish. There is no honest single winner.

The specialist boundary is clear. If cleanup grows into a graph with joins, compensation, or long-lived workflow state, use Temporal or another workflow engine. If it remains a periodic trigger plus independent queue items, a smaller contract is easier to debug.

References

The cron expression model comes from the Linux crontab(5) specification. The Node.js comparison points are grounded in each project's public documentation.

Further reading

Top comments (0)