Timestamps in APIs & Databases: Storage, Formats & Timezone Pitfalls

Most timestamp bugs aren't about the epoch or the math — they're about a team not agreeing, early on, exactly how a timestamp is stored, transmitted, and interpreted. Here's how to make that decision once, correctly, instead of debugging it in production later.

Why Timestamp Handling Breaks More Systems Than It Should

A timestamp looks like one of the simplest values a system can hold — a moment in time, encoded as a number or a string. In practice, it's one of the most common sources of subtle, hard-to-reproduce bugs in any codebase that touches a database and an API at the same time. The reason isn't the underlying math; it's that "timestamp" quietly means several different things depending on where you look — a column type, a wire format, an application-layer object, and a display string — and each of those layers can disagree about timezone, precision, and format without throwing an obvious error.

Most of these bugs are avoidable with a small set of decisions made deliberately at the start of a project rather than discovered accidentally months in. This guide walks through those decisions: what to store, how to format it for an API, and how to keep the two in sync as a system grows.

💡
ToolsNovaHub Pro Tip
Pick one timestamp format for your entire API surface and document it in one place (an API style guide, not scattered across endpoint docs). Consistency matters more than which specific format you pick — mixing seconds and milliseconds across different endpoints of the same API causes more real bugs than either choice alone.
⚠️
Common Beginner Mistake
Storing a timestamp using a database column type that silently applies the server's local timezone (or worse, whatever timezone the connecting client happens to be in) instead of a timezone-aware or UTC-normalized type. The bug doesn't show up in development — it shows up months later when the server, a client, or a colleague's laptop is in a different timezone and the same row suddenly displays a different time.

Choosing a Storage Format: INTEGER vs TIMESTAMP vs TIMESTAMPTZ

At the column level, there are really three broad approaches, and each has a real, current tradeoff rather than one being universally correct.

ApproachWhat It StoresTimezone HandlingBest Fit
INTEGER (Unix epoch)Seconds or milliseconds since 1970-01-01 UTCNone — timezone-agnostic by nature, all conversion happens in application codeSystems that need to match a specific external API or legacy format exactly
TIMESTAMP (no timezone)A calendar date and time with no timezone attachedAmbiguous unless the application enforces a convention (e.g. "always UTC")Values that are genuinely timezone-independent, like a recurring daily schedule time
TIMESTAMPTZ / timezone-awareA moment in time, normalized and stored consistently regardless of session timezoneHandled by the database — always comparable and sortable correctlyAlmost everything else: event timestamps, audit logs, created/updated fields

For the large majority of application data — when something happened, was created, or was last modified — a timezone-aware type is the safer default. The narrow exception is data that's genuinely about a repeating local time (a daily alarm, a weekly recurring meeting slot) where you want the wall-clock time itself to survive a timezone change unaffected.

Why "Always Store UTC" Is Necessary But Not Sufficient

"Just store everything in UTC" is good advice, but it's incomplete for a specific and common case: recurring events tied to a person's local time. If a user schedules a weekly reminder for 9 AM in their timezone and you only store the UTC-converted instant, that reminder silently shifts by an hour whenever daylight saving time changes on either end — because you stored a fixed point in time, not "9 AM, local, recurring." For genuinely recurring local-time events, store both the UTC instant for the next occurrence and the IANA timezone name (like America/Chicago, not a fixed UTC offset like -05:00) so the next occurrence can be recalculated correctly across DST transitions.

ISO 8601 in JSON APIs: The De Facto Standard

For timestamps traveling over an API in JSON, ISO 8601 (specifically its RFC 3339 profile) has become the practical default — a string like 2026-08-30T14:22:05Z. It's unambiguous, sorts correctly as plain text, and every mainstream language's standard library can parse it without a third-party dependency. The trailing Z denotes UTC explicitly; if you instead include a fixed offset like +05:30, make sure your parsing code distinguishes between "this moment, expressed with an offset" and "this local wall-clock time," since conflating the two is a frequent source of off-by-a-few-hours bugs when data crosses systems.

Seconds, Milliseconds & the Precision Mismatch Between Systems

Beyond string formats, plenty of APIs still exchange timestamps as raw numbers, and this is where seconds-vs-milliseconds confusion causes real damage — a millisecond value misread as seconds lands the resulting date somewhere in the year 1970, and the reverse produces a date far in the future. The underlying precision fundamentals (why JavaScript defaults to milliseconds while most Unix-heritage tooling defaults to seconds) are covered in depth in What Is Unix Time? and the practical per-language conversion code lives in Timestamps in JS, Python & PHP — worth bookmarking both if your API needs to interoperate with several of these ecosystems at once. Whichever precision you choose for a given endpoint, document it explicitly in the field name or the API reference rather than leaving it implied.

Common Database Column Types Across Popular Systems

The same conceptual choice (timezone-aware vs not) is implemented differently enough across popular databases that assuming one system's behavior carries over to another is a frequent source of migration bugs.

SystemNative TypeTimezone Behavior
PostgreSQLTIMESTAMPTZStored internally in UTC, converted to the session's timezone setting on display — genuinely timezone-aware
MySQLTIMESTAMPConverted to UTC on write and back to session timezone on read; DATETIME stores the literal value with no conversion at all
SQLiteNo dedicated typeStores as TEXT, INTEGER, or REAL depending on the format you choose — timezone handling is entirely up to the application
MongoDBDate (BSON)Stores milliseconds since epoch, timezone-agnostic — display timezone is purely an application-layer concern

The practical implication: if you migrate data between these systems, don't assume a column labeled "timestamp" behaves identically on both sides. Confirm the actual conversion behavior on both ends before trusting a straight copy.

Designing API Timestamp Fields: Naming & Convention

A few small naming conventions prevent a disproportionate number of integration questions later. Suffix field names with their unit when you're not using ISO 8601 strings — created_at_ms rather than an ambiguous created_at that could be seconds, milliseconds, or a string depending on the reader's assumption. Keep create and update timestamps immutable and server-set rather than accepting them from API clients, unless the endpoint is explicitly designed for backfilling historical data — allowing clients to set their own created_at invites both accidental and intentional inconsistency in your data's timeline.

It's also worth distinguishing, in your own documentation, between a fixed UTC offset and a named timezone — they answer different questions and get conflated constantly. An offset like +05:30 tells you the relationship between a specific instant and UTC at that moment, nothing more. A named timezone like Asia/Kolkata encodes a set of rules (including historical and future DST transitions, where applicable) for converting any instant to local time. If your API needs to answer "what will this be in local time next year," an offset alone can't do that reliably wherever the region observes DST — you need the named zone.

Handling Client-Submitted Timestamps Safely

Whenever a timestamp arrives from outside your system — a webhook payload, a form submission, a third-party API response — validate it strictly rather than parsing leniently. A permissive parser that accepts multiple formats "to be helpful" also accepts malformed or ambiguous input that later causes a silent misinterpretation rather than a clear error at the boundary. Reject anything that doesn't match your documented format exactly, and if a client-supplied timestamp lacks explicit timezone information, don't guess — either require it or clearly document the assumed default.

Pagination & Timestamps: Cursor-Based APIs

Timestamps show up in API design beyond just data fields — they're also commonly used as pagination cursors. A timestamp-based cursor (fetch everything after 2026-08-30T14:22:05Z) stays stable even as new rows are inserted between page requests, unlike offset-based pagination, which can skip or duplicate records when the underlying dataset changes mid-scroll. The tradeoff is that timestamp cursors need a tie-breaker (commonly a secondary sort on a unique ID) for the rare case where multiple records share the exact same timestamp, since millisecond precision isn't always fine-grained enough to guarantee uniqueness at scale.

Legacy 32-Bit Columns: Still a Live Risk in 2026

Teams migrating older systems occasionally discover a timestamp column that was defined as a 32-bit signed integer years ago, which will overflow in January 2038 regardless of which database platform it lives in. Most current database-native timestamp types default to 64-bit storage, so this risk is specifically about manually chosen INT columns inherited from an older schema, not something new schemas typically introduce by accident. If you're auditing a legacy system for this, the full mechanics and a systematic way to check your own systems are covered in The Year 2038 Problem Explained.

Real-World Scenarios

E-commerce orders across timezones
An order placed at 11:58 PM local time needs to store the UTC instant for correct global reporting, while still displaying "11:58 PM" to the customer who placed it — both values matter, for different audiences.
Audit log immutability
Compliance-sensitive audit logs should record event time with a timezone-aware, server-assigned timestamp that can never be backdated by a client request, since the integrity of the sequence itself is often the point of the log.
Webhook delivery verification
Many webhook providers include a timestamp in the payload specifically so the receiver can reject requests outside a tolerance window, mitigating replay attacks — this only works if both sides agree precisely on format and precision.
Cron jobs across DST changes
A job scheduled for "2 AM local time" needs the IANA timezone stored alongside the schedule, not just a fixed UTC offset, or it silently runs an hour off twice a year when DST shifts.

A pattern worth noticing across all four: in each case, the naive fix (just store UTC, just store the offset) works fine until a specific edge case exposes the gap — a DST transition, a replay attempt, a cross-timezone report. Building the extra field or the extra validation step in from the start is cheaper than the migration that follows discovering the gap in production.

Migration Checklist: Moving Off Legacy Integer Timestamps

  1. Audit every timestamp column for its actual type and whether it's genuinely 32-bit or 64-bit before assuming either.
  2. Decide on one canonical wire format (typically ISO 8601 UTC) for all API responses going forward, even if internal storage varies.
  3. For recurring local-time data, add an explicit IANA timezone column rather than relying on a fixed offset.
  4. Update API documentation to state precision (seconds vs milliseconds) explicitly per field, not just once at the top of the document.
  5. Test the migration against DST transition dates specifically, not just arbitrary sample dates, since that's where silent bugs concentrate.

Summary

Timestamp bugs are rarely about the math — they're about a mismatch in unstated assumptions between a database column, an API contract, and the code reading both. Choosing a timezone-aware storage type by default, standardizing on ISO 8601 for API payloads, documenting precision explicitly, and storing IANA timezone names alongside genuinely recurring local-time data covers the overwhelming majority of real-world cases without needing anything exotic.

Frequently Asked Questions

Use your database's native timezone-aware timestamp type when available, such as PostgreSQL's TIMESTAMPTZ. Integers work but push all timezone and formatting logic into application code, which is easy to get wrong across a growing team.
UTC storage solves ordering and comparison, but you often also need the original timezone the event happened in for correct display, especially for recurring calendar events tied to a user's local time.
ISO 8601, typically as an extended-format string like 2026-08-30T14:22:05Z, which is unambiguous, human-readable, and natively parseable by virtually every language's standard library.
There's no universal standard, so document your choice explicitly per endpoint. JavaScript's Date expects milliseconds by default while many Unix-heritage systems default to seconds, and mixing the two silently is a common integration bug.
MySQL's TIMESTAMP is stored in UTC internally and converted on read/write based on the session timezone, while DATETIME stores exactly what's written with no timezone conversion at all.
Parse it strictly against the expected format (typically ISO 8601) rather than accepting loosely-parsed date strings, and reject anything ambiguous rather than guessing the client's intended timezone or format.
A timestamp cursor stays stable even as new records are inserted between page loads, while page-number pagination can skip or repeat records when the underlying data changes mid-scroll.
MongoDB's native Date type stores milliseconds since the Unix epoch internally and is timezone-agnostic by design, meaning timezone display logic has to happen entirely in the application layer.
Yes, if the column is genuinely a 32-bit signed integer, it will overflow in 2038 regardless of database platform. Most modern databases default to 64-bit storage for their native timestamp types, but manually chosen INT columns remain at risk — see The Year 2038 Problem Explained.
Store the event time in UTC and store the IANA timezone name (like America/New_York) separately if you need to reconstruct local time later, since a fixed UTC offset alone doesn't account for DST transitions.
🕑
Expert Tip
When in doubt about a legacy column's timezone behavior, write a small test that inserts a known timestamp from two different session timezones and compares what's actually stored — documentation and real behavior don't always match.
⏳
ToolsNovaHub Tool
Convert between epoch seconds, milliseconds, and human-readable dates instantly with the Unix Timestamp Converter — useful for spot-checking API payloads and database values alike.

📋 Related Guides Comparison

ResourceTypeLink
Unix Timestamp ConverterToolOpen Tool →
What Is Unix Time?GuideRead Guide →
Timestamps in JS, Python & PHPGuideRead Guide →
The Year 2038 ProblemGuideRead Guide →
Explore All ToolsNovaHub Tools
🏠 Go to Homepage

🔗 More Guides