Skip to content

Engineering

Postgres timestamptz microseconds vs JavaScript Date milliseconds

A lookup that matches a row by its exact timestamp can return nothing even though the row is there and the value looks identical. This post explains the precision gap behind it, how it shows up, four ways to fix it, and the test that keeps it fixed, from the PostgreSQL and JavaScript references read on 5 November 2026.

By Angelina F. · Updated · 6 min read

A Postgres timestamptz stores microseconds, but a JavaScript Date holds whole milliseconds, so a timestamp read into JavaScript loses its last three digits. Match the row by that Date with exact equality and it can silently find nothing. Fix it by matching a one-millisecond window, or by storing timestamps at millisecond precision.

Key takeaways

  • Postgres timestamp and timestamptz keep 1-microsecond resolution; a JavaScript Date keeps whole milliseconds.
  • A stored …03.981164 becomes …03.981 in JavaScript, so exact equality against the column fails.
  • The bug hides until a timestamp has sub-millisecond digits, which is why it can survive for months.
  • Match a one-millisecond half-open window, or store timestamps as timestamptz(3), and test the sub-millisecond case.
On this page

Why doesn’t a Postgres timestamp match a JavaScript Date?

Because they hold different amounts of time. PostgreSQL stores timestamp and timestamptz values to the microsecond, while a JavaScript Date is a whole number of milliseconds since the epoch. When a driver reads …03.981164 into a Date, the last three digits are gone, and a value built from that Date no longer equals the one in the row.

Illustration: a database with a clock dial full of fine tick marks, a cracked equals sign, and a code editor with a sparser dial that a hand examines with a magnifier.
Six digits in the database, three in JavaScript.
Timestamp precision on each side (PostgreSQL and MDN references, read 5 November 2026)
Postgres timestamptzJavaScript Date
Resolution1 microsecond1 millisecond
Fraction digitsUp to 63
Example value10:15:03.98116410:15:03.981
Storage8 bytesA number of milliseconds since 1970

What does the bug look like?

A query that looks up a row by an exact timestamp returns nothing, and an update keyed on the same timestamp changes zero rows without an error. It typically appears when a job carries a scheduled time as an ISO string, the worker rebuilds a Date from it, and then matches the row with WHERE scheduled_at = $1.

It only bites when the stored value has sub-millisecond digits, which depends on how it was written: now() in SQL keeps microseconds, a value written from JavaScript does not. A codebase can run for months before the first such row appears.

How do you fix the Postgres and JavaScript precision mismatch?

The window is half-open, from the millisecond value up to but not including the next millisecond, so it matches exactly the rows a millisecond-precision value stands for and stays narrow enough for the database to use the index and prune partitions. Apply it everywhere code rebuilds a timestamp from a job payload, not just the first place it broke.

Four ways to fix timestamp matching between Postgres and JavaScript
FixHowWhen to use it
Match a windowWHERE scheduled_at >= $1 AND scheduled_at < $1 + interval '1 millisecond'Existing data you cannot rewrite; still uses the index
Store millisecondsDeclare the column timestamptz(3)New tables; Postgres rounds extra digits on write
Truncate on writedate_trunc('milliseconds', now()) instead of now()When SQL writes the value and JavaScript reads it back
Compare in SQLMatch by the row’s id, or pass the original text through unchangedWhen the exact value never needs to reach JavaScript
Diagram: Postgres stores 03.981164, JavaScript reads it as 03.981, an exact match finds no row, and a one-millisecond window match finds it.
Where the digits go, and how a window match finds the row.

How do you keep this bug from coming back?

Test the case that breaks: insert a row with a timestamp that has sub-millisecond digits (for example …03.981164), read it back through your driver into a Date, run the lookup, and assert that it finds the row. A test that only uses values written from JavaScript will always pass and prove nothing.

In SkedCast this matching sits in the path that publishes scheduled posts, which is why it is tested this way; see why scheduled social posts fail for the other causes, exactly-once publishing for how each network guards against duplicates, and idempotent Bluesky posting for one worked example.

Sources

The precision of each type is quoted from its reference, read on 5 November 2026.

postgres timestamptz microsecondsjavascript date millisecondspostgresqljavascriptengineeringpublish reliability

FAQ

Why does Postgres timestamptz not match a JavaScript Date built from the same value?
Postgres keeps microseconds and a JavaScript Date keeps whole milliseconds, so a value with sub-millisecond digits (…03.981164) never equals the Date rebuilt from it (…03.981).
How do you fix a timestamp-precision mismatch between a database and an application?
Match a half-open window one millisecond wide instead of exact equality, or store timestamps at millisecond precision (timestamptz(3)) so both sides hold the same value.
Does timestamptz(3) round or truncate?
Postgres rounds a value to the declared precision when it stores it. Use date_trunc if you need to cut digits off instead.
Does this bug depend on the platform a post is scheduled for?
No. It is in the matching logic, so it affects any scheduled row whose stored timestamp has sub-millisecond digits, whatever network the post is for.
How do you keep this class of bug from coming back?
Test it: store a timestamp with sub-millisecond digits, rebuild it through your application’s date type, and assert the lookup still finds the row.

Ready to broadcast everywhere?

Sign up free — no credit card. You land on the Free plan, and you can start a one-time 7-day Studio trial from your workspace whenever you are ready. Connect your first accounts, import a batch, and watch one post fan out across every platform.