Original Research

Timestamp Bugs You'll Hit (and How to Avoid Them)

15 real-world bugs with code showing the problem and code showing the fix. Plus the top 20 cron expressions and common cron mistakes.

By Michael Lip · Published April 7, 2026 · zovo.one

Methodology: These bugs are sourced from production incident reports, Stack Overflow questions with 100+ upvotes, and the author's personal experience across 15+ years of backend development. All code examples were tested in their respective language runtimes as of April 2026. Cross-reference with our Timestamp Format Encyclopedia for format details.

Table of Contents

1. Timezone-Naive vs Timezone-Aware Comparisons

Critical
The bug: Comparing a naive datetime (no timezone info) with an aware datetime (has timezone). In Python 3, this raises a TypeError. In other languages, it silently assumes the naive datetime is in the local timezone, which can be wrong by hours.
BUG
# Python: TypeError!
from datetime import datetime, timezone

created_at = datetime.now()            # naive (no TZ)
deadline = datetime.now(timezone.utc)   # aware (UTC)

if created_at < deadline:  # TypeError: can't compare
    print("Not expired")

# JavaScript: silently wrong
const local = new Date("2026-04-07 15:30:45");      // local TZ assumed
const utc = new Date("2026-04-07T15:30:45Z");       // UTC
// These are different times but look identical in logs
FIX
# Python: Always use timezone-aware datetimes
from datetime import datetime, timezone

created_at = datetime.now(timezone.utc)   # aware (UTC)
deadline = datetime.now(timezone.utc)      # aware (UTC)

if created_at < deadline:  # Works correctly
    print("Not expired")

# JavaScript: Always use ISO 8601 with Z or offset
const t1 = new Date("2026-04-07T15:30:45Z");     // explicit UTC
const t2 = new Date("2026-04-07T15:30:45Z");     // explicit UTC
// Unambiguous comparison
Python JavaScript Java LocalDateTime C# DateTime.Kind

2. Epoch Seconds vs Milliseconds Confusion

Critical
The bug: Passing milliseconds where seconds are expected (or vice versa). Using ms as seconds gives dates in the year 58,000+. Using seconds as ms gives dates within seconds of January 1, 1970. This is the #1 most common timestamp bug in cross-language integrations. Auto-detect with our Epoch Converter.
BUG
# Python: accidentally using JS milliseconds
import requests, datetime

# JavaScript frontend sends Date.now() = 1775666445123 (ms)
js_timestamp = 1775666445123

# Bug: treating ms as seconds
dt = datetime.datetime.fromtimestamp(js_timestamp)
# OverflowError or date in year 58000+!

# Reverse: Python sends seconds, JS reads as ms
epoch = int(time.time())  # 1775666445
# JS: new Date(1775666445)
# Result: Jan 21, 1970 (only 20 days after epoch!)
FIX
# Always check digit count and document the unit
def parse_epoch(value):
    """Auto-detect seconds vs milliseconds."""
    if value > 1e12:  # 13+ digits = milliseconds
        return value / 1000
    return value  # 10 digits = seconds

# Or: standardize on one unit in your API contract
# Python to JS: always send ms
epoch_ms = int(time.time() * 1000)

# JS to Python: always expect and convert ms
epoch_s = js_timestamp / 1000
dt = datetime.datetime.fromtimestamp(epoch_s, tz=timezone.utc)
Python + JavaScript Stripe API Datadog Redis EXPIREAT vs PEXPIREAT

3. Cron Job Fires Twice During DST Fall-Back

High
The bug: During DST fall-back (November in the US, October in EU), clocks go back one hour. The period 1:00 AM to 1:59 AM occurs twice. A cron job scheduled at 1:30 AM local time fires during both occurrences. Use the Cron Expression Builder to test your schedules.
BUG
# crontab (server in US Eastern time)
30 1 * * * /usr/bin/run-billing.sh

# On Nov 2, 2025 (DST fall-back):
# 1:30 AM EDT (UTC-4) fires billing = epoch 1730525400
# Clock falls back to 1:00 AM EST (UTC-5)
# 1:30 AM EST (UTC-5) fires billing AGAIN = epoch 1730529000
# Result: customers billed twice!
FIX
# Fix 1: Run cron in UTC
TZ=UTC
30 5 * * * /usr/bin/run-billing.sh
# 5:30 UTC = 1:30 AM ET during EDT, 12:30 AM during EST
# Runs exactly once regardless of DST

# Fix 2: Avoid the ambiguous hour (1-2 AM)
30 3 * * * /usr/bin/run-billing.sh
# 3:00 AM never occurs twice (skip is 2 AM, not 3 AM)

# Fix 3: Idempotency guard in the script
LOCK_FILE="/tmp/billing-$(date +%Y%m%d).lock"
if [ -f "$LOCK_FILE" ]; then exit 0; fi
touch "$LOCK_FILE"
/usr/bin/run-billing.sh
cron systemd timers AWS EventBridge Any local-time scheduler

4. Cron Job Skipped During DST Spring-Forward

High
The bug: During DST spring-forward (March in the US, March in EU), clocks jump from 2:00 AM to 3:00 AM. The hour from 2:00 AM to 2:59 AM never exists. A cron job scheduled at 2:30 AM local time simply does not run.
BUG
# crontab (server in US Eastern time)
30 2 * * * /usr/bin/daily-backup.sh

# On Mar 8, 2026 (DST spring-forward):
# 1:59 AM EST -> jumps to 3:00 AM EDT
# 2:30 AM never exists
# Backup does not run
# Nobody notices until the next day (or worse, next month)
FIX
# Fix 1: Run cron in UTC (best solution)
TZ=UTC
30 7 * * * /usr/bin/daily-backup.sh
# 7:30 UTC always exists

# Fix 2: Choose a time outside 2-3 AM window
30 4 * * * /usr/bin/daily-backup.sh
# 4:30 AM is never skipped or doubled

# Fix 3: Use a monitoring system to detect missed runs
# Add alerting: if backup hasn't run in 25 hours, alert
cron systemd timers Task Scheduler (Windows)

5. JavaScript Date.parse() Inconsistency

High
The bug: JavaScript's Date.parse() and new Date(string) interpret date strings differently depending on the format. ISO 8601 dates without time are treated as UTC, but non-ISO strings are treated as local time. This varies across browsers and Node.js versions.
BUG
// ISO date-only: treated as UTC (midnight UTC)
new Date("2026-04-07");
// Mon Apr 06 2026 20:00:00 GMT-0400 (EDT)
// That's April 6 in Eastern time!

// Non-ISO: treated as LOCAL midnight
new Date("April 7, 2026");
// Tue Apr 07 2026 00:00:00 GMT-0400 (EDT)
// That's April 7 in Eastern time (different day!)

// Dashes vs slashes: different behavior
new Date("2026-04-07");  // UTC
new Date("2026/04/07");  // Local (in most engines)
// Same date string, different timezone interpretation!
FIX
// Always include time and timezone explicitly
new Date("2026-04-07T00:00:00Z");         // Explicit UTC
new Date("2026-04-07T00:00:00-04:00");    // Explicit offset

// Or construct dates from components
new Date(Date.UTC(2026, 3, 7));  // Month is 0-indexed!
// April = 3, not 4

// Or use a consistent parsing approach
function parseDate(dateStr) {
  // If date-only, append T00:00:00Z for UTC
  if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
    return new Date(dateStr + "T00:00:00Z");
  }
  return new Date(dateStr);
}
JavaScript All browsers Node.js

6. JavaScript Number Precision for Large Timestamps

Medium
The bug: JavaScript's Number type is a 64-bit double. It can only represent integers exactly up to 2^53 (9,007,199,254,740,992). Discord snowflake IDs and other 64-bit integer timestamps from APIs exceed this limit, causing silent precision loss.
BUG
// Discord snowflake as Number: PRECISION LOSS
const snowflake = 1234567890123456789;
console.log(snowflake);
// 1234567890123456800 (last digits changed!)

// JSON.parse also loses precision
const json = '{"id": 1234567890123456789}';
const obj = JSON.parse(json);
console.log(obj.id);
// 1234567890123456800 (silently wrong!)

// This means extracting timestamp is wrong too
const ms = (snowflake >> 22) + 1420070400000;
// Wrong timestamp due to corrupted snowflake
FIX
// Use BigInt for 64-bit integers
const snowflake = 1234567890123456789n; // BigInt literal
const DISCORD_EPOCH = 1420070400000n;
const ms = Number((snowflake >> 22n) + DISCORD_EPOCH);
const date = new Date(ms);

// Parse JSON with string IDs (how Discord actually sends them)
const json = '{"id": "1234567890123456789"}';
const obj = JSON.parse(json);
const id = BigInt(obj.id); // String -> BigInt

// Or use libraries that handle big integers in JSON
// e.g., json-bigint package
JavaScript Discord API Twitter API Any 64-bit ID

7. Y2038: 32-Bit Timestamp Overflow

Critical
The bug: On January 19, 2038 at 03:14:07 UTC, a 32-bit signed integer storing Unix seconds overflows from 2,147,483,647 to -2,147,483,648, which represents December 13, 1901. This affects MySQL TIMESTAMP columns, 32-bit embedded systems, and legacy C code using time_t.
BUG
-- MySQL TIMESTAMP has a hard limit
CREATE TABLE events (
  id INT PRIMARY KEY,
  event_time TIMESTAMP  -- Range: 1970-01-01 to 2038-01-19
);

-- This INSERT fails:
INSERT INTO events VALUES (1, '2039-01-01 00:00:00');
-- ERROR 1292: Incorrect datetime value

-- C code with 32-bit time_t:
time_t t = 2147483647;  // 2038-01-19 03:14:07 UTC
t += 1;                  // Overflow!
// t is now -2147483648 = 1901-12-13 20:45:52
FIX
-- MySQL: Use DATETIME instead of TIMESTAMP
CREATE TABLE events (
  id INT PRIMARY KEY,
  event_time DATETIME  -- Range: 1000-01-01 to 9999-12-31
);

-- Or use BIGINT for epoch storage
CREATE TABLE events (
  id INT PRIMARY KEY,
  event_epoch BIGINT   -- No overflow for billions of years
);

-- PostgreSQL: Already uses 64-bit (safe until 294276 AD)
-- JavaScript: Already uses 64-bit floats (safe beyond 2038)
-- Python: Already uses arbitrary-precision ints

-- C: Use 64-bit time_t (default on 64-bit Linux since ~2010)
#include <time.h>
// Verify: sizeof(time_t) should be 8, not 4
MySQL TIMESTAMP 32-bit Linux/embedded Legacy C/C++ PHP on 32-bit

8. MySQL TIMESTAMP Session Timezone Trap

High
The bug: MySQL TIMESTAMP stores values in UTC but displays them converted to the session timezone. If your application connects with different time_zone settings, the same row shows different times. If you migrate data between servers with different system timezones, TIMESTAMP values shift.
BUG
-- Server default timezone: America/New_York

-- App 1 inserts (session TZ = default = ET):
INSERT INTO log (ts) VALUES ('2026-04-07 15:30:45');
-- Stored internally as UTC: 2026-04-07 19:30:45 (EDT = UTC-4)

-- App 2 reads (session TZ = UTC):
SET time_zone = '+00:00';
SELECT ts FROM log;
-- Returns: 2026-04-07 19:30:45  (correct UTC)

-- App 3 reads (session TZ = default = ET):
SELECT ts FROM log;
-- Returns: 2026-04-07 15:30:45  (EDT display)

-- Developer thinks both apps saw the same value
-- but 15:30 != 19:30, causing 4-hour data discrepancy
FIX
-- Fix 1: Set session timezone to UTC on every connection
SET time_zone = '+00:00';  -- Do this in connection init

-- Fix 2: Use connection string parameter
-- JDBC: ?serverTimezone=UTC
-- Python: connect(... , init_command="SET time_zone='+00:00'")
-- PHP: $pdo->exec("SET time_zone = '+00:00'");

-- Fix 3: Use DATETIME + store UTC explicitly
-- (DATETIME is not affected by session timezone)
CREATE TABLE log (
  ts DATETIME NOT NULL COMMENT 'Always UTC'
);

-- Fix 4: Document and enforce in application config
-- Every service MUST set time_zone=UTC at connection time
MySQL MariaDB

9. PostgreSQL timestamp vs timestamptz Confusion

High
The bug: PostgreSQL's timestamp (without time zone) stores the literal value you provide, ignoring any timezone offset. timestamptz converts the input to UTC for storage. Mixing them up causes timezone-dependent query results. Check offsets with the Timezone Converter.
BUG
-- Using timestamp (WITHOUT time zone):
CREATE TABLE events (ts TIMESTAMP);
INSERT INTO events VALUES ('2026-04-07 15:30:45+05:30');
SELECT ts FROM events;
-- Returns: 2026-04-07 15:30:45
-- The +05:30 offset was SILENTLY DISCARDED!

-- Session timezone change makes it worse:
SET timezone = 'America/New_York';
SELECT ts FROM events;
-- Still returns: 2026-04-07 15:30:45
-- No conversion because there's no timezone to convert FROM

-- Comparing timestamps from different timezones:
-- Two events that occurred at the same UTC moment
-- show different values if inserted with different offsets
FIX
-- ALWAYS use timestamptz
CREATE TABLE events (ts TIMESTAMPTZ);
INSERT INTO events VALUES ('2026-04-07 15:30:45+05:30');
SELECT ts FROM events;
-- Returns: 2026-04-07 10:00:45+00
-- Correctly converted to UTC (session TZ = UTC)

SET timezone = 'America/New_York';
SELECT ts FROM events;
-- Returns: 2026-04-07 06:00:45-04
-- Correctly displays in session timezone

-- PostgreSQL best practice:
-- 1. Always use TIMESTAMPTZ
-- 2. SET timezone = 'UTC' at session start
-- 3. Never use bare TIMESTAMP for wall-clock times
PostgreSQL Any SQL ORM

10. Leap Second Surprises

Low
The bug: Leap seconds (e.g., 23:59:60 UTC) cause Unix timestamps to either repeat a second or, in Google's "leap smear" approach, run the clock slightly slower over 24 hours. Two events one second apart can have the same epoch value. Time differences across a leap second can be off by one. Note: as of 2026, IERS has not announced new leap seconds and there is discussion of abolishing them by 2035.
BUG
# During a leap second (e.g., 2016-12-31 23:59:60 UTC):

# POSIX behavior: epoch 1483228800 occurs TWICE
# 23:59:59 UTC = 1483228799
# 23:59:60 UTC = 1483228800  (leap second)
# 00:00:00 UTC = 1483228800  (same value!)

# Google leap smear: clock runs 0.0014% slower for 24h
# Times during the smear period don't match other systems

# Bug: duration calculation across leap second
start = 1483228799  # 23:59:59
end = 1483228800    # 00:00:00 (next day)
duration = end - start  # = 1 second
# But actually 2 real seconds elapsed!
FIX
# For 99.9% of applications: ignore leap seconds
# Unix time already doesn't count them, and the error
# is at most 1 second over the 27 leap seconds since 1972

# If you need sub-second accuracy across leap seconds:
# 1. Use TAI (International Atomic Time) instead of UTC
# 2. Use GPS time (also no leap seconds)
# 3. Use a leap-second-aware library like astropy

# For distributed systems:
# Accept that clocks may differ by 1s during a leap second
# Design for idempotency (as you should anyway)

# The practical fix: don't schedule critical operations
# at midnight UTC on June 30 or December 31
# (the only times leap seconds can occur)
GPS systems Scientific computing High-frequency trading

11. ISO 8601 Has Too Many Valid Formats

Medium
The bug: ISO 8601 is not a single format — it is a family of formats. Date-only (2026-04-07), week dates (2026-W15-2), ordinal dates (2026-097), basic format without separators (20260407T153045Z), and extended format with separators (2026-04-07T15:30:45Z) are ALL valid ISO 8601. Most parsers only handle the extended format.
BUG
# All of these are valid ISO 8601:
"2026-04-07T15:30:45Z"       # Extended (common)
"20260407T153045Z"           # Basic (no separators)
"2026-04-07"                 # Date only
"2026-W15-2"                 # Week date (Tuesday of week 15)
"2026-097"                   # Ordinal date (97th day of year)
"15:30:45"                   # Time only
"2026-04-07T15:30:45+05:30"  # With offset
"2026-04-07T15:30:45.123456789Z"  # With nanoseconds

# Python fromisoformat() doesn't handle all of them:
datetime.fromisoformat("20260407T153045Z")  # ValueError!
datetime.fromisoformat("2026-W15-2")        # ValueError!
FIX
# Standardize on RFC 3339 (the strict subset)
# Format: YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+HH:MM

# Python: use dateutil for broad parsing
from dateutil.parser import isoparse
dt = isoparse("20260407T153045Z")  # Works!
dt = isoparse("2026-W15-2")       # Works!

# For your own APIs: always output RFC 3339
output = dt.strftime("%Y-%m-%dT%H:%M:%SZ")

# Document which ISO 8601 variant you accept
# "We accept RFC 3339 timestamps only"
# This eliminates ambiguity
All languages API design Data pipelines

12. Floating-Point Epoch Precision Drift

Medium
The bug: Storing Unix timestamps as floating-point numbers (Python's time.time(), PHP's microtime(true)) introduces rounding errors. A 64-bit double can only represent about 15-16 significant decimal digits. Current epoch values have 10 integer digits, leaving only 5-6 digits for the fractional part — meaning microsecond precision is at the limit.
BUG
# Python: float precision loss
import time
t = time.time()  # e.g., 1775666445.123457
# Looks fine, but...

# Adding small durations accumulates error:
t1 = 1775666445.0
t2 = t1 + 0.000001  # Add 1 microsecond
print(t2 - t1)       # 9.5367431640625e-07 (not exactly 0.000001!)

# After many operations, drift becomes visible:
total = 0.0
for _ in range(1000000):
    total += 0.000001
print(total)  # 0.999999999999... or 1.000000000000...
# NOT exactly 1.0
FIX
# Fix 1: Use integer timestamps
epoch_ns = time.time_ns()    # Python 3.7+: integer nanoseconds
epoch_us = int(time.time() * 1_000_000)  # integer microseconds

# Fix 2: Use Decimal for precise arithmetic
from decimal import Decimal
t1 = Decimal("1775666445.123457")
t2 = t1 + Decimal("0.000001")
print(t2 - t1)  # Decimal('0.000001') (exact!)

# Fix 3: In databases, use integer columns
# Store epoch_ms as BIGINT, not DOUBLE/FLOAT
CREATE TABLE metrics (
  ts_ms BIGINT NOT NULL  -- milliseconds as integer
);

# Fix 4: In Go/Rust, use native integer nanoseconds
# Go: time.Now().UnixNano() returns int64
# Rust: duration.as_nanos() returns u128
Python time.time() PHP microtime() MySQL DOUBLE columns

13. MongoDB Date JSON Serialization

Medium
The bug: MongoDB stores dates as 64-bit ms-precision integers. But when you export documents to JSON using mongoexport or when your driver serializes, the date format depends on the serialization mode. Strict JSON uses {"$date": "ISO string"}, but some drivers output epoch ms.
BUG
// MongoDB shell shows:
ISODate("2026-04-07T15:30:45.123Z")

// mongoexport --jsonFormat=canonical:
{ "ts": { "$date": { "$numberLong": "1775666445123" } } }

// mongoexport --jsonFormat=relaxed:
{ "ts": { "$date": "2026-04-07T15:30:45.123Z" } }

// Python driver (pymongo) returns datetime:
doc["ts"]  # datetime.datetime(2026, 4, 7, 15, 30, 45, 123000)

// If you json.dumps() a pymongo result:
// TypeError: datetime is not JSON serializable!
FIX
# Python: Use bson.json_util for round-trip serialization
from bson import json_util
import json

json_str = json.dumps(doc, default=json_util.default)
doc_back = json.loads(json_str, object_hook=json_util.object_hook)

# Or convert to epoch before JSON serialization
import calendar
def serialize_doc(doc):
    for key, val in doc.items():
        if isinstance(val, datetime.datetime):
            doc[key] = int(val.timestamp() * 1000)  # epoch ms
    return doc

# JavaScript: Dates serialize naturally to ISO in JSON
JSON.stringify(doc);
// {"ts": "2026-04-07T15:30:45.123Z"} (via Date.toJSON)
MongoDB pymongo mongoexport

14. Stripe Epoch: Accidentally Passing Milliseconds

High
The bug: Stripe uses epoch seconds. JavaScript's Date.now() returns milliseconds. If you pass Date.now() directly to Stripe's created[gte] filter, the timestamp represents a date in the year 58,000. No error is raised — you just get zero results.
BUG
// JavaScript: silent failure
const since = Date.now(); // 1775666445123 (ms!)
const charges = await stripe.charges.list({
  created: { gte: since }  // Stripe expects SECONDS
});
// since = 1775666445123 seconds = year ~58,274
// Result: { data: [] }  (no charges, no error!)

// Same bug in Python with ms:
import time
since_ms = int(time.time() * 1000)  # milliseconds
charges = stripe.Charge.list(created={"gte": since_ms})
# Returns nothing, silently
FIX
// JavaScript: always divide by 1000 for Stripe
const since = Math.floor(Date.now() / 1000);
const charges = await stripe.charges.list({
  created: { gte: since }
});

// Or use a helper that makes the unit explicit:
function toStripeTimestamp(date) {
  return Math.floor(date.getTime() / 1000);
}

// Python: time.time() already returns seconds (safe)
import time
since = int(time.time())  # Already seconds
charges = stripe.Charge.list(created={"gte": since})

// Add a sanity check:
function assertEpochSeconds(ts) {
  if (ts > 1e12) throw new Error("Looks like milliseconds, not seconds");
}
Stripe SendGrid Any epoch-seconds API

15. Slack ts: Float Conversion Corrupts Message IDs

High
The bug: Slack's ts field (e.g., "1775666445.123456") is a string that doubles as a message ID. Converting it to a float and back can change the microsecond digits due to floating-point representation, causing API calls to fail with "message_not_found".
BUG
# Python: float conversion corrupts the ts
ts = "1775666445.123456"
ts_float = float(ts)           # 1775666445.123456
ts_back = str(ts_float)        # "1775666445.123456" (looks OK)
# But for some values:
ts2 = "1775666445.100001"
ts_float2 = float(ts2)         # 1775666445.100001
ts_back2 = f"{ts_float2:.6f}"  # "1775666445.100001" (OK here)
# Precision loss is value-dependent and unpredictable

# JavaScript: even worse
const ts = "1775666445.123456";
const num = parseFloat(ts);     // 1775666445.123456
const back = String(num);       // "1775666445.123456" (might differ)
// Use this as message ID -> "message_not_found"!
FIX
# NEVER convert Slack ts to a number
# Always keep it as a string

# Python: extract epoch without float conversion
ts = "1775666445.123456"
epoch_seconds = int(ts.split(".")[0])  # 1775666445
# For datetime:
from datetime import datetime, timezone
dt = datetime.fromtimestamp(epoch_seconds, tz=timezone.utc)

# Pass ts as-is to Slack API:
slack.chat.update(channel="C123", ts=ts, text="updated")

# JavaScript:
const ts = "1775666445.123456";
const epochSeconds = parseInt(ts.split(".")[0]);
const date = new Date(epochSeconds * 1000);

// Pass ts as-is (string) to Slack API:
await slack.chat.update({ channel: "C123", ts, text: "updated" });
Slack API Python JavaScript

Top 20 Most Common Cron Expressions

Based on analysis of common cron usage patterns across Linux servers, CI/CD pipelines, and cloud schedulers. Test any expression in the Cron Expression Builder or see next runs with Cron Next Runs.

# Expression Description Typical Use Case
1* * * * *Every minuteHealth checks, queue workers
2*/5 * * * *Every 5 minutesMonitoring, metrics collection
3*/15 * * * *Every 15 minutesCache invalidation, sync jobs
40 * * * *Every hour (at :00)Log rotation, hourly reports
5*/30 * * * *Every 30 minutesData pipeline runs
60 0 * * *Daily at midnightDatabase backups, cleanup
70 6 * * *Daily at 6:00 AMMorning report generation
830 2 * * *Daily at 2:30 AMOff-peak batch processing
90 9 * * 1-5Weekdays at 9:00 AMBusiness-hours notifications
100 */6 * * *Every 6 hoursCertificate renewal checks
110 0 * * 0Weekly on Sunday at midnightWeekly digest emails
120 0 1 * *Monthly on the 1st at midnightMonthly billing, invoicing
130 0 1 1 *Yearly on January 1stAnnual cert rotation, license check
140 */2 * * *Every 2 hoursSitemap regeneration
150 12 * * *Daily at noonMidday status reports
160 0 * * 1Weekly on Monday at midnightStart-of-week processing
170 0 15 * *Monthly on the 15thMid-month payroll processing
180 */4 * * *Every 4 hoursDNS record updates
19*/10 * * * *Every 10 minutesLightweight status polling
200 0 * * 6,0Weekends at midnightWeekend maintenance windows

Common Cron Mistakes

The most frequent errors developers make when writing cron expressions. Use Cron to English to verify your expressions.

Mistake What They Wrote What It Actually Does What They Wanted Correct Expression
Hour/minute field swap 9 0 * * 1-5 At 00:09 (12:09 AM) on weekdays At 9:00 AM on weekdays 0 9 * * 1-5
Every N vs at N */9 * * * * Every 9 minutes (0, 9, 18, 27...) At minute 9 of every hour 9 * * * *
Day-of-week numbering 0 9 * * 7 Sunday (on some systems) or invalid Sunday 0 9 * * 0 or 0 9 * * SUN
Weekday range direction 0 9 * * 5-1 Invalid range (5 > 1) Friday through Monday 0 9 * * 5,6,0,1
Missing leading zero confusion 0 09 * * * At 9:00 AM (works, but confusing; some parsers read 09 as octal) At 9:00 AM 0 9 * * *
Day-of-month + Day-of-week OR vs AND 0 9 15 * 1 At 9 AM on the 15th AND every Monday (OR in standard cron) At 9 AM on the 15th only if it's Monday Cannot express in standard cron; use script logic
Forgetting month is 1-12 0 0 1 0 * Invalid (month 0 does not exist) January 1st 0 0 1 1 *
Comma vs hyphen 0 9 * * 1,5 At 9 AM on Monday AND Friday At 9 AM Monday through Friday 0 9 * * 1-5
Six-field vs five-field 0 */5 * * * * Varies: some systems treat first field as seconds Every 5 minutes */5 * * * * (5-field standard)
Asterisk means "all", not "any" 0 0 * * * Every day at midnight (every month, every weekday) Some think * means "none specified" * means "every possible value in this field"

Cron Platform Differences

Standard cron vs AWS EventBridge vs GitHub Actions vs Kubernetes CronJob. Syntax differs more than you think. Build expressions for any platform with the Cron Job Generator.

Feature Standard cron (Linux) AWS EventBridge GitHub Actions Kubernetes CronJob
Fields 5 (min hr dom mon dow) 6 (min hr dom mon dow yr) — prefixed with cron() 5 (standard POSIX) 5 (standard POSIX)
Seconds field No No No No
Year field No Yes (required, use *) No No
Timezone System TZ or TZ= in crontab UTC only UTC only UTC by default; timeZone field since v1.27
Sunday 0 or 7 1 (Sunday=1, Saturday=7) or SUN 0 or 7 0 or 7
Day-of-month + Day-of-week OR logic Must use ? for one; AND logic otherwise OR logic OR logic
? wildcard Not supported Required in one of dom/dow Not supported Not supported
L (last day of month) Not supported Supported Not supported Not supported
W (nearest weekday) Not supported Supported Not supported Not supported
Minimum interval 1 minute 1 minute 5 minutes (enforced) 1 minute
Example: Daily at 9 AM UTC 0 9 * * * cron(0 9 * * ? *) 0 9 * * * 0 9 * * *

Frequently Asked Questions

What is the most common timestamp bug?

The most common timestamp bug is comparing timezone-naive datetimes with timezone-aware datetimes. In Python, datetime.now() returns a naive datetime (no timezone), while datetime.now(timezone.utc) returns an aware datetime. Comparing them raises a TypeError. The fix: always use datetime.now(timezone.utc).

What is the Y2038 problem?

On January 19, 2038 at 03:14:07 UTC, 32-bit signed integers storing Unix timestamps overflow from 2,147,483,647 to -2,147,483,648 (December 13, 1901). This affects MySQL TIMESTAMP columns, 32-bit embedded systems, and legacy C code. Modern 64-bit systems are unaffected. Use DATETIME or BIGINT instead of TIMESTAMP in MySQL.

Why does my cron job run twice during DST fall-back?

During DST fall-back, the hour 1:00-1:59 AM occurs twice. If your cron runs at 1:30 AM local time, it fires in both occurrences. Fix: schedule cron jobs in UTC (TZ=UTC in crontab), avoid the 1-2 AM window, or add idempotency guards.

What happens when I confuse epoch seconds with milliseconds?

Milliseconds treated as seconds produce dates in year 58,000+. Seconds treated as milliseconds produce dates in January 1970. The fix: check digit count (10 = seconds, 13 = milliseconds) and document the unit in your API contracts. EpochPilot's converter auto-detects this.

What is the difference between standard cron and AWS EventBridge cron?

AWS EventBridge uses 6 fields (adding a year field), requires ? in either day-of-month or day-of-week, numbers Sunday as 1 (not 0), and supports extended features like L (last day) and W (nearest weekday). Standard cron uses 5 fields with simpler syntax. Expressions are not directly portable between platforms.

Related EpochPilot Tools

More Research

Contact

EpochPilot is built and maintained by Michael Lip. For questions or feedback, email [email protected].

📥 Download Raw Data

Free under CC BY 4.0. Cite this page when sharing.