Why time still breaks software
Time-related bugs are common, often subtle, and can cause data corruption, scheduling failures, analytics errors, and flaky tests. Problems usually come from three root causes: mixing timezones or naive/aware datetimes, using wall-clock time to measure intervals, and poor serialization/round-tripping across services. The guidance below focuses on practical, repeatable patterns you can apply immediately.
Core rules
- Store timestamps in UTC. Persist a single canonical instant in UTC; convert to local time only for display.
- Use timezone-aware datetimes. In languages that distinguish naive vs aware datetimes, prefer aware datetimes (or attach explicit offset info).
- Use monotonic clocks for intervals. For timeouts, measuring elapsed time, and rate-limiting, use a monotonic clock that isn’t affected by system clock updates.
- Serialize as ISO 8601 with offset (or Z for UTC). ISO 8601 strings are compact, human-readable, and widely supported by parsers and databases.
- Keep timezone metadata when needed. If a scheduled event depends on a local timezone (calendar-based reminders), store the local timezone identifier alongside the UTC instant.
Practical examples
Below are small, copy-pasteable patterns for common tasks: canonical storage, elapsed-time measurement, and local-time display.
JavaScript — canonical UTC storage and measuring elapsed time
const now = new Date();
// Canonical storage format (UTC / ISO 8601)
const iso = now.toISOString(); // e.g. "2026-05-10T12:34:56.789Z"
// Parse back into a Date (always treated as UTC if ISO has Z or offset)
const parsed = new Date(iso);
// Measure elapsed time: use a monotonic API
// Browser: performance.now(); Node: process.hrtime.bigint()
function elapsedMillis(start) {
return performance.now() - start;
}
const start = performance.now();
// ... work ...
console.log(`Elapsed ms: ${elapsedMillis(start)}`);Notes: Date.toISOString() always emits UTC (Z). For scheduling or human-facing formatting, convert using Intl.DateTimeFormat or a timezone-aware library.
Python — timezone-aware datetimes and monotonic timing
from datetime import datetime
from zoneinfo import ZoneInfo
import time
# Create an aware UTC timestamp
now_utc = datetime.now(tz=ZoneInfo("UTC"))
iso = now_utc.isoformat() # e.g. '2026-05-10T12:34:56.789000+00:00'
# Parse back (fromisoformat available for matching formats)
parsed = datetime.fromisoformat(iso)
# Use monotonic for elapsed intervals
start = time.monotonic()
# ... work ...
elapsed = time.monotonic() - start
print(f"Elapsed seconds: {elapsed:.6f}")Database schema example (Postgres)
-- Store canonical instant with timezone information
CREATE TABLE events (
id serial PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
event_local_tz text -- optional: e.g. 'America/Los_Angeles' if you need local scheduling
);Notes: timestamptz stores instants with timezone awareness in Postgres and converts to UTC internally; store the user's timezone separately if you need to re-evaluate local calendar rules later.
Formatting for users
- Keep a single UTC instant in your model and store the user's IANA timezone (e.g., "Europe/Paris") when you need to display local times or reschedule recurring events.
- When showing datetimes, use the user's locale + timezone to format via Intl.DateTimeFormat (JS) or Babel/zoneinfo (Python).
Testing strategies
- Freeze or fake time in unit tests to get deterministic behavior. Use freezegun for Python and @sinonjs/fake-timers (or similar) for JavaScript.
- Tests that rely on elapsed durations should mock or stub monotonic sources when possible.
- End-to-end tests for scheduled jobs should run in an isolated timezone environment or explicitly set the TZ to a known value to avoid DST flakiness.
Common pitfalls and tradeoffs
- UTC everywhere vs. local-time semantics: Storing UTC simplifies ordering and math, but if your domain uses local calendar semantics (e.g., "send invoice on the 1st of every month at 09:00 in Europe/Berlin"), you must also store the timezone and apply rules on display / scheduling.
- Using wall clock for intervals: Relying on system time (Date.now(), time.time()) for timeouts can break when the clock is adjusted. The tradeoff is limited—use monotonic APIs for intervals and wall-clock for human timestamps.
- Leap seconds: Most platforms and databases do not expose leap seconds directly and may smear or ignore them. For almost all applications, you don’t need special leap-second handling; in very high-precision systems, consult your platform docs and consider NTP/PTP practices.
- Storing timezone as offset only: Offsets (e.g., +02:00) do not encode DST rules. Prefer IANA zone names when you need to preserve scheduling semantics across DST transitions.
Checklist to apply right now
- Ensure all persisted timestamps are normalized to UTC (ISO string or native DB instant).
- Switch interval measurement to monotonic clocks where you need reliable timings.
- Store IANA timezone identifiers for user preferences or scheduled events.
- Add deterministic time fakes to your test suite for time-dependent behaviors.
- Audit external APIs for how they serialize datetimes (ISO 8601 with offset recommended).
Conclusion
Time-related bugs are predictable and avoidable with consistent rules: store instants in UTC, use timezone-aware types, measure intervals with monotonic clocks, and include timezone metadata for calendar-driven features. Apply the checklist above to reduce a large class of bugs and make time handling maintainable across your stack.
Further reading: the Stack Overflow Blog piece "Time is a construct but it can still break your software" provides a good conceptual overview of why these issues persist; use the practical patterns here to operationalize those ideas.
Was this helpful?
Share this post
Comments (0)
Want to join the conversation?
Log in or sign up to leave a comment and share your thoughts.
Log in to Comment