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
Table of Contents
- Naive vs Aware Datetimes
- Seconds vs Milliseconds
- DST Double-Fire Cron
- DST Spring-Forward Skip
- JavaScript Date.parse Ambiguity
- JavaScript Number Precision
- Y2038 Overflow
- MySQL TIMESTAMP Session TZ
- PostgreSQL timestamp vs timestamptz
- Leap Second Surprises
- ISO 8601 Variant Confusion
- Floating-Point Epoch Drift
- MongoDB Date Serialization
- Stripe Millisecond Mistake
- Slack ts Float Truncation
- Top 20 Cron Patterns
- Common Cron Mistakes
- Cron Platform Differences
1. Timezone-Naive vs Timezone-Aware Comparisons
CriticalTypeError. In other languages, it silently assumes the naive datetime is in the local timezone, which can be wrong by hours.
# 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
# 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
2. Epoch Seconds vs Milliseconds Confusion
Critical# 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!)
# 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)
3. Cron Job Fires Twice During DST Fall-Back
High# 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 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
4. Cron Job Skipped During DST Spring-Forward
High# 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 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
5. JavaScript Date.parse() Inconsistency
HighDate.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.
// 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!
// 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);
}
6. JavaScript Number Precision for Large Timestamps
MediumNumber 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.
// 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
// 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
7. Y2038: 32-Bit Timestamp Overflow
CriticalTIMESTAMP columns, 32-bit embedded systems, and legacy C code using time_t.
-- 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
-- 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
8. MySQL TIMESTAMP Session Timezone Trap
HighTIMESTAMP 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.
-- 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 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
9. PostgreSQL timestamp vs timestamptz Confusion
Hightimestamp (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.
-- 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
-- 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
10. Leap Second Surprises
Low# 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!
# 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)
11. ISO 8601 Has Too Many Valid Formats
Medium2026-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.
# 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!
# 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
12. Floating-Point Epoch Precision Drift
Mediumtime.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.
# 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 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
13. MongoDB Date JSON Serialization
Mediummongoexport 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.
// 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!
# 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)
14. Stripe Epoch: Accidentally Passing Milliseconds
HighDate.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.
// 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
// 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");
}
15. Slack ts: Float Conversion Corrupts Message IDs
Hights 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".
# 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"!
# 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" });
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 minute | Health checks, queue workers |
| 2 | */5 * * * * | Every 5 minutes | Monitoring, metrics collection |
| 3 | */15 * * * * | Every 15 minutes | Cache invalidation, sync jobs |
| 4 | 0 * * * * | Every hour (at :00) | Log rotation, hourly reports |
| 5 | */30 * * * * | Every 30 minutes | Data pipeline runs |
| 6 | 0 0 * * * | Daily at midnight | Database backups, cleanup |
| 7 | 0 6 * * * | Daily at 6:00 AM | Morning report generation |
| 8 | 30 2 * * * | Daily at 2:30 AM | Off-peak batch processing |
| 9 | 0 9 * * 1-5 | Weekdays at 9:00 AM | Business-hours notifications |
| 10 | 0 */6 * * * | Every 6 hours | Certificate renewal checks |
| 11 | 0 0 * * 0 | Weekly on Sunday at midnight | Weekly digest emails |
| 12 | 0 0 1 * * | Monthly on the 1st at midnight | Monthly billing, invoicing |
| 13 | 0 0 1 1 * | Yearly on January 1st | Annual cert rotation, license check |
| 14 | 0 */2 * * * | Every 2 hours | Sitemap regeneration |
| 15 | 0 12 * * * | Daily at noon | Midday status reports |
| 16 | 0 0 * * 1 | Weekly on Monday at midnight | Start-of-week processing |
| 17 | 0 0 15 * * | Monthly on the 15th | Mid-month payroll processing |
| 18 | 0 */4 * * * | Every 4 hours | DNS record updates |
| 19 | */10 * * * * | Every 10 minutes | Lightweight status polling |
| 20 | 0 0 * * 6,0 | Weekends at midnight | Weekend 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].