Cron Expression Best Practices: From Pitfalls to Production

Utilities

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

  1. Open the Cron Expression tool
  2. Enter your Cron expression
  3. 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"
  4. Review the next execution times (the next 5-10 runs)
  5. 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

  1. Always use the UTC timezone, unless your business strongly depends on local time
  2. Never specify both day-of-month and day-of-week, to avoid the union trap
  3. Concurrency control: only one instance runs at any moment
  4. Distributed lock: prevent duplicate execution in multi-instance deployments
  5. Retries + alerting: never fail silently
  6. Timeout control: prevent tasks from hanging forever
  7. Idempotent design: repeated runs produce the same result
  8. Execution logs: record the time, duration, and status of every run
  9. Validate expressions with a tool: confirm the semantics before deploying
#Cron#定时任务#crontab#时区#运维