Cron 表達式完整教學:50 個實用範例 + 線上產生器 + macOS launchd 對照

寫給:每次寫 crontab 都要 google 五次的工程師、想搞懂 */15 * * * * 跟 0,15,30,45 差別的 DevOps、第一次設定 Kubernetes CronJob 的 SRE。

Cron 是 1975 年誕生於 Unix Version 7 的時間排程系統,至今仍是 server 自動化任務的事實標準。從每日備份、每小時拉資料、每月發報,到 Kubernetes CronJob、GitHub Actions schedule、AWS EventBridge,全部繼承 Cron 表達式語法。但 Cron 的五個欄位看似簡單,實際充滿陷阱——時區、夏令時、月份天數差異、星期重疊都讓多數工程師寫錯。本文 14 分鐘把 Cron 表達式、50 個實戰範例、跨平台對照一次講透。

1. Cron 是什麼

Cron(源自希臘文 chronos 時間)是 Unix-like 系統的時間排程守護程式(daemon)。crond 在背景每分鐘掃一次 /etc/crontab 與每位使用者的 crontab,比對當前時間與每行 schedule,符合則執行對應指令。

1.1 五個欄位

*  *  *  *  *  command
│  │  │  │  │
│  │  │  │  └─── 星期 (0-7, 0 與 7 都是星期日)
│  │  │  └────── 月份 (1-12)
│  │  └───────── 日 (1-31)
│  └──────────── 小時 (0-23)
└─────────────── 分鐘 (0-59)

範例:

30 14 * * *   /usr/local/bin/backup.sh

每天下午 2:30 跑備份。

1.2 第六欄(秒)的擴展

部分系統(Quartz、Spring Schedule)支援 6 欄位(含秒):

*  *  *  *  *  *  command
│  │  │  │  │  └── 星期
│  │  │  │  └───── 月
│  │  │  └──────── 日
│  │  └─────────── 小時
│  └────────────── 分鐘
└───────────────── 秒

GitHub Actions、Kubernetes CronJob 仍用標準 5 欄位(最小單位分鐘)。

2. 5 個核心觀念

2.1 5 種特殊字元

字元 意義 範例
* 任意值 * * * * * = 每分鐘
, 列舉 0,15,30,45 * * * * = 每 15 分
- 範圍 0 9-17 * * * = 9-17 點整點
/ 步進 */10 * * * * = 每 10 分鐘
? 不指定(Quartz) 0 0 12 ? * MON = 每週一中午

組合:

0,30 9-17/2 * * 1-5

每週一到五,9 點、11 點、13 點、15 點、17 點的 :00 與 :30。

2.2 預定縮寫

縮寫 等同 意義
@yearly / @annually 0 0 1 1 * 每年 1/1 0:00
@monthly 0 0 1 * * 每月 1 號 0:00
@weekly 0 0 * * 0 每週日 0:00
@daily / @midnight 0 0 * * * 每日 0:00
@hourly 0 * * * * 每小時整點
@reboot - 開機時

直覺好讀,但部分系統(Kubernetes)不支援。

2.3 月份與星期重疊

day 與 weekday 同時設定時是 OR 關係(任一條件成立就跑):

0 0 1 * 1

每月 1 號 0:00 或每週一 0:00 都會跑。常見錯誤是以為 AND,導致跑太多次。

要 AND 需在指令內檢查:

0 0 1 * * [ "$(date +\%u)" -eq 1 ] && /script.sh

2.4 時區陷阱

crond 用系統時區。改時區或 server 跨時區部署時:

# 設定 cron 時區(部分系統支援)
CRON_TZ=Asia/Taipei
0 9 * * * /script.sh   # 台灣時間 9 點

Kubernetes CronJob 設 spec.timeZone: Asia/Taipei。

夏令時切換(DST)特別坑:

  • 春天跳前 1 小時 → 2:00-2:59 的 cron 不會執行
  • 秋天倒退 1 小時 → 1:00-1:59 的 cron 會執行兩次

關鍵時段任務(金融結算)避開 2-3 AM。

2.5 並發控制

預設 cron 不阻擋同一個任務並發。前一個跑超過 1 分鐘、下一個又啟動:

# 用 flock 防並發
* * * * * /usr/bin/flock -n /tmp/lock.lock /script.sh

-n 是 non-blocking,搶不到鎖就跳過。

3. 實戰範例

下面 50 個範例覆蓋日常自動化所有場景。可直接複製貼進 crontab。

3.1 分鐘級

* * * * *           # 每分鐘
*/5 * * * *         # 每 5 分鐘
*/15 * * * *        # 每 15 分鐘
*/30 * * * *        # 每 30 分鐘
0,30 * * * *        # 同上(每整點與半點)
1-59/2 * * * *      # 奇數分鐘
0-59/2 * * * *      # 偶數分鐘
0 * * * *           # 每小時整點
30 * * * *          # 每小時 :30

3.2 小時級

0 */2 * * *         # 每 2 小時整點
0 9-17 * * *        # 09:00, 10:00, ... 17:00
0 9,12,18 * * *     # 09:00, 12:00, 18:00
30 8-18/2 * * 1-5   # 平日 08:30, 10:30, 12:30, 14:30, 16:30, 18:30
0 0 * * *           # 每天午夜
0 12 * * *          # 每天中午

3.3 每日固定時間

0 6 * * *           # 每天 06:00(起床)
30 23 * * *         # 每天 23:30(睡前)
0 8 * * 1-5         # 平日 08:00(上班打卡)
0 9 * * 6,0         # 週末 09:00
0 0 1 * *           # 每月 1 號 0:00
0 0 15 * *          # 每月 15 號 0:00(薪水)

3.4 週級

0 9 * * 1           # 每週一 09:00(週報)
0 18 * * 5          # 每週五 18:00(週末提醒)
0 0 * * 0           # 每週日午夜(週備份)
0 9-17 * * 1-5      # 平日 09-17 點整點
0 0 1-7 * 1         # 每月第一個週一

3.5 月與年

0 0 1 * *           # 每月 1 號
0 0 1 1 *           # 每年 1/1
0 0 25 12 *         # 聖誕節 0:00
0 0 1 1,4,7,10 *    # 每季第一天
0 0 L * *           # 每月最後一天(Quartz)
0 0 1W * *          # 每月最近 1 號的工作日(Quartz)

3.6 特定日期

0 9 14 2 *          # 每年情人節 09:00
0 0 10 5 *          # 每年母親節(5/10)
0 0 14 2 *          # 同情人節
0 17 24 12 *        # 12/24 17:00(聖誕前夕下班)

3.7 系統維護

0 3 * * *                       # 每天 03:00(最少使用者)
0 2 * * 0                       # 每週日 02:00 大備份
0 4 1 * *                       # 每月 1 號 04:00 月度任務
0 1 * * * find /tmp -mtime +7 -delete   # 清理舊 tmp
*/10 * * * * /usr/bin/check_health.sh   # 每 10 分鐘健康檢查

3.8 ETL / 資料

0 2 * * *                       # 每天凌晨 2 點 ETL
0 */4 * * *                     # 每 4 小時拉外部資料
*/15 9-15 * * 1-5               # 台股盤中每 15 分鐘
1 8 * * *                       # 早盤 08:01(盤前)
59 13 * * 1-5                   # 收盤前 1 分鐘

3.9 通知 / 報告

0 9 * * 1                       # 週一早會
0 17 * * 5                      # 週五結算
0 18 28-31 * *                  # 月底結算(月底 4 天)
0 0 1 * *                       # 月度報表
0 8 1 4 *                       # 4/1 早報(年度報表)

3.10 雜項

@reboot                         # 開機跑
@hourly                         # 每小時
@daily                          # 每天
*/30 6-22 * * *                 # 早 6 點到晚 22 點每 30 分鐘
0 12 * * 1#2                    # 每月第二個週一中午(Quartz)

4. 進階技巧

4.1 crontab 環境變數

SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=admin@example.com
HOME=/home/user

# job 自動繼承上述環境
0 * * * * my_script.sh

否則 cron 環境是極簡的(沒有 user 的 .bashrc)。

4.2 macOS launchd 對照

macOS 雖支援 cron,但官方推薦 launchd。對照表:

Cron launchd plist
0 9 * * * <key>StartCalendarInterval</key><dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
*/15 * * * * <key>StartInterval</key><integer>900</integer>
@reboot <key>RunAtLoad</key><true/>

完整 plist 範例:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.example.backup</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/backup.sh</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key><integer>3</integer>
    <key>Minute</key><integer>0</integer>
  </dict>
  <key>StandardOutPath</key>
  <string>/tmp/backup.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/backup.err</string>
</dict>
</plist>

安裝:

cp com.example.backup.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.example.backup.plist
launchctl start com.example.backup

4.3 Kubernetes CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "0 3 * * *"
  timeZone: "Asia/Taipei"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: backup:latest
          restartPolicy: OnFailure

concurrencyPolicy: Forbid 防並發(相當於 flock)。

4.4 GitHub Actions schedule

on:
  schedule:
    - cron: "0 0 * * *"     # 每天 UTC 0:00
    - cron: "0 12 * * 1"    # 每週一 UTC 12:00

注意:GitHub schedule 用 UTC,台灣時間需 -8 小時。

4.5a 重試與冪等性

cron job 失敗時行為:

  • 預設不重試
  • 下次 schedule 時間到才再跑

對「重要任務必須跑成功一次」需求:

# 失敗自動重試 3 次
0 3 * * * for i in 1 2 3; do /backup.sh && break; sleep 60; done

但更重要的是冪等性——任務跑兩次結果應該相同。例如:

  • 寄報表 → 加 idempotency key(同日同 user 只寄一次)
  • 同步資料 → 用 UPSERT 而非 INSERT
  • 清理檔案 → rm -f(不存在不 error)

4.5b launchd 完整 plist 範本

macOS 完整生產級 plist 範本:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.company.daily-etl</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/python3</string>
        <string>/Users/admin/etl.py</string>
    </array>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin</string>
        <key>DB_URL</key>
        <string>postgresql://...</string>
    </dict>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key><integer>3</integer>
        <key>Minute</key><integer>0</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>/var/log/etl.out</string>
    <key>StandardErrorPath</key>
    <string>/var/log/etl.err</string>
    <key>RunAtLoad</key>
    <false/>
    <key>KeepAlive</key>
    <false/>
</dict>
</plist>

launchctl load -w ~/Library/LaunchAgents/com.company.daily-etl.plist 載入。

4.5 監控 cron 跑沒跑

# heartbeat URL(healthchecks.io、cronitor)
0 3 * * * /backup.sh && curl -fsS -m 10 --retry 5 -o /dev/null https://hc-ping.com/UUID

跑失敗或超時,服務會發信通知。

5. 常見錯誤與解法

錯誤 1:cron 沒跑

症狀:時間到了沒執行。 根因: - 沒裝 crond 或服務沒啟 - crontab 沒寫對(用 crontab -l 確認) - 環境變數缺(PATH 找不到指令)

解法:

sudo systemctl status crond
crontab -l
cat /var/log/cron       # 查 cron log

錯誤 2:時區錯

症狀:以為設台灣時間 9 點,實際 UTC 9 點(台灣 17 點)。 根因:server 時區是 UTC。 解法:設 CRON_TZ 或 server 改時區。

錯誤 3:跑兩次

症狀:每月 1 號跑兩次。 根因:day 與 weekday 都設了(OR 關係)。 解法:星期欄留 *,或在 script 內判斷。

錯誤 4:並發爆炸

症狀:5 分鐘一個 job,但前一個沒跑完下一個又啟動,最後幾十個並發。 根因:沒控並發。 解法:用 flock、Kubernetes concurrencyPolicy: Forbid、或 script 內檢查 PID。

錯誤 5:log 沒寫

症狀:cron 失敗但完全沒留訊息。 根因:stdout / stderr 沒導向。 解法:

* * * * * /script.sh >> /var/log/script.log 2>&1

6. 用 SuperTools 線上版完成相同任務

不確定 */3 0-5 * * 1-3 到底什麼時間跑?打開 SuperTools Cron 工具:

  • 自然語言解析:顯示「每 3 分鐘、星期一到三、午夜到清晨 5 點」
  • 下次執行時間預測:列出未來 10 次執行時間
  • 視覺化曆表:標出整年所有觸發時刻
  • 語法產生器:選下拉式選單自動產 cron
  • 跨平台轉換:cron ↔ launchd plist ↔ Kubernetes CronJob
  • 時區換算:UTC ↔ Asia/Taipei ↔ 其他

工具 client-side,表達式不會上傳。

實際使用流程:

  1. 開 supertools.tw/dev/cron
  2. 貼 0 0 1 * 1 看語意
  3. 工具警告「day + weekday 都設了,會 OR」
  4. 改成 0 0 1 * *,預測下次執行時間
  5. 「Convert to launchd」一鍵轉 plist

立即使用 SuperTools Cron 工具:https://supertools.tw/dev/cron

7. 其他語言實作

Python(schedule 套件)

import schedule, time
schedule.every().day.at("09:00").do(job)
schedule.every(15).minutes.do(check)
while True:
    schedule.run_pending()
    time.sleep(1)

Node.js(node-cron)

const cron = require("node-cron");
cron.schedule("0 9 * * *", () => console.log("daily 9am"));

Go(robfig/cron)

import "github.com/robfig/cron/v3"
c := cron.New()
c.AddFunc("0 9 * * *", func() { ... })
c.Start()

Rust(cron crate)

use cron::Schedule;
use std::str::FromStr;
let s = Schedule::from_str("0 0 9 * * *").unwrap();
for next in s.upcoming(chrono::Utc).take(5) { println!("{}", next); }

Bash(crontab)

crontab -e   # 編輯
crontab -l   # 列出
crontab -r   # 清空

8. FAQ

Q1:cron 跟 systemd timer 哪個好? systemd timer 較新、可記錄精準執行時間、可依賴其他 unit。簡單任務用 cron,需要複雜依賴用 timer。

Q2:可以最小單位到秒嗎? 標準 cron 不行。Quartz / Spring 支援 6 欄位含秒。

Q3:DST 怎麼辦? 避開 2-3 AM;用 UTC 跑(無 DST);或用 systemd timer + Persistent=true。

Q4:log 寫到哪? 預設寫 /var/log/cron 或 /var/log/syslog。建議手動 redirect 到專屬 log 檔。

Q5:cron 與 K8s CronJob 差別? K8s CronJob 給容器化用,集中 manage、支援重試、history。傳統 cron 給單機。

9. 實戰案例研究

案例 1:跨時區 SaaS 的排程災難

某新加坡 SaaS 提供「每天早上 9 點寄報表」功能。架在 AWS Tokyo region(時區 UTC+9)。內部 cron 設 0 0 * * *(UTC 0:00 = 新加坡 8:00、東京 9:00)。

問題:

  1. 香港客戶(UTC+8)收信時間是 8:00(非預期 9:00)
  2. 台灣客戶(UTC+8)同上
  3. 印尼客戶(UTC+7)收信時間是 7:00

修法:

  1. DB 加 user.timezone 欄位
  2. cron 改每小時跑 0 * * * *,掃描所有 user.timezone 對應到當地 9 點的人
  3. 確認 dst 切換時不重複也不漏

教訓:multi-tenant SaaS 不能用單一 cron 解決所有 user 時區。

案例 2:Kubernetes CronJob 並發爆炸

某公司每 5 分鐘跑一個 ETL job 拉外部 API。某天外部 API 變慢,每個 job 從 30 秒變 8 分鐘。

spec:
  schedule: "*/5 * * * *"
  concurrencyPolicy: Allow   # 預設

5 分鐘後新 job 啟動,舊的還沒完。1 小時後 12 個 job 並發,把外部 API 壓掛、自家 DB 連線池爆滿、k8s pod OOM。

修法:

concurrencyPolicy: Forbid   # 前一個沒結束就跳過下一個
startingDeadlineSeconds: 60 # 超過 60 秒沒啟動視為失敗

教訓:所有 schedule job 必須有並發保護。

案例 3:DST 雙跑導致重複扣款

某金融系統每天 02:00 結算扣款。設定 0 2 * * *。北美夏令時切換時:

  • 春天跳時:02:00 不存在 → 那天沒扣
  • 秋天倒退:02:00 跑兩次 → 重複扣款

修法:

  1. 改用 UTC(無 dst)
  2. 加 idempotency key(同 user 同日只能扣一次)
  3. cron 改 03:30(避開 02-03 dst 危險區)

教訓:金融排程必避 dst 切換時段。

10. 結論 + 延伸閱讀

Cron 是 50 年屹立不搖的排程經典。掌握 5 個欄位 + 5 種特殊字元 + 跨平台對照後,幾乎所有排程需求都能寫對。下次寫 schedule 前,先到 SuperTools 線上工具驗證「下次執行時間」是否符合預期,避免半夜被告警吵醒。最後三條黃金法則:時區明確化、並發控制必加、金融與重要 job 避開 dst 切換。

延伸閱讀: