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,表達式不會上傳。
實際使用流程:
- 開 supertools.tw/dev/cron
- 貼
0 0 1 * 1看語意 - 工具警告「day + weekday 都設了,會 OR」
- 改成
0 0 1 * *,預測下次執行時間 - 「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)。
問題:
- 香港客戶(UTC+8)收信時間是 8:00(非預期 9:00)
- 台灣客戶(UTC+8)同上
- 印尼客戶(UTC+7)收信時間是 7:00
修法:
- DB 加
user.timezone欄位 - cron 改每小時跑
0 * * * *,掃描所有user.timezone對應到當地 9 點的人 - 確認 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 跑兩次 → 重複扣款
修法:
- 改用 UTC(無 dst)
- 加 idempotency key(同 user 同日只能扣一次)
- cron 改 03:30(避開 02-03 dst 危險區)
教訓:金融排程必避 dst 切換時段。
10. 結論 + 延伸閱讀
Cron 是 50 年屹立不搖的排程經典。掌握 5 個欄位 + 5 種特殊字元 + 跨平台對照後,幾乎所有排程需求都能寫對。下次寫 schedule 前,先到 SuperTools 線上工具驗證「下次執行時間」是否符合預期,避免半夜被告警吵醒。最後三條黃金法則:時區明確化、並發控制必加、金融與重要 job 避開 dst 切換。
延伸閱讀: