Skip to content

/sync/watched/shows returns different last_updated_at values for the same show depending on extended #876

Description

@betablon

Summary

On GET /sync/watched/shows, the last_updated_at field of the watched-show
wrapper holds a different value under extended=full than under
extended=progress — for the same show, in the same account, from requests
made seconds apart.

This appears to be two semantically different timestamps sharing one key, rather
than one timestamp with drift. I could not find this documented, and it isn't
mentioned in #775.

Related but distinct: api-help#17 compared last_updated_at from
/sync/watched against updated_at from /shows/:id and was closed as
Cloudflare caching. This report is a different comparison — same endpoint,
same field, only extended differs
— and the evidence below is not
consistent with a caching explanation.

Environment

  • Endpoint: GET /sync/watched/shows
  • Compared: ?extended=full&limit=250 vs ?extended=progress&limit=100
  • Account size: 175 watched shows
  • Both requests issued back-to-back from the same code path, ~1 second apart
  • Observed 2026-07-20, reproduced across several runs hours apart

Steps to reproduce

  1. GET /sync/watched/shows?extended=full&limit=250
  2. GET /sync/watched/shows?extended=progress&limit=100
  3. Join the two result sets on show.ids.trakt
  4. Compare last_updated_at (the wrapper field, not show.updated_at)

Observed

Across 175 shows: 134 agreed, 41 differed, none null under either variant.

Show extended=full extended=progress
Die Landarztpraxis 2026-07-20T16:27:00.000Z 2026-07-20T16:27:51.000Z
The Nanny 2026-07-14T16:16:00.000Z 2026-07-14T16:16:24.000Z
Villa der Versuchung 2026-07-05T14:42:00.000Z 2026-07-05T14:42:12.000Z
The Traitors 2026-07-13T10:00:00.000Z 2026-07-13T22:25:37.000Z
Prominent getrennt – Die Villa der Verflossenen 2026-07-16T10:00:00.000Z 2026-07-17T22:11:41.000Z

The pattern

Three things stand out:

  1. full values are always truncated to the minute (:00.000Z seconds).
    progress values carry real seconds. This held across every differing show.
  2. The progress value matches /sync/last_activities. In the same refresh
    where Die Landarztpraxis reported progress = 2026-07-20T16:27:51.000Z,
    /sync/last_activities reported episodes.watched_at = 2026-07-20T16:27:51.000Z — identical to the second.
  3. The gaps are far larger than clock skew. The Traitors differs by ~12
    hours; Prominent getrennt by more than a day.

Working hypothesis: under extended=progress, last_updated_at reflects when
this user's watched progress for the show last changed
, while under
extended=full it reflects when the show record itself last changed. The
10:00:00 values under full look like show-level events (episode air times),
and the progress values line up with when the user actually watched.

If that's right, both values are individually reasonable — the problem is that
they share a key, so a client can't tell which one it received.

Why this doesn't look like caching

api-help#17 was closed with "the dates can be different due to Cloudflare
caching." That explanation doesn't fit here:

  • Caching would not consistently truncate one variant's seconds to :00.
  • Caching would produce arbitrary skew, not a value that matches
    /sync/last_activities to the second.
  • Caching would not reproduce the same 41 shows across runs hours apart.
  • The two requests were issued ~1 second apart in the same code path.

Why it matters for clients

last_updated_at is the natural change-detection gate for a sync client: store
it per show, re-fetch when it moves. That breaks silently when different code
paths request different extended variants.

Concretely, in our client one pass used extended=full (metadata refresh) and
another used extended=progress (watched-state sync). Both stored the same
field. Each pass saw the other's value as "changed" and overwrote it, so ~41 of
175 shows were reprocessed on every refresh — including a redundant
/shows/:id/next_episode request each. Nothing was visibly broken, which is why
it went unnoticed; we only found it by diffing the two payloads directly.

Workaround: store the two values in separate fields and never compare across
variants.

Questions

  1. Is this intentional — are these meant to be two different timestamps?
  2. If so, could the docs state what last_updated_at means per extended
    variant? Distinct keys would be even better.
  3. Is the minute-truncation under extended=full intended?

Filed as a follow-up to #875, which covers the field-coverage differences
between these same two variants.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions