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.

| Postgres timestamptz | JavaScript Date | |
|---|---|---|
| Resolution | 1 microsecond | 1 millisecond |
| Fraction digits | Up to 6 | 3 |
| Example value | 10:15:03.981164 | 10:15:03.981 |
| Storage | 8 bytes | A 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.
| Fix | How | When to use it |
|---|---|---|
| Match a window | WHERE scheduled_at >= $1 AND scheduled_at < $1 + interval '1 millisecond' | Existing data you cannot rewrite; still uses the index |
| Store milliseconds | Declare the column timestamptz(3) | New tables; Postgres rounds extra digits on write |
| Truncate on write | date_trunc('milliseconds', now()) instead of now() | When SQL writes the value and JavaScript reads it back |
| Compare in SQL | Match by the row’s id, or pass the original text through unchanged | When the exact value never needs to reach JavaScript |
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.
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.