Your own client ↔ server
You control both ends — agree one explicit wire contract and serialize symmetrically.
When you own both the client and the server, you get to define the wire format — and that is a gift worth using carefully. Pick one explicit representation, apply it consistently everywhere, and document it. The safest choice for most applications is RFC 3339 in UTC: human-readable, unambiguous, supported natively or near-natively in every modern language, and sortable as a string.
The classic failure mode is trusting the language’s default serialization. JavaScript’s Date.prototype.toString() emits a locale-specific, implementation-defined string. Python’s datetime.__str__ omits the offset if the object is naive. If client and server independently call toString() or str(), they may agree today and diverge when you upgrade a library or deploy to a server in a different locale.
Define the contract once, enforce it at both ends, and treat any deviation as a bug — not a “close enough”.
Serialize and deserialize symmetrically
Every path that sends a timestamp (REST body, WebSocket message, GraphQL field, cookie, local-storage value) should use the same serializer. Every path that receives one should use the same deserializer. If you find yourself parsing a date in three different ways in three different files, that is technical debt accumulating against correctness.
Pitfall: Relying on JSON.stringify to serialize a JavaScript Date object. It calls toISOString(), which gives UTC with milliseconds — good — but JSON.parse deserializes it back as a plain string, not a Date. The round-trip is not symmetric, so a date value read back from JSON is silently a string until something breaks.
Go deeper: versioning, clocks, and typed schemas
Document and version the contract. Put the wire format in an API doc, a schema (OpenAPI, Protobuf, GraphQL type), or at minimum a code comment that says “ISO 8601 / RFC 3339 UTC, millisecond precision”. Future developers — and future services that consume your API — will thank you. See RFC 3339 for the format spec.
Schema enforcement. If you use a typed schema layer (Zod, Pydantic, Protobuf), express timestamps as a dedicated type with explicit parsing. Don’t accept string and handle parsing ad hoc — that is how inconsistencies accumulate.
Clock skew. Even when client and server agree on format, their clocks may disagree by seconds or minutes. For ordering events, prefer server-assigned timestamps over client-supplied ones. For durations measured across the round-trip, account for skew or use a monotonic clock for the client-local measurement. See Store vs display for the principle of keeping instants in UTC and converting only at display time.
Relative vs absolute. Avoid sending relative durations (“5 minutes ago”) over the wire as your source of truth — they depend on when they are evaluated. Send the absolute instant and let the receiver render the relative label.