Cron Expression Best Practices: From Pitfalls to Production
UtilitiesRachel Kim
Cron Expressions: 5 Fields to Control Time
┌───────── minute (0-59)
│ ┌───────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌───────── month (1-12)
│ │ │ │ ┌───────── day of week (0-7, both 0 and 7 are Sunday)
│ │ │ │ │
* * * * *
1. Syntax Cheat Sheet
| Expression | Meaning | Runs |
|---|---|---|
* * * * * |
Every minute | 1440 times/day |
*/5 * * * * |
Every 5 minutes | 288 times/day |
0 * * * * |
Every hour on the hour | 24 times/day |
0 9 * * * |
Daily at 9:00 | 1 time/day |
0 9 * * 1-5 |
Weekdays at 9:00 | 5 times/week |
30 2 1 * * |
2:30 on the 1st of each month | 1 time/month |
0 0 1 1 * |
Midnight on January 1st | 1 time/year |
Special Characters
| Character | Meaning | Example |
|---|---|---|
* |
Any value | * * * * * |
, |
List | 0 9,12,18 * * * (at 9/12/18 o'clock) |
- |
Range | 0 9-17 * * 1-5 (weekdays 9am-5pm) |
/ |
Step | */15 * * * * (every 15 minutes) |
L |
Last | 0 0 L * * (last day of the month) |
W |
Nearest weekday | 0 0 1W * * (nearest weekday to the 1st) |
# |
Nth weekday | 0 0 * * 5#3 (third Friday) |
2. Common Pitfalls
Pitfall 1: Specifying Both Day-of-Month and Day-of-Week
# Misinterpretation: the 15th of each month AND Friday
0 0 15 * 5
# Actual meaning: the 15th of each month OR every Friday (union!)
# The correct approach requires extra logic in your code
Rule: When both day-of-month and day-of-week are specified, they are combined as a union, not an intersection.
Pitfall 2: Timezone Issues
# Expected to run at 9:00 Beijing time
0 9 * * *
# Server is in UTC → actually runs at 17:00 Beijing time!
# Fix: specify the timezone explicitly
// node-cron with an explicit timezone
cron.schedule('0 9 * * *', task, {
scheduled: true,
timezone: 'Asia/Shanghai',
});
Pitfall 3: Writing 60 for Minutes
# Wrong: there is no 60 in the minute field
0 0 60 * * *
# Correct: run every hour
0 * * * *
Pitfall 4: February 30th
# February has no 30th, so this expression never runs in February
0 0 30 2 *
Pitfall 5: Daylight Saving Time
When DST switches, some local times may not exist or may occur twice:
Spring forward: 2:00 → 3:00 (2:30 does not exist)
Fall back: 2:00 → 1:00 (1:30 runs twice)
Recommendation: Use the UTC timezone to avoid DST issues.
3. Validate Cron with ToolsKu
Steps
- Open the Cron Expression tool
- Enter your Cron expression
- Read the human-readable interpretation:
0 9 * * 1-5→ "Every Monday to Friday at 09:00"*/30 8-18 * * *→ "Every day 08:00-18:00, every 30 minutes"
- Review the next execution times (the next 5-10 runs)
- Verify the expression is valid
4. Designing Production-Grade Scheduled Tasks
Concurrency Control
import { CronJob } from 'cron';
let isRunning = false;
const job = new CronJob('0 */5 * * * *', async () => {
if (isRunning) {
console.log('Previous run has not finished, skipping this execution');
return;
}
isRunning = true;
try {
await processData();
} finally {
isRunning = false;
}
});
Distributed Lock (Multi-Instance Deployment)
import { Redis } from 'ioredis';
const redis = new Redis();
async function acquireLock(key, ttl = 300) {
const acquired = await redis.set(key, '1', 'EX', ttl, 'NX');
return acquired === 'OK';
}
const job = new CronJob('0 2 * * *', async () => {
const lockKey = `cron:daily-report:${new Date().toISOString().slice(0, 10)}`;
if (!(await acquireLock(lockKey))) {
console.log('Another instance holds the lock, skipping');
return;
}
try {
await generateDailyReport();
} finally {
await redis.del(lockKey);
}
});
Failure Retries
async function runWithRetry(task, maxRetries = 3, delay = 5000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await task();
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error.message);
if (attempt < maxRetries) {
await sleep(delay * attempt); // Increasing delay
} else {
throw error;
}
}
}
}
const job = new CronJob('0 */10 * * * *', () => {
runWithRetry(syncData, 3, 5000);
});
Timeout Control
async function runWithTimeout(task, timeoutMs = 60000) {
return Promise.race([
task(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Task timed out')), timeoutMs)
),
]);
}
5. Task Monitoring
Execution Logging
const job = new CronJob('0 2 * * *', async () => {
const startTime = Date.now();
const context = { job: 'daily-sync', startedAt: new Date().toISOString() };
try {
await runWithRetry(syncData);
context.status = 'success';
} catch (error) {
context.status = 'failed';
context.error = error.message;
await sendAlert(context); // Alert
} finally {
context.duration = Date.now() - startTime;
await logExecution(context); // Log the execution
}
});
Health Checks
// Record the last successful execution time
await redis.set('cron:last-success:daily-sync', Date.now());
// Health check: alert if no run for over 25 hours
const lastSuccess = await redis.get('cron:last-success:daily-sync');
if (Date.now() - lastSuccess > 25 * 3600 * 1000) {
await sendAlert({ type: 'cron_stale', job: 'daily-sync' });
}
6. Common Scheduled Task Patterns
| Requirement | Cron Expression | Notes |
|---|---|---|
| Hourly data sync | 0 * * * * |
On the hour |
| Weekday morning report | 0 9 * * 1-5 |
Monday-Friday at 9:00 |
| Daily backup | 0 2 * * * |
2:00 AM |
| Weekly cleanup | 0 3 * * 0 |
Sunday at 3:00 |
| Monthly reconciliation | 0 6 1 * * |
6:00 on the 1st of each month |
| Heartbeat every 15 minutes | */15 * * * * |
High-frequency check |
| Quarterly report | 0 8 1 1,4,7,10 * |
1st day of each quarter's first month |
7. Best Practices Checklist
- Always use the UTC timezone, unless your business strongly depends on local time
- Never specify both day-of-month and day-of-week, to avoid the union trap
- Concurrency control: only one instance runs at any moment
- Distributed lock: prevent duplicate execution in multi-instance deployments
- Retries + alerting: never fail silently
- Timeout control: prevent tasks from hanging forever
- Idempotent design: repeated runs produce the same result
- Execution logs: record the time, duration, and status of every run
- Validate expressions with a tool: confirm the semantics before deploying
#Cron#定时任务#crontab#时区#运维