Skip to content

Engineering

Postmortem: Postgres timestamptz microseconds vs JavaScript Date milliseconds bug

This is a real incident from SkedCast's own production history (PR #258/#260, 2026-07-29), not a hypothetical. A scheduled YouTube post sat queued for 35 minutes: it published nothing, recorded no error, and refused to retry. Four compounding defects caused it, and the root-cause bug was a precision mismatch between a Postgres timestamptz column, which stores microseconds, and a JavaScript Date, which only holds milliseconds.

All articles

By The SkedCast Team · Updated · 8 min read

Key takeaways

  • Postgres's timestamptz column stores microsecond precision; a JavaScript Date object only holds milliseconds — so rebuilding a Date from a stored timestamp and matching rows by exact equality compares a millisecond-precision value (…03.981) against a microsecond-precision one (…03.981164), which never matches.
  • That mismatch made the affected row invisible to its own worker: the UPDATE meant to park it as failed matched zero rows, so the status stayed scheduled with a null error log even though the log line claimed the row had been parked.
  • A second, independent defect meant a stranded claim marker on a row left in that state made it invisible to the polling query forever, with no error surfaced anywhere.
  • Because the row was never actually in a retryable state, the UI's Retry button answered 409 not_retryable — which is also why disabling composer enforcement, the team's first troubleshooting move, changed nothing: nothing was re-running the publish at all.

The incident

A scheduled YouTube post sat in scheduled status for 35 minutes: it published nothing, recorded no error anywhere, and refused to retry. Disabling composer enforcement — the team's first troubleshooting move — did not help. Four compounding defects, all shipped and fixed together in PR #258 (commit 0a0191c0), turned out to be responsible.

The root cause: Postgres timestamptz microseconds vs. JavaScript Date milliseconds

Postgres's timestamptz type stores microsecond precision. A JavaScript Date object holds only millisecond precision. Publish jobs carried their scheduledAt value as an ISO string, so the worker reconstructed a JavaScript Date from it and matched the target row by composite primary key using exact equality on scheduled_at — comparing a millisecond-precision value like …03.981 against the microsecond-precision value actually stored, …03.981164. Those two values are never equal, so the row was invisible to that query.

Only one row in production actually carried sub-millisecond digits, which is why the bug hid for as long as it did before surfacing. The fix matches on a 1-millisecond half-open window (scheduleInstantWindow, in the shared schedule-precision module) instead of exact equality — precision-proof, while still narrow enough to prune to a single database partition — and was applied to all three processors that rebuild a target instant from a job payload: publish, the dead-letter queue, and reconcile.

Why the row could not be seen, fixed, or retried

The invisibility cascaded. The worker's attempt to park the row as failed was an UPDATE keyed on that same mismatched timestamp, so it matched zero rows — status stayed scheduled with a null error_log, even though the worker's own log line claimed the row had been parked as content_locked. A second, independent defect meant that a row left scheduled while still carrying its claim marker (queued_at) was invisible to the poller that claims due targets FOREVER, since that poller only considers rows where queued_at is null — with no error raised anywhere to say why.

Because the row was never actually parked into a retryable status, retrying it was structurally impossible: the retry endpoint accepts only failed or rate_limited targets, and this row read scheduled, so every retry attempt answered 409 not_retryable. That is also the reason turning composer enforcement off changed nothing — nothing was re-running the publish call at all, because nothing considered this row eligible to run.

The four fixes

The precision-window match described above was the root-cause fix. Alongside it: a new recovery sweep re-claims any row left scheduled with a stale claim marker after a 30-minute grace period (chosen because publish is itself idempotent, so re-claiming a job that is actually still running safely no-ops rather than double-publishing); the retry endpoint's error surfaces the real underlying state instead of a bare 409; and — a related, separately-discovered defect in the same investigation — YouTube's server-required title field was being derived only client-side in the console, which silently broke automation clients that scheduled a title-less post through the API directly. That derivation now happens server-side, so every client agrees on what YouTube requires.

The lesson

The UI reported "Publishing", the worker's own log line reported "parking content_locked", and the database row said scheduled with a null error. The database was the one telling the truth — both of the other signals were reporting an action that had never actually persisted. When a system's log and its database state disagree, trust the row.

postgresqljavascriptengineering postmortempublish reliability

FAQ

Why does Postgres timestamptz not match a JavaScript Date built from the same value?
Postgres stores timestamptz values at microsecond precision, while a JavaScript Date can only represent millisecond precision — so a value with genuine sub-millisecond digits (e.g. …03.981164) never exactly equals the millisecond-truncated Date rebuilt from it (…03.981), and exact-equality matching between the two silently fails.
How do you fix a timestamp-precision mismatch between a database and an application language?
Match on a narrow half-open time window (for example, plus or minus one millisecond) instead of exact equality, so the comparison tolerates the precision the application layer actually has while still being narrow enough to stay fast and unambiguous.
Why did retrying the stuck post return a 409 error instead of working?
Because the post was never actually moved into a retryable status — the UPDATE meant to park it as failed matched zero rows due to the same timestamp mismatch, so the row stayed in its original scheduled status, and the retry endpoint only accepts failed or rate-limited targets.
Why didn't turning off a feature flag fix the stuck post?
Because nothing about the stuck row was related to that flag — the row was never eligible to be picked up and re-run in the first place, so disabling any downstream behavior could not have changed the outcome. The actual fix was making the row visible to the worker again.
Is this kind of bug specific to YouTube, or could it affect any scheduled post?
The precision mismatch itself is platform-agnostic — it affects the matching logic for any scheduled target regardless of platform. Only one row in this specific incident happened to carry sub-millisecond digits, which is a matter of how the original timestamp was generated, not something particular to YouTube.
How many tests were added to prevent this from happening again?
22 new tests were added across the fix — 11 for the required-field derivation, 5 for the timestamp-precision matching, and 6 for the stranded-claim recovery sweep — alongside the full existing suite across the affected services.

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.