Quick Answer: Cron job drift occurs when scheduled tasks execute at skewed times due to timezone mismatches, NTP desynchronization, or system clock drift. Prevent it by: (1) explicitly setting TZ=UTC in crontab entries, (2) ensuring NTP/Chrony synchronization with sub-second accuracy, (3) using Unix timestamps instead of human-readable dates, and (4) implementing timestamp window idempotency—processing only records where created_at falls within a specific [window_start, window_end] bracket—so jobs remain safe to execute twice without duplication.
Your 2 AM batch job ran at 2:47 AM. Your 6 PM analytics didn't process yesterday's data at all. The logs show timestamps in three different timezones. This is cron job drift—and it compounds across distributed systems where one scheduler's clock skew cascades into missed data windows, duplicate processing, and silent data loss. Production systems at scale hit this relentlessly: database replicas in different regions report mismatched "last_run_time" values, Kubernetes nodes with clock skew >500ms fail TLS validation, and containerized cron jobs inherit the host's timezone without your explicit override. The root cause isn't the schedule itself—it's the interaction between system time, cron's minimal environment, and the idempotency assumptions you're making (or not making) about what happens when a job runs twice.
What Causes Cron Job Drift?
Cron job drift stems from three distinct failure modes: timezone mismatch, clock skew, and schedule window ambiguity. Each requires different diagnostics and fixes.
# Diagnosis: Check cron vs. interactive timezone output
# Interactive shell (your terminal)
date +"%Y-%m-%d %H:%M:%S %Z"
# → 2026-05-07 19:50:23 UTC
# What cron actually sees (add this to crontab for 1 minute)
* * * * * date +"%Y-%m-%d %H:%M:%S %Z" >> /tmp/cron-tz-check.log 2>&1
# Check the log after 1 minute
tail -f /tmp/cron-tz-check.log
# If output differs from interactive date, you have timezone skew
# Verify system timezone and NTP status
timedatectl status
# Expected: NTP enabled: yes, Timezone: UTC (or your intended zone)
# Offset should be near 0.000s
# Check NTP daemon synchronization
chronyc tracking | grep -E "Stratum|Last offset|RMS offset"
# Stratum should be 2-4 (lower = closer to atomic clock)
# Last offset should be < 0.001s in normal operation
Explicit Timezone Configuration in Crontab
The quickest fix: force UTC or target timezone directly in crontab without requiring system-wide changes. Cron inherits a minimal environment—no user-specific TZ variables by default—so it falls back to system locale. Override this explicitly.
# Add timezone declaration at the top of crontab
# crontab -e
TZ=UTC
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Now all timestamp operations respect UTC
*/5 * * * * /usr/local/bin/backup-job.sh
# → Will use UTC internally, even if system is set to EST
# Inside backup-job.sh, reference TZ explicitly for clarity
#!/bin/bash
set -euo pipefail
# TZ is already set from crontab, but redundancy prevents mistakes
export TZ=UTC
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# → Always outputs ISO 8601 UTC format: 2026-05-07T19:50:23Z
LOG_FILE="/var/log/backup-${TIMESTAMP%T*}.log"
# → Creates /var/log/backup-2026-05-07.log (date portion only)
echo "[${TIMESTAMP}] Backup started" >> "${LOG_FILE}"
# → Logs with full UTC timestamp for traceability
Unix Timestamp Windows: The Idempotency Pattern
The most robust approach: divide processing into discrete time windows (hourly, daily) and process only records falling within that window. This decouples the cron execution time from the data window being processed, making the job safely rerunnable.
#!/bin/bash
# process-hourly-events.sh - Idempotent batch processor
set -euo pipefail
export TZ=UTC
# Define the processing window
# End time: current hour, rounded down (idempotent anchor)
WINDOW_END=$(date -u +%s | xargs -I {} bash -c 'echo $(({} / 3600 * 3600))')
# → Unix timestamp of the start of the current hour
# → Example: 1715000400 (exactly on the hour boundary)
WINDOW_END_ISO=$(date -u -d @${WINDOW_END} +"%Y-%m-%dT%H:%M:%SZ")
# → 2026-05-07T19:00:00Z
# Start time: one hour earlier
WINDOW_START=$((WINDOW_END - 3600))
WINDOW_START_ISO=$(date -u -d @${WINDOW_START} +"%Y-%m-%dT%H:%M:%SZ")
# → 2026-05-07T18:00:00Z
CHECKPOINT_FILE="/var/lib/batch-processor/last_window.json"
# Query database: process ONLY events created in [WINDOW_START, WINDOW_END)
# This ensures:
# 1. Events are never re-processed if job reruns within same hour
# 2. Events from next hour won't leak into this processing
# 3. Job is safe to execute at 19:05, 19:15, 19:55 — always same result
QUERY=$(cat <<EOF
SELECT event_id, user_id, action, created_at
FROM events
WHERE created_at >= '${WINDOW_START_ISO}'
AND created_at < '${WINDOW_END_ISO}'
AND processed = false
ORDER BY event_id ASC;
EOF
)
# Execute query (PostgreSQL example)
psql -h localhost -U batch_user -d analytics -c "${QUERY}" | while IFS='|' read -r event_id user_id action created_at; do
# Process each event (idempotent operation)
/usr/local/bin/aggregate-event.sh "${event_id}" "${user_id}" "${action}"
# Mark as processed immediately (atomic transaction)
psql -h localhost -U batch_user -d analytics -c \
"UPDATE events SET processed = true, processed_at = NOW() WHERE event_id = ${event_id};"
done
# Record this window's completion
mkdir -p "$(dirname "${CHECKPOINT_FILE}")"
cat > "${CHECKPOINT_FILE}" <<JSON
{
"window_start": ${WINDOW_START},
"window_end": ${WINDOW_END},
"window_start_iso": "${WINDOW_START_ISO}",
"window_end_iso": "${WINDOW_END_ISO}",
"completed_at": "$(date -u -d @$(date +%s) +"%Y-%m-%dT%H:%M:%SZ")",
"status": "success"
}
JSON
echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Window ${WINDOW_START_ISO} to ${WINDOW_END_ISO} processed successfully"
exit 0
The key insight: even if this cron job runs at 19:05, 19:15, and 19:47 (three separate executions in the same hour), the database query produces identical results because it's anchored to immutable time boundaries (18:00–19:00 UTC), not cron execution time. The second and third runs find no unprocessed events and exit cleanly.
Clock Synchronization: NTP and Chrony Configuration
Drift prevention at the system level: ensure all nodes stay synchronized to within milliseconds of atomic time. Chrony (default on modern Ubuntu/RHEL) handles this better than legacy ntpd.
# Install Chrony (RHEL 8+, Ubuntu 20.04+)
sudo apt-get update && sudo apt-get install -y chrony
# or: sudo yum install -y chrony
# Configure /etc/chrony.conf for aggressive sync
sudo tee /etc/chrony.conf > /dev/null <<'EOF'
# Use multiple time servers (pool.ntp.org + Cloudflare)
server 0.pool.ntp.org iburst maxdelay 1
server 1.pool.ntp.org iburst maxdelay 1
server time.cloudflare.com iburst maxdelay 1
# makestep: correct time jumps > 1 second on first 3 sync attempts
# Prevents jobs from executing retroactively if clock jumps forward
makestep 1.0 3
# Poll more frequently in first hour after boot
minpoll 6
maxpoll 10
# Restrict default access
restrict default kod nomodify notrap nopeer noquery
restrict 127.0.0.1
restrict ::1
EOF
# Restart Chrony
sudo systemctl restart chronyd
sudo systemctl enable chronyd
# Force immediate synchronization (for testing)
sudo chronyc makestep
# → Output: "200 OK"
# Verify sync status
chronyc tracking
# Expected output sample:
# Reference ID : 0.pool.ntp.org (162.159.200.1)
# Stratum : 2
# Last offset : -0.000312 seconds
# RMS offset : 0.001 seconds
# Residual freq : -0.001 ppm
# Residual skew : 0.001 ppm
# Check per-source status
chronyc sources -v
# Sources should show all in SYNC state with low jitter
Database Idempotency: Atomic Operations and Checkpoint Pattern
Databases provide atomic row-level locking to ensure timestamp windows don't overlap. Combine this with explicit checkpoint tracking for production safety.
-- PostgreSQL idempotent update pattern
-- This ensures events are marked processed exactly once, even if cron runs twice
BEGIN TRANSACTION;
-- Lock rows matching criteria, marking them atomically
UPDATE events
SET processed = true,
processed_at = NOW(),
processed_by = 'batch-hourly-v2' -- Track job version for debugging
WHERE created_at >= '2026-05-07T18:00:00Z'
AND created_at < '2026-05-07T19:00:00Z'
AND processed = false -- Only unprocessed events
RETURNING event_id, user_id; -- Return affected rows
-- Write checkpoint atomically in same transaction
INSERT INTO batch_checkpoints (
job_name, window_start, window_end, event_count, completed_at
) VALUES (
'hourly-aggregation',
'2026-05-07T18:00:00Z'::timestamp,
'2026-05-07T19:00:00Z'::timestamp,
(SELECT COUNT(*) FROM events WHERE processed = true AND processed_at = NOW()),
NOW()
)
ON CONFLICT (job_name, window_start, window_end)
DO UPDATE SET completed_at = NOW(), event_count = EXCLUDED.event_count;
COMMIT;
Postgres's FOR UPDATE SKIP LOCKED ensures distributed cron schedulers don't collide when claiming work across multiple machines:
-- Distributed cron: multiple servers poll simultaneously
-- Only one acquires the lock and processes the window
SELECT job_id, window_start, window_end
FROM scheduled_jobs
WHERE next_run_time <= NOW()
AND status = 'pending'
FOR UPDATE SKIP LOCKED -- ← Critical: skip already-locked rows
LIMIT 1;
-- If two servers query simultaneously, one gets the row, other gets empty result
-- Result: no duplicate processing across the fleet
Unix Timestamps in Cron: Implementation Patterns
Move away from human-readable time formats in cron jobs. Unix timestamps (seconds since 1970-01-01 UTC) are unambiguous, timezone-neutral, and sortable. Use the Unix timestamp converter for quick verification.
#!/bin/bash
# Use Unix timestamps for all internal time calculations
export TZ=UTC
# Current Unix timestamp (seconds)
NOW_SECONDS=$(date +%s)
# → Example: 1715086223
# Current Unix timestamp with nanoseconds (avoid floating point)
NOW_NANOS=$(date +%s%N)
# → Example: 1715086223456789012
# Convert Unix timestamp back to human format (for logging)
READABLE=$(date -d @${NOW_SECONDS} -u +"%Y-%m-%dT%H:%M:%SZ")
# → 2026-05-07T19:30:23Z
# Define window: last hour (3600 seconds = 1 hour)
HOUR_AGO=$((NOW_SECONDS - 3600))
# → 1715082623 (one hour before NOW_SECONDS)
# Query records by Unix timestamp range (database-agnostic)
# Records where created_at is a Unix timestamp in seconds
QUERY=$(cat <<EOF
SELECT * FROM events
WHERE created_at_unix >= ${HOUR_AGO}
AND created_at_unix < ${NOW_SECONDS}
AND processed = 0;
EOF
)
# JSON logging with timestamps (Prometheus-compatible)
jq -n \
--arg ts "${NOW_SECONDS}" \
--arg event "batch_processed" \
--arg records "42" \
'{
timestamp: ($ts | tonumber),
event: $event,
records_processed: ($records | tonumber),
unix_ms: (($ts | tonumber) * 1000)
}' >> /var/log/batch-metrics.json
# Output example:
# {"timestamp":1715086223,"event":"batch_processed","records_processed":42,"unix_ms":1715086223000}
Handling Missed and Delayed Cron Executions
When cron can't run a job at the scheduled time (system reboot, high load), define policy explicitly to avoid reprocessing old data or missing new data windows.
#!/bin/bash
# Advanced cron job: handles missed executions gracefully
export TZ=UTC
CHECKPOINT_FILE="/var/lib/batch/last_success"
MAX_BACKLOG_HOURS=24 # Don't process data older than 24 hours
read -r LAST_SUCCESS < "${CHECKPOINT_FILE}" 2>/dev/null || LAST_SUCCESS=0
# → If no checkpoint exists, start from Unix epoch (process everything)
NOW=$(date +%s)
HOURS_SINCE_RUN=$(( (NOW - LAST_SUCCESS) / 3600 ))
# Prevent runaway: if offline for >24 hours, skip old windows
if [ "${HOURS_SINCE_RUN}" -gt "${MAX_BACKLOG_HOURS}" ]; then
echo "Backlog exceeds ${MAX_BACKLOG_HOURS} hours (was offline $(printf '%d hours' ${HOURS_SINCE_RUN})). Skipping to current window."
WINDOW_START=$((NOW - 3600)) # Process only last hour
else
# Process all missed windows since last successful run
WINDOW_START=${LAST_SUCCESS}
fi
WINDOW_END=$(date -d @${NOW} -u +%s | xargs -I {} bash -c 'echo $(({} / 3600 * 3600))')
# → Round current time down to nearest hour boundary
# Log the decision
echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Processing from $(date -d @${WINDOW_START} -u +"%Y-%m-%dT%H:%M:%SZ") to $(date -d @${WINDOW_END} -u +"%Y-%m-%dT%H:%M:%SZ")"
# Process all hourly windows in the range
CURRENT_WINDOW=${WINDOW_START}
while [ "${CURRENT_WINDOW}" -lt "${WINDOW_END}" ]; do
NEXT_WINDOW=$((CURRENT_WINDOW + 3600))
# Process this window idempotently
/usr/local/bin/process-window.sh "${CURRENT_WINDOW}" "${NEXT_WINDOW}" || {
echo "ERROR: Failed on window ${CURRENT_WINDOW}. Stopping to prevent silent data loss."
exit 1
}
CURRENT_WINDOW=${NEXT_WINDOW}
done
# Mark success: checkpoint points to the end of last processed window
echo "${WINDOW_END}" > "${CHECKPOINT_FILE}"
exit 0
Distributed Cron: Leader Election and Message Queues
At scale, cron jobs run on multiple servers. Use a single scheduler (leader) elected via database lock or etcd, which delegates work to a message queue. This prevents duplicate execution across the fleet.
#!/bin/bash
# Distributed cron: try to acquire leadership lock via database
export TZ=UTC
DB_HOST="postgres.internal"
DB_NAME="scheduler"
DB_USER="cron_user"
# Attempt to acquire lock via PostgreSQL advisory lock
# Advisory locks are held only for connection lifetime
LOCK_ACQUIRED=$(psql \
-h "${DB_HOST}" \
-U "${DB_USER}" \
-d "${DB_NAME}" \
-tc "SELECT pg_advisory_lock(12345); SELECT 1;" 2>/dev/null | grep -c "^[[:space:]]*1$")
if [ "${LOCK_ACQUIRED}" -eq 0 ]; then
echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Leader lock held by another scheduler. Exiting."
exit 0 # Other node is leader, silently exit
fi
# We have the lock: we're the leader
echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Acquired leader lock. Scheduling jobs."
# Query pending jobs and push to work queue
psql -h "${DB_HOST}" -U "${DB_USER}" -d "${DB_NAME}" -c \
"SELECT job_id, command, next_run_time FROM scheduled_jobs
WHERE next_run_time <= NOW() AND status = 'pending'
ORDER BY next_run_time ASC;" | \
while IFS='|' read -r job_id command next_run; do
# Push to RabbitMQ / Kafka / SQS
# (example: RabbitMQ via amqp-publish)
amqp-publish --url="amqp://guest:guest@localhost/%2F" \
--exchange="job_queue" \
--routing-key="execute" \
--message="{\"job_id\": \"${job_id}\", \"command\": \"${command}\", \"scheduled_for\": \"${next_run}\"}"
# Mark job as enqueued (not executed yet)
psql -h "${DB_HOST}" -U "${DB_USER}" -d "${DB_NAME}" -c \
"UPDATE scheduled_jobs SET status = 'enqueued', enqueued_at = NOW() WHERE job_id = '${job_id}';"
done
# Release lock automatically (psql connection closes)
exit 0
Testing Cron Jobs with Fake Time
Use faketime to simulate past/future times during development. This lets you verify idempotency and window boundaries without waiting days.
# Install faketime
sudo apt-get install -y faketime
# Test job at a specific timestamp
# Simulate it's 2026-05-07 19:50:00 UTC
faketime "2026-05-07 19:50:00 UTC" /usr/local/bin/batch-job.sh
# → Job executes as if NOW = May 7, 2026 at 19:50:00 UTC
# → WINDOW_END will round to 19:00 (same as real-time behavior)
# Test with clock skew: simulate running 3 times in same hour
for run in 1 2 3; do
echo "=== Run ${run} ==="
faketime "2026-05-07 19:0${run}:00 UTC" /usr/local/bin/batch-job.sh
done
# → All three runs process identical data (same WINDOW_START/END)
# → Idempotency verified: records not double-counted
# Test missed execution: skip 4 hours
faketime "2026-05-07 23:30:00 UTC" /usr/local/bin/batch-job.sh
# → With checkpoint logic, job processes missed windows (19:00–23:00)
Common Mistakes and How to Fix Them
Mistake: Processing by cron execution time instead of data window
# ✗ WRONG: Uses cron execution time as the data window
CUTOFF=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# If cron runs at 19:05:23, cutoff = 19:05:23
# If same job reruns at 19:47:12, cutoff = 19:47:12
# → Same job produces different results on rerun, data gets split unpredictably
sqlite3 events.db "SELECT * FROM events WHERE created_at < '${CUTOFF}' AND processed = 0;"
# ✓ RIGHT: Round execution time to window boundary
WINDOW_END=$(date -u +%s | xargs -I {} bash -c 'echo $(({} / 3600 * 3600))')
WINDOW_END_ISO=$(date -d @${WINDOW_END} -u +"%Y-%m-%dT%H:%M:%SZ")
WINDOW_START=$((WINDOW_END - 3600))
WINDOW_START_ISO=$(date -d @${WINDOW_START} -u +"%Y-%m-%dT%H:%M:%SZ")
# Process only data in this immutable window
sqlite3 events.db \
"SELECT * FROM events
WHERE created_at >= '${WINDOW_START_ISO}'
AND created_at < '${WINDOW_END_ISO}'
AND processed = 0;"
This happens because cron execution time is unpredictable (delayed by 1 second to 5 minutes depending on load). Anchoring to window boundaries ensures deterministic behavior regardless of execution delay.
Mistake: Assuming TZ variable carries from login shell to cron
# ✗ WRONG: TZ is set in .bashrc, but cron doesn't source it
# ~/.bashrc
export TZ=UTC
# crontab -e
0 2 * * * /usr/local/bin/backup.sh
# → Cron runs backup.sh in minimal environment
# → TZ=UTC from .bashrc is never loaded
# → Job uses system timezone instead (maybe EST, JST, etc.)
# ✓ RIGHT: Declare TZ at top of crontab, not in shell rc
# crontab -e
TZ=UTC
SHELL=/bin/bash
0 2 * * * /usr/local/bin/backup.sh
# → Cron explicitly sets TZ=UTC for all subsequent jobs
# → Or inline in job command:
0 2 * * * TZ=UTC /usr/local/bin/backup.sh
Cron's environment is intentionally minimal for security. It doesn't source ~/.bashrc, ~/.bash_profile, or /etc/profile. All environment variables must be declared in crontab itself.
Mistake: Comparing human-readable dates instead of Unix timestamps
# ✗ WRONG: String comparison fails across timezones and daylight saving
LAST_RUN="2026-05-07T18:30:00 EDT" # Ambiguous, timezone-dependent
CURRENT_TIME="2026-05-07T18:35:00 UTC"
if [ "2026-05-07T18:35:00 UTC" > "2026-05-07T18:30:00 EDT" ]; then
echo "Time has passed" # String comparison is unreliable here
fi
# ✓ RIGHT: Always compare Unix timestamps (unambiguous, atomic)
LAST_RUN_UNIX=1715086200 # This is absolute: May 7 2026, 18:30:00 UTC
CURRENT_UNIX=$(date +%s) # Always in UTC, always unambiguous
if [ "${CURRENT_UNIX}" -gt "${LAST_RUN_UNIX}" ]; then
echo "$(((CURRENT_UNIX - LAST_RUN_UNIX) / 60)) minutes have passed"
fi
Human-readable dates are vulnerable to timezone conversion errors, daylight saving time transitions, and string comparison bugs. Unix timestamps are immutable, universal, and sortable as integers.
Mistake: Not handling database transaction rollback in failed jobs
# ✗ WRONG: Partial processing if job crashes mid-execution
#!/bin/bash
psql -c "UPDATE events SET processed = true WHERE id < 1000;"
/usr/local/bin/send-notifications.sh # If this fails...
psql -c "INSERT INTO checkpoints (window_end) VALUES (NOW());"
# → Events are marked processed but notifications were never sent
# → Data inconsistency: job ran twice, second time finds no events to process
# ✓ RIGHT: Wrap all database changes in explicit transaction, handle errors
#!/bin/bash
set -euo pipefail # Exit on any error
psql <<EOF
BEGIN TRANSACTION;
UPDATE events SET processed = true WHERE id < 1000 RETURNING id INTO TEMP processed_ids;
EOF
if ! /usr/local/bin/send-notifications.sh; then
# Notification failed
psql <<EOF
ROLLBACK; -- ← Undo the UPDATE
EOF
echo "Notifications failed. Rolling back events." >&2
exit 1
fi
# Success: commit atomically
psql <<EOF
INSERT INTO checkpoints (window_end, status) VALUES (NOW(), 'success');
COMMIT;
EOF
If a job crashes after modifying data but before reaching the checkpoint, the next execution finds partial state. Wrap all database operations in explicit transactions and verify success before advancing the checkpoint.
Cron Scheduling Comparison Table
| Approach | Timezone Handling | Idempotency | Distributed Safety | Best For |
|---|---|---|---|---|
| Basic Cron + System TZ | Relies on system timezone (error-prone) | Not guaranteed; depends on job logic | Single server only | Simple single-server scripts |
| TZ=UTC in Crontab | Explicit UTC for all jobs | Requires database checkpoint | Still single-leader or ad-hoc | Multi-region services with explicit TZ |
| Unix Timestamp Windows + Checkpoint | Timestamps are TZ-agnostic | Guaranteed (window-based processing) | Safe with FOR UPDATE locks | Data pipelines, analytics, audit-critical systems |
| Leader-Elected Scheduler + Queue | Explicit TZ in leader job | Guaranteed (single execution point) | Fully safe (etcd/db leader election) | High-scale distributed systems (Kubernetes, multi-region) |
| Database Triggers (event-driven) | Database server TZ | Guaranteed (ACID triggers) | Fully safe (replicated database) | Real-time event processing, no schedule drift |
Frequently Asked Questions
What causes cron job drift?
Cron job drift occurs from three causes: (1) Timezone mismatch—cron inherits system timezone, which may differ from your intended zone or vary across servers, (2) NTP desynchronization—system clock skews relative to actual time, causing jobs to execute early/late or at wrong absolute times, and (3) Schedule window ambiguity—using cron execution time as the data processing window instead of immutable time boundaries, causing identical data to split across runs. Prevent drift via explicit TZ=UTC in crontab, chrony/NTP synchronization ensuring <1ms offset, and timestamp windows anchored to hour/day boundaries.
How to make cron jobs idempotent?
Make cron jobs idempotent by: (1) Anchoring to immutable time windows—process only records where created_at falls within [window_start, window_end), regardless of cron execution time, (2) Atomic database transactions—wrap all data modifications in BEGIN/COMMIT with explicit checkpoints, so rerun finds identical state, (3) Using unique idempotency keys—generate keys from time windows (e.g., "job:hour:1715000000") and skip already-processed windows, and (4) Marking records processed atomically—update a "processed" flag in the same transaction as the checkpoint. Use the cron generator to verify your schedule timing first.
What is cron skew and how to fix it?
Cron skew is when cron jobs execute at unexpected times due to system clock drift, timezone mismatches, or OS scheduling delays. Fix it by: (1) Running chrony/NTP daemon to sync system clock with atomic time servers, keeping offset <1ms, (2) Explicitly setting TZ=UTC in crontab to override system locale, (3) Monitoring clock offset with `chronyc tracking` (check RMS offset <0.005s), and (4) Using Unix timestamps instead of human-readable times—they're immune to timezone/skew issues. Use the timestamp converter to verify conversions.
How to use Unix timestamps in cron jobs?
Use Unix timestamps (seconds since 1970-01-01
Key Takeaways
- What Causes Cron Job Drift?
- Explicit Timezone Configuration in Crontab
- Unix Timestamp Windows: The Idempotency Pattern
- Clock Synchronization: NTP and Chrony Configuration
- Database Idempotency: Atomic Operations and Checkpoint Pattern