Mahmoud AbdelwahabRun Scheduled and Recurring Tasks with Cron
This guide explains how to deploy scheduled and recurring tasks with cron. It introduces the execution model that governs cron jobs, examines common use cases, and outlines the operational practices that keep these tasks reliable in production. It also describes how to deploy cron-based workloads on Railway.
Cron provides a time-based execution system that runs commands according to predefined schedules. A cron job is a schedule-command pair. The schedule defines when a task should start, and the command specifies what the system executes at that moment.
Cron evaluates schedules at fixed time boundaries, checks them against the current time, and runs any command whose schedule matches. Each run is independent. Cron does not store job history, track partial progress, or coordinate concurrency across entries. It launches a process with the configured environment and leaves error handling to the invoked program.
This model suits tasks that run at predictable intervals without external orchestration. Workloads that need coordination across nodes, queue semantics, or recovery guarantees fall outside cron’s scope.
Cron’s constraints shape the categories of work it handles reliably. Cron aligns with workloads that run on predictable intervals and do not require coordination across multiple workers. These tasks complete in a single invocation, tolerate being triggered strictly by time, and can handle their own error conditions. Here are some use cases:
- Data backups, log rotation, and cleanup: Periodic maintenance fits well into cron’s model. Backups, archival routines, temporary file cleanup, and log rotation all rely on fixed schedules and run as isolated operations. They benefit from cron’s simplicity and do not need queueing or distributed orchestration.
- Batch processing: Many batch workloads operate on daily or hourly intervals. Examples include report generation, data extraction pipelines with fixed windows, and transformations that process a known set of inputs. As long as a batch task can complete within its scheduled window and does not depend on event-driven triggers, cron provides a stable trigger mechanism.
- Scheduled notifications and email delivery: Cron can initiate periodic notifications such as system summaries, daily digests, or alert checks. The underlying logic must handle its own rate limits and delivery failures, because cron itself provides no retry semantics.
- Syncing or fetching external data: Recurring synchronization tasks, such as pulling data from external APIs or refreshing caches, fit the model when they can be executed independently and tolerate occasional delays or transient failures.
- Automation for DevOps and CI tasks: Infrastructure-oriented routines such as certificate checks, dependency audits, container cleanup, or automated environment validation often run on fixed intervals. These tasks are usually short-lived and isolated, which makes cron an appropriate trigger mechanism.
Cron schedules use a compact notation to express when a task should start. The format encodes minutes, hours, days, months, and weekdays in a fixed sequence of fields. Cron evaluates these fields independently and starts a task when all specified conditions match the current time. Understanding this model is essential for predicting how a job behaves under real workloads.
A cron schedule contains five space-separated fields:
* * * * *
│ │ │ │ └── weekday (0–6)
│ │ │ └──── month (1–12)
│ │ └────── day of month (1–31)
│ └──────── hour (0–23)
└────────── minute (0–59)Each field represents an exact match or a set of matches. Cron checks all fields at the start of each minute. A schedule triggers only when every field matches the current timestamp. Cron performs no cross-field reasoning. For example, a job that specifies both a day of month and a weekday will fire whenever either field matches, which can create schedules that run more often than intended.
Cron supports a compact expression language in each field.
- Wildcards. An asterisk matches every possible value in the field. For example,
* * * * *runs every minute. - Ranges. A range such as
1-5matches all values between the endpoints. - Lists. Comma-separated values create a set. For example,
1,15in the day-of-month field runs on the 1st and 15th. - Steps. A slash introduces a step.
*/5in the minute field triggers every 5 minutes. Steps also apply to ranges, such as1-10/2, which matches 1, 3, 5, 7, and 9.
These operators combine freely within a field. Cron does not attempt to simplify or interpret intent. It treats the final set of values as the rule.
Several patterns appear frequently in production systems:
0 * * * *runs at the start of every hour0 0 * * *runs once per day at midnight/10 * * * *runs every 10 minutes0 3 * * 1runs every Monday at 03:0030 2 1 * *runs on the first day of each month at 02:3030 * * * *runs every hour at the 30th minute15 15 * * *runs every day at 3:15 PM0 8 * * 1runs every Monday at 8:00 AM0 0 1 * *runs every month on the 1st day at 12:00 AM30 14 * 1 0runs every Sunday at 2:30 PM in January30 9 * * 1-5runs every weekday (Monday to Friday) at 9:30 AM0 6 2 * *runs every 2nd day of the month at 6:00 AM
The simplicity of these expressions is useful, but misalignment between human expectations and cron’s matching rules is common. Testing schedules before deployment avoids unintended triggers.
Cron interprets schedules according to the host’s timezone unless configured otherwise. Systems running in containers or distributed environments often default to UTC. Tasks that depend on local time boundaries, such as end-of-day processing or region-specific reporting, must account for this.
Running cron in UTC keeps behavior predictable across deployments and avoids drift caused by daylight saving transitions. If local time behavior is required, the environment should set consistent timezone configuration and ensure the task logic tolerates DST adjustments.
Cron does not manage state, coordination, or error recovery. Production reliability depends on how each job behaves when triggered and how it interacts with external systems.
A cron job should safely tolerate repeated execution with the same inputs. Idempotence prevents corrupted state when a job retries logic internally or when operators need to re-run a task manually. Database upserts are a common pattern:
INSERT INTO reports (id, generated_at, data)
VALUES ($1, NOW(), $2)
ON CONFLICT (id) DO UPDATE
SET generated_at = NOW(), data = EXCLUDED.data;Idempotence also applies to file generation, API requests, and cache refresh logic. Deterministic filenames, unique request keys, and guard checks help prevent partial updates.
Cron provides no built-in history. Every job must log its start, success, and error conditions explicitly. A simple TypeScript structure keeps logs predictable:
async function main() {
const startedAt = new Date().toISOString();
console.log("job_start", { ts: startedAt });
try {
await runJob();
console.log("job_success", { ts: new Date().toISOString() });
} catch (err) {
console.error("job_error", { ts: new Date().toISOString(), err });
process.exitCode = 1;
}
}These logs become the foundation for observability and alerting.
Cron assumes the task completes before the next scheduled trigger. Jobs that approach their interval duration risk overlapping with themselves, creating contention or double processing. Reducing batch size, dividing work into smaller windows, or offloading heavier computation to a dedicated worker system avoids these issues.
Cron will not clean up leaked resources. Network clients, database pools, file handles, and buffers must be released explicitly:
import { Client } from "pg";
async function runJob() {
const client = new Client({ connectionString: process.env.DATABASE_URL });
try {
await client.connect();
await processWork(client);
} finally {
await client.end(); // avoid leaked connections across runs
}
}Even small leaks accumulate across many invocations.
Cron does not serialize job execution. If a previous run is still active, cron will start another. A database-level advisory lock provides a simple and robust guard:
-- Try to acquire a lock identified by an integer key.
-- Returns true only if this process acquires the lock.
SELECT pg_try_advisory_lock(42) AS acquired;A job should check this before doing work:
const res = await client.query(
"SELECT pg_try_advisory_lock($1) AS acquired",
[42]
);
if (!res.rows[0].acquired) {
console.log("job_skip", { reason: "lock_not_acquired" });
return;
}Only the instance that acquires the lock proceeds. Others exit immediately, avoiding conflicting updates.
Cron expressions are compact and easy to misinterpret. Values that combine day-of-month and weekday fields are especially error-prone. Before deploying a schedule, preview it using a tool such as crontab.guru.
This makes timing assumptions visible and prevents unexpected triggering patterns.
Cron is suitable when jobs are short, independent, and tolerant of fixed time triggers. It is less suitable for workloads that require task chaining, backpressure, retries with guarantees, distributed coordination, or progress tracking. Systems such as job schedulers and message queues provide durability, visibility, and structured workflows that cron does not model.
These practices make cron predictable under load and resilient to the failure modes inherent in timer-driven execution.
Railway’s cron system provides scheduled execution without keeping a service running between tasks. This eliminates the need for an in-process scheduler such as node-cron or Quartz, and avoids paying for idle compute. The platform starts a service only when work needs to be performed, runs the task, and then returns the service to an inactive state.

Railway evaluates cron expressions in UTC and triggers a job by running the service’s start command at the scheduled time. The process is expected to execute a single task, close all external resources, and exit. Open database connections, active HTTP clients, background timers, or any other long-lived handles keep the process alive after the task is complete.
Since Railway does not automatically terminate active deployments, a job that stays running blocks the next scheduled execution. If the previous run is still active at the next trigger time, the new run is skipped. Most skipped executions trace back to code paths that never return, unclosed connections, pending promises, or background work that was not awaited.
RAILWAY CRON EXECUTION FLOW
┌─────────────────────────────────────────────────────────────────┐
│ CRON SCHEDULER (UTC) │
└───────────────────────────┬─────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ Trigger Scheduled │
│ Start Command │
└───────────┬───────────┘
│
▼
╔═══════════════════════════════════════════════════════════════════╗
║ JOB EXECUTION ║
╠═══════════════════════════════════════════════════════════════════╣
║ ║
║ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ║
║ │ Execute │──▶│ Close │──▶│ Exit │ ║
║ │ Task │ │ Resources │ │ Process │ ║
║ └──────────────┘ └──────────────┘ └───────┬──────┘ ║
║ │ ║
╚═════════════════════════════════════════════════╪═════════════════╝
│
┌─────────────────────────────────┴───────────────┐
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ ✓ SUCCESS │ │ ✗ STUCK │
│ Process exited │ │ Process running │
└─────────┬─────────┘ └─────────┬─────────┘
│ │
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────┐
│ Ready for next │ │ BLOCKING RESOURCES: │
│ scheduled trigger │ │ ┌───────────────────────┐ │
└─────────────────────────┘ │ │ • Open DB connections │ │
│ │ • Active HTTP clients │ │
│ │ • Background timers │ │
│ │ • Pending promises │ │
│ │ • Unawaited work │ │
│ └───────────────────────┘ │
└──────────────┬──────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ NEXT TRIGGER TIME │
│ ┌────────────────────────────────────────┐ │
│ │ Previous run still active? │ │
│ │ │ │
│ │ YES ──▶ ⊘ NEW RUN SKIPPED │ │
│ │ │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘Railway’s implementation introduces several operational boundaries:
- All schedules are evaluated in UTC. Any workload that depends on local time boundaries must account for its own offset and daylight-saving behavior when defining its cron expression
- Actual firing times may drift by a few minutes under platform load
- The minimum interval between runs is five minutes
- Only one execution may be active at a time
- The system is designed for short-lived tasks, not long-running services such as web servers or chat bots
These constraints define the workflows that align cleanly with Railway’s cron model.
To configure a scheduled task:
- Open the service and navigate to Settings
- Enter a valid cron expression in the Cron Schedule field
- Save the configuration
The cron expression determines the trigger schedule. The service itself is responsible for performing the work and terminating promptly.
Cron remains a precise and reliable mechanism for time-based execution when the workload fits its constraints. Its model is straightforward: evaluate a schedule, start a process, and allow that process to perform a discrete unit of work. Everything else, including correctness, idempotence, observability, concurrency control, and cleanup, belongs to the task itself. When these responsibilities are handled with care, cron provides a stable foundation for recurring operational tasks.
Railway builds on this model by supplying an execution environment that runs only when needed. A scheduled task consumes resources only for the duration of its work and then exits. This keeps recurring jobs efficient while preserving the essential behavior of cron.
As workloads increase in complexity, the distinction becomes clear. Cron is suitable for tasks that are short, bounded, and self-contained. A job scheduler or queue is suitable for tasks that require coordination, durability, or higher throughput. With these boundaries in mind, cron continues to serve as a dependable tool for periodic automation in production systems.