Epoch Time Demystified: A Developer's Guide to Unix
Category: Dev Tools
Key Takeaways (TL;DR)
- Unix epoch time (aka POSIX time) is the count of whole seconds since 1970-01-01T00:00:00Z (UTC). Leap seconds are ignored.
- One integer, no time zones, no DST shifts—excellent for machines and distributed systems.
- The 2038 problem only affects 32-bit time storage. Use 64-bit integers end-to-end to avoid overflow.
- Always store and transmit UTC. Convert to local time only at the presentation edge.
- Document your timestamp units (seconds vs milliseconds vs finer). Unit mismatches cause 1000× or 1e6× bugs.
- Prefer ISO 8601/RFC 3339 strings for human-facing fields and epoch integers for machine-facing fields.
Use Zenix Tools to convert, compare, and validate timestamps: https://www.zenixtools.com
Quick Answer: What is Unix epoch time?
Unix epoch time is a single integer counting seconds since 1970-01-01T00:00:00Z (UTC). It is time zone–neutral, DST-proof, and ideal for computation, storage, and API payloads.
Table of Contents
- What Is the Unix Epoch?
- Why Developers Rely on Epoch Time
- Units and Precision (sec vs ms vs µs vs ns)
- The Year 2038 Problem (Y2K38)
- How to Check and Mitigate 2038 Risk
- Handling Timestamps in Code (Recipes in 10+ languages)
- Time Zones, DST, and ISO 8601
- Leap Seconds and Time Smearing
- Monotonic vs Wall-Clock Time (Don’t use epoch for latency)
- API and Database Best Practices
- Testing, Validation, and Observability
- Troubleshooting Common Timestamp Bugs
- Migrating from 32-bit to 64-bit Time
- Cross-Platform Notes (Windows FILETIME, .NET ticks)
- Security and Signed Timestamps (JWT, replay prevention)
- Performance Considerations at Scale
- Handy Tools (Zenix Tools and more)
- FAQ
- Glossary
- References
- Conclusion
What Is the Unix Epoch?
The Unix Epoch begins at Thursday, January 1, 1970, 00:00:00 UTC. Unix time (also called POSIX time) measures the number of whole seconds elapsed since that instant.
- Time zone–neutral: The same value globally for the same moment.
- DST-proof: Daylight saving changes do not affect the integer.
- Format-agnostic: Format only when presenting to humans.
Important: POSIX time ignores leap seconds. Systems either ignore the extra second entirely or smear it across a window so clocks remain monotonic for practical purposes.
Examples:
- 0 → 1970-01-01T00:00:00Z
- 1609459200 → 2021-01-01T00:00:00Z
- 1704067200 → 2024-01-01T00:00:00Z
Try your own values with Zenix Tools: https://www.zenixtools.com
Why Developers Rely on Epoch Time
- Compact: A single integer is small, fast to compare, sort, serialize, hash, and index.
- Predictable: No locale, DST, or time-zone conversions in computation paths.
- Portable: Supported across languages, databases, and platforms.
- Ideal for:
- Logs and tracing
- Caches and TTLs
- Distributed systems and message ordering
- Idempotency windows and deduping
- Partitioning and time series storage
Units and Precision (sec vs ms vs µs vs ns)
Epoch time is commonly represented in different granularities:
- Seconds (s): Traditional POSIX value (e.g., 1704067200). Most UNIX APIs use this.
- Milliseconds (ms): Common in JavaScript and many front-end APIs (e.g., 1704067200000).
- Microseconds (µs) and Nanoseconds (ns): Used in high-precision logging, trading, and telemetry.
Best practices:
- Be explicit about units in field names and documentation (e.g., created_at_epoch_s, updated_at_epoch_ms).
- Convert units at the edges—don’t mix them mid-pipeline.
- When rounding from higher to lower precision, choose a deterministic rounding policy (floor is most common) and document it.
The Year 2038 Problem (Y2K38)
Historically, many systems stored epoch time in a 32-bit signed integer (max 2,147,483,647). This value corresponds to 2038-01-19T03:14:07Z. At the next tick, the counter overflows into negative numbers (interpreting time as 1901), breaking comparisons, sorting, and scheduling.
Modern platforms use 64-bit integers for time, which safely cover ranges far beyond any practical horizon (±292 billion years at 1-second resolution).
Symptoms of Y2K38 bugs:
- Future dates parse as years near 1901.
- Sorting breaks around 2038 boundaries.
- Schedulers and TTLs misfire for far-future times.
How to Check and Mitigate 2038 Risk
Checklist:
- Language/Runtime: Confirm time_t or equivalent is 64-bit on your target OS/architecture.
- Database: Ensure columns storing epoch values are BIGINT (PostgreSQL bigint, MySQL BIGINT, etc.).
- Serialization: Audit binary formats and JSON fields for 32-bit assumptions.
- Third-party SDKs: Verify dependencies (IoT/embedded SDKs are frequent culprits).
- Tests: Create automated tests with inputs beyond 2038 and around boundary values (2,147,483,647; 2,147,483,648).
Mitigations:
- Upgrade OS, compiler, runtimes, and libc that still use 32-bit time on your deployment targets.
- Migrate schemas and wire formats to 64-bit integers.
- Add validation guards rejecting timestamps outside safe ranges.
- Use feature flags and dual-write strategies when migrating storage formats.
Handling Timestamps in Code (Recipes)
Below are reliable patterns to get “now,” convert to ISO 8601/RFC 3339, and convert from epoch. All examples use UTC.
JavaScript / Node.js
// Seconds since epoch
const nowSec = Math.floor(Date.now() / 1000);
// Milliseconds since epoch (native Date uses ms)
const nowMs = Date.now();
// Seconds -> ISO 8601 (UTC)
const iso = new Date(nowSec * 1000).toISOString(); // e.g., 2024-01-01T00:00:00.000Z
// ISO -> epoch seconds
const toSec = Math.floor(new Date('2024-01-01T00:00:00Z').getTime() / 1000);
Python
import time
from datetime import datetime, timezone
now_sec = int(time.time())
now_ms = int(time.time() * 1000)
iso = datetime.fromtimestamp(now_sec, tz=timezone.utc).isoformat()
# ISO -> epoch seconds
parsed = datetime.fromisoformat('2024-01-01T00:00:00+00:00')
sec = int(parsed.timestamp())
Go
package main
import (
"fmt"
"time"
)
func main() {
nowSec := time.Now().Unix() // seconds
nowMs := time.Now().UnixMilli() // milliseconds
iso := time.Unix(nowSec, 0).UTC().Format(time.RFC3339)
// ISO -> epoch seconds
t, _ := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z")
sec := t.Unix()
fmt.Println(nowSec, nowMs, iso, sec)
}
Java
import java.time.*;
long nowSec = Instant.now().getEpochSecond();
long nowMs = Instant.now().toEpochMilli();
String iso = Instant.ofEpochSecond(nowSec).toString(); // RFC 3339/ISO 8601
long sec = Instant.parse("2024-01-01T00:00:00Z").getEpochSecond();
C# (.NET)
using System;
long nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
string iso = DateTimeOffset.FromUnixTimeSeconds(nowSec).UtcDateTime.ToString("o"); // ISO 8601
long sec = DateTimeOffset.Parse("2024-01-01T00:00:00Z").ToUnixTimeSeconds();
Rust
use chrono::{DateTime, TimeZone, Utc};
let now = Utc::now();
let now_sec = now.timestamp(); // i64 seconds
let now_ms = now.timestamp_millis();
let iso = now.to_rfc3339();
let dt: DateTime<Utc> = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc);
let sec = dt.timestamp();
Swift
import Foundation
let now = Date()
let nowSec = Int64(now.timeIntervalSince1970)
let nowMs = Int64(now.timeIntervalSince1970 * 1000)
let isoFormatter = ISO8601DateFormatter()
isoFormatter.timeZone = TimeZone(secondsFromGMT: 0)
let iso = isoFormatter.string(from: now)
let parsed = isoFormatter.date(from: "2024-01-01T00:00:00Z")!
let sec = Int64(parsed.timeIntervalSince1970)
PHP
$nowSec = time();
$nowMs = (int) round(microtime(true) * 1000);
$iso = gmdate('c', $nowSec); // ISO 8601 in UTC
$sec = strtotime('2024-01-01T00:00:00Z');
Ruby
now_sec = Time.now.to_i
now_ms = (Time.now.to_f * 1000).to_i
iso = Time.at(now_sec).utc.iso8601
sec = Time.iso8601('2024-01-01T00:00:00Z').to_i
Bash / CLI
# Seconds now (UTC)
date +%s
# Epoch -> ISO 8601 (UTC)
date -u -d @1704067200 '+%Y-%m-%dT%H:%M:%SZ' # GNU date
# macOS (brew install coreutils):
gdate -u -d @1704067200 '+%Y-%m-%dT%H:%M:%SZ'
SQL
-- PostgreSQL
SELECT EXTRACT(EPOCH FROM NOW())::bigint AS now_sec;
SELECT to_timestamp(1704067200) AT TIME ZONE 'UTC' AS as_utc;
-- MySQL/MariaDB
SELECT UNIX_TIMESTAMP() AS now_sec;
SELECT CONVERT_TZ(FROM_UNIXTIME(1704067200), '+00:00', '+00:00') AS as_utc;
Time Zones, DST, and ISO 8601
Rules:
- Store and transmit UTC. Keep servers and databases on UTC.
- Convert to local time only for display. Use IANA time zones (e.g., America/New_York), not ambiguous abbreviations (EST, CST).
- Prefer ISO 8601/RFC 3339 strings for human output—e.g., 2024-01-01T00:00:00Z or 2024-01-01T00:00:00+00:00.
- Never apply DST logic before storage; apply it only on read for presentation.
Practical patterns:
- API response:
- created_at: ISO string in UTC (e.g., 2024-01-01T00:00:00Z)
- created_at_epoch: integer in seconds (or ms) for machines
- UI formatting: Resolve the user’s IANA zone then render using a quality time library.
Leap Seconds and Time Smearing
POSIX time ignores leap seconds; UTC occasionally inserts a leap second at the end of June or December. Some providers implement time smearing: spreading the leap second over a defined window (e.g., 24 hours) so the observed rate of time is slightly adjusted rather than introducing a 23:59:60 timestamp.
Developer guidance:
- Do not manually handle leap seconds—use OS time services (NTP/chrony/systemd-timesyncd) or cloud time services.
- Don’t assume every civil day equals 86,400 seconds. Use calendar-aware libraries for date math.
- For distributed systems, choose a consistent time source/provider; mixing smeared and unsmeared sources can cause micro-skews.
Monotonic vs Wall-Clock Time (Don’t Measure Durations with Epoch)
- Wall-clock (epoch) time can move backward or forward due to sync adjustments (slew/step), leap smears, or manual changes.
- Monotonic time only increases and is ideal for measuring durations and timeouts.
Use these for durations, not epoch:
- Go: time.Since, time.Now().UnixNano() for capture but time.Since/timers use monotonic component.
- Java: System.nanoTime() (not related to wall clock).
- Python: time.monotonic(), time.perf_counter().
- Node.js: performance.now() (ms, monotonic) for intervals.
Pattern:
- Use monotonic time for elapsed durations and scheduling logic.
- Convert to epoch only when you need to persist or communicate an absolute timestamp.
API and Database Best Practices
Design principles:
- Use UTC everywhere internally. Convert to local time only on display.
- Use 64-bit integers for epoch fields (bigint). Define units: _epoch_s or _epoch_ms.
- Include both machine- and human-friendly fields in public APIs.
- Use RFC 3339/ISO 8601 strings with explicit timezone (Z or +00:00) for human-facing fields.
Database design:
- Column types: bigint for epoch fields; timestamptz for rich temporal queries in Postgres.
- Indexing: Index epoch or timestamp columns for range queries (e.g., WHERE created_at_epoch_s BETWEEN ...).
- Partitioning: Partition time series tables by day/week/month using epoch or timestamp; use CHECK constraints and pruning.
- TTL: Implement expiration as expires_at_epoch_s = created_at_epoch_s + ttl_seconds.
- Constraints: Add CHECKs for reasonable ranges (e.g., > 946684800 for year 2000 if that applies to your domain).
Serialization:
- JSON fields must clearly document units. Example:
- created_at_epoch_s: 1704067200
- created_at: 2024-01-01T00:00:00Z
- Protobuf/Avro: Prefer 64-bit integers; include annotations for units.
Observability:
- Log in UTC with both ISO strings and epoch values for easy grepping and sorting.
- Ensure all services use the same NTP source to minimize drift.
Testing, Validation, and Observability
Tests to include:
- Boundary dates: 1970-01-01, 1999-12-31, 2000-01-01, 2038-01-19, and far-future dates.
- DST transitions: Spring forward/fall back for multiple locales.
- Time math: Adding durations across DST and month/year boundaries.
- Unit conversion: ms ↔ s ↔ µs, ensure no double-scaling.
Validation patterns:
- Accept only integers for epoch fields where appropriate.
- Reject values that are clearly in the wrong unit (e.g., a 13-digit ms value in a seconds field) unless you explicitly normalize.
- Apply monotonicity checks for sequences (e.g., sorted event streams).
Observability:
- Emit metrics for clock drift, NTP sync status, and invalid timestamp counts.
- Add log annotations showing parsed timestamps and source offsets.
Use Zenix Tools to quickly sanity-check inputs and boundaries: https://www.zenixtools.com
Troubleshooting Common Timestamp Bugs
Symptoms and fixes:
- Everything appears 1000× too large or small → Units mismatch (ms used where s expected, or vice versa). Add explicit unit suffixes and validators.
- Times render in local instead of UTC → Ensure formatting uses UTC/Z and UI explicitly selects the user’s intended zone.
- Negative values or dates in 1901 → 32-bit overflow. Migrate to 64-bit.
- Drifting clocks across nodes → Unaligned NTP sources or disabled time sync in containers/VMs. Standardize NTP and monitor drift.
- Schedulers fire early/late around DST → Don’t schedule by local wall clock; schedule by absolute UTC epoch or use calendar-aware libraries.
Debug tips:
- Print raw epoch, ISO 8601 UTC, and the system’s time zone data.
- Verify library versions and system tzdata are current.
- Compare local conversion against Zenix Tools to isolate environment issues: https://www.zenixtools.com
Migrating from 32-bit to 64-bit Time
- Discover
- Search code, schemas, and protobufs/IDLs for int32 time fields.
- Inventory platforms still built for 32-bit targets.
- Plan
- Choose canonical unit (seconds or milliseconds) and document it.
- Design dual-write: write both old and new fields for a window.
- Migrate
- Schema changes: int32 → int64/bigint.
- Serialization: add a versioned field and consume both during transition.
- Backfill historical records where needed.
- Validate
- Run load tests using future dates (2038+).
- Add canary services that compare old/new paths.
- Cut over
- Remove old int32 usage and update client SDKs.
- Windows FILETIME: Count of 100-nanosecond intervals since 1601-01-01T00:00:00Z. Convert between FILETIME and Unix epoch by adding/subtracting the 369 years gap and scaling by 10,000,000.
- .NET DateTime.Ticks: 100-ns intervals since 0001-01-01T00:00:00 (Gregorian). Prefer DateTimeOffset and its ToUnixTimeSeconds/ToUnixTimeMilliseconds for reliable conversions.
- Negative epochs: Times before 1970 are represented by negative epoch values; ensure your libraries fully support them.
Security and Signed Timestamps
Timestamps are used in security-sensitive flows:
- JWT iat/nbf/exp: Always UTC epochs; clock skew handling (±30–300 seconds) is common.
- Request signing: Include x-timestamp with a signature; enforce narrow acceptance windows.
- Replay prevention: Discard stale requests using epoch-based TTL or nonce + timestamp.
Best practices:
- Validate timestamp freshness server-side.
- Document accepted skew and units in API specs.
- Log both the declared timestamp and the server receipt time.
- Storage efficiency: bigint epochs compress and index well; columnar stores benefit from monotonic sequences.
- Partitioning: Partition by time to speed up scans and lifecycle management.
- Batching: When ingesting high-volume events, use epochs for fast range merges.
- Caching: Compute cache keys that include truncated epochs (e.g., 5-min buckets) for coarser aggregation.
- Zenix Tools: Convert epoch ↔ human-readable, compare ranges, and validate inputs quickly: https://www.zenixtools.com
- NTP/chrony: Keep hosts synchronized; monitor drift and sync status.
- tzdata/IANA time zone database: Keep updated for accurate local conversions.
FAQ
Q: What exactly is Unix epoch time?
A: The number of whole seconds since 1970-01-01T00:00:00Z (UTC), ignoring leap seconds.
Q: What units should my API use—seconds or milliseconds?
A: Pick one, document it, and remain consistent. Seconds are traditional in back ends; milliseconds are common on the web. If you expose both, label clearly (created_at_epoch_s, created_at_epoch_ms).
Q: Does epoch time handle daylight saving time (DST)?
A: The epoch integer is unaffected by DST. Only the human-readable display changes when converting to local time zones.
Q: Do I need to handle leap seconds in code?
A: No. Let OS/time services handle leap seconds or smearing. Don’t hardcode leap-second logic.
Q: What is the 2038 problem?
A: Systems using 32-bit signed integers for epoch seconds overflow at 2038-01-19T03:14:07Z. Use 64-bit integers.
Q: Should I store ISO 8601 strings or epochs?
A: For internal storage and indexing, epochs (bigint) are efficient. For external APIs and logs, include both an ISO 8601 UTC string and an epoch field.
Q: Why do I see dates in 1901?
A: Likely 32-bit overflow or negative interpretation due to signedness issues.
Q: How do I convert Windows FILETIME or .NET ticks to epoch?
A: Use platform-provided methods when possible. Otherwise, shift by the epoch difference and scale (FILETIME: divide by 10,000,000 after subtracting the 1601 offset; ticks: divide by 10,000,000 after subtracting the 0001 offset), then adjust to 1970.
Q: How do I schedule recurring jobs across DST safely?
A: Prefer UTC-based schedules. If you must schedule by local wall time (e.g., 9 AM local daily), use calendar-aware libraries that handle DST transitions correctly.
Glossary
- Epoch (Unix/POSIX time): Seconds since 1970-01-01T00:00:00Z.
- UTC: Coordinated Universal Time; no DST.
- DST: Daylight Saving Time; local policy-driven shifts.
- RFC 3339/ISO 8601: Standard timestamp string formats (e.g., 2024-01-01T00:00:00Z).
- NTP: Network Time Protocol; synchronizes clocks over networks.
- Time smearing: Gradual adjustment across a window to avoid a 61-second minute during leap seconds.
- Monotonic clock: Clock that never goes backward; used for measuring durations.
- FILETIME: Windows epoch (1601) in 100-ns ticks.
- Ticks (.NET): 100-ns intervals since year 0001.
References
- POSIX Time: The Open Group Base Specifications
- IETF RFC 3339: Date and Time on the Internet: Timestamps
- ISO 8601: Date and time format standard
- IANA Time Zone Database (tzdata)
- NTP (Network Time Protocol) documentation
- Google SRE discussions on leap smear
Conclusion
Unix epoch time simplifies time for machines: one counter, one reference, no time zones. To build robust systems in 2026 and beyond:
- Use 64-bit integers for time and document your units.
- Store and transmit UTC; convert at the edges.
- Use monotonic clocks for durations, not epoch.
- Test boundary cases (2038, DST) and validate inputs.
When in doubt, verify with Zenix Tools: https://www.zenixtools.com