Pitfalls & anti-patterns
A citable index of the warnings scattered across this guide.
Date and time bugs are insidious because they are often silent: the code runs, the value looks plausible, and the error only surfaces when a user in a different time zone reports a problem, or when DST transitions, or when a third-party API quietly changes its format. This page collects the most common anti-patterns as a single citable reference. Each item links to the pitfall callout where it is explained in context.
None of these are exotic edge cases. Every item in this list has caused production incidents in widely-used software. This index is a curated subset — the highest-impact traps — not an exhaustive catalog; most topic pages carry their own pitfall callout in context. The positive mirror of this page — the guide’s rules stated as imperatives, each with its own shareable link — is The rules.
Common anti-patterns
- Storing a naive/zoneless time and later assuming UTC. → Instant vs civil time
- Confusing a fixed offset with a named zone. An offset like
+02:00is not a time zone — it does not know about DST transitions. → Time zones vs offsets - Assuming every local day has 24 hours / every local time exists. DST gaps and overlaps violate both assumptions. → DST
- Measuring elapsed time with the wall clock. The wall clock can jump or go backwards (NTP, DST, user adjustment). Use a monotonic clock for durations. → Clocks
- Mixing precisions (seconds vs milliseconds). Epoch values in seconds and milliseconds look similar but differ by a factor of 1000. → Precision
- Trusting a lenient parser with user input. Free-text date strings are locale-dependent and ambiguous; lenient parsers silently guess wrong. → Parsing user input
- Confusing epoch seconds vs milliseconds from 3rd parties. External APIs do not label their units; assuming the wrong one silently misplaces every timestamp by a factor of 1000 — collapsing recent dates to near the 1970 epoch, or flinging them tens of thousands of years into the future. → 3rd-party data
- Comparing timestamps across a precision-truncating hop. A value written at microsecond precision and read back at milliseconds is never equal to itself, so equality-based change detection and sync logic runs hot forever. → The lowest common denominator
- Storing a pure date (or other civil value) as a UTC instant. A birthday or invoice date has no time and no zone; a UTC
timestamptz/Dateinvents both and can shift the date by a day. Use the type that fits the meaning. → Store vs display - Formatting an instant as a date without naming the zone. The environment’s zone silently decides the calendar day, so a server and a user can disagree about “today” — the display-side twin of the storage mistake above. → Formatting for display
- Materializing a recurring series as fixed UTC instants. “9 AM daily” is a civil rule, not a list of instants; precomputed UTC timestamps drift by an hour at every DST transition and break silently when zone rules change. → Recurring events
- Letting a column’s type or units contradict its name. A
datethat is really atimestamptz, or an epoch integer with no documented unit, turns every downstream query into a guess. → Your schema is a contract - Naming a numeric time field without its unit.
timeoutorttlwith no_ms/_secondsleaves seconds-vs-milliseconds to chance. → Naming time fields - Giving a field a name that lies about its kind. A boolean-sounding
deletedthat is really a soft-delete timestamp, or atimestampcolumn holding civil time. → Naming time fields
Pitfall: The meta anti-pattern — not deciding instant vs civil time up front. Choosing the wrong representation at schema-design time propagates through every layer of an application and is expensive to fix later. Make this decision explicitly before writing your first migration.
Go deeper: why these bugs cluster and how to prevent them systematically
Most of the anti-patterns above share a root cause: implicit assumptions. The wall clock is assumed to be monotonic. The offset is assumed to be stable. The external field is assumed to be UTC. The user’s locale is assumed to match the server’s.
The systematic fix is to make every assumption explicit at the boundary where data enters your system:
- Parse strictly at ingestion. Whether the source is a user, an external API, or a database read from a legacy system, parse with an explicit format and fail loudly on mismatch. See Parsing user input and 3rd-party data.
- Carry zone information with the value. Never let a zoneless timestamp exist inside your system’s business logic. Attach the zone or UTC marker at ingestion and keep it through to display. See Store vs display.
- Agree on a wire format. When data crosses a process boundary, use an unambiguous serialization (RFC 3339 UTC is the safest default) and document it. See Your own client ↔ server.
- Test around DST transitions. Unit tests that only run with fixed, non-boundary timestamps will not catch DST bugs. Add test cases for the hour before and after a known transition in your target zones.
The IANA time zone database is the authoritative source for zone rules; keep it up to date in your runtime and infrastructure.