TTL strategies — sliding TTL and a per-thread checkpoint cap?

Hi team,

I’ve been working through the thread/checkpoint TTL model on LangGraph Platform (checkpointer.ttl in langgraph.json, plus per-thread ttl via the Threads API) and I’ve read the existing threads on this — Checkpoint cleanup (#3037), How to handle growing checkpoint blob (#4448), configuring TTL via env vars (#4253), and the keep_latest pruning proposal (#4291).
Also the official docs on configure ttl

Those cover the pieces individually, but I couldn’t find one place that ties together how TTL is supposed to coexist with human-in-the-loop (HITL) and replay/time-travel debugging. I want to confirm my understanding and raise two potential feature-shaped questions.

My understanding (please correct)

  • delete: at expiry, the entire thread (runs + all checkpoints + blobs + writes) is removed. The timer is absolute — it starts when the TTL is applied and does not refresh on activity.

  • keep_latest: here the docs are genuinely ambiguous to me. I can read it two ways:

    • Interpretation A (what I assume is correct): the thread is never deleted. keep_latest continuously prunes older checkpoints, keeping the latest per (thread_id, checkpoint_ns), and the window refreshes when a run finishes or state updates — i.e. a sliding prune-to-latest that runs indefinitely.
    • Interpretation B: keep_latest retains only the latest checkpoint until TTL expiry, and then at the deadline everything is removed (thread + latest checkpoint included) — essentially delete, just with intermediate checkpoints pruned along the way.

    My reading of “retains the thread and latest checkpoint, but deletes older checkpoint data” and “the keep_latest window refreshes when a run finishes or thread state is updated” points to Interpretation A, but the docs never explicitly say the thread survives the TTL deadline. Can someone confirm A is correct and B is wrong? This distinction is critical for continuity guarantees.

The rest of this post assumes Interpretation A.

If that’s right, then:

1. HITL + replay seem to be in tension under any single strategy

  • keep_latest preserves the latest checkpoint → HITL survives, but it prunes the older checkpoints that replay/time-travel/debug depend on.
  • delete keeps the full chain (so replay works) but only until the absolute deadline, after which both HITL and replay are gone.

So it looks like there’s no strategy that supports HITL + full replay history beyond the TTL window — you must size delete to cover your entire replay/debug horizon, or disable TTL and manage growth manually. Is that the intended model? Is there a recommended pattern for “keep the latest for continuity/HITL, but retain the last N checkpoints (not just 1) for replay”?

2. Is there a sliding TTL for delete?

For long-running conversations that stay active for many days, delete will remove an active thread at the deadline regardless of activity. Today the workarounds I see are:

  • switch to keep_latest (loses replay history), or
  • PATCH /threads/{id} with a fresh relative ttl on each activity to roll the window myself, or
  • set a very large default_ttl upfront.

Is application-managed PATCH the blessed way to get a sliding delete TTL? Or is an activity-refreshing option for delete on the roadmap?

3. No count-based cap on checkpoints per thread?

There’s a time-based prune (keep_latest) but no max_checkpoints_per_thread style cap. For long threads under a large TTL, checkpoints accumulate until the sweeper acts. Two asks:

  • A count-based retention (“keep last N checkpoints per namespace”) would serve replay + bounded growth better than keep-exactly-1.
  • Separately, even with keep_latest, each surviving blob still holds the entire message history (raised in #4448). Is ReducingSaver / state-level trimming the recommended answer for blob-size growth, or is there platform-level guidance?

Thanks in advance — mainly want to confirm the HITL-vs-replay tradeoff is by design and hear the recommended production pattern.