Slow list_threads and read_thread calls on large LangSmith projects
Last updated: July 31, 2026
Symptom
On a large or long-running LangSmith project, calls to the v1 thread APIs in the Python SDK get slow or time out:
client.read_thread(thread_id=...)takes tens of seconds even for a thread with only a handful of runs.client.list_threads(project_name=...)with a largelimit(for example, several hundred) never finishes in a usable time.list_threadswithout astart_timereturns fewer threads than expected, or an empty list, on a project with plenty of older activity.
Cause
list_threads and read_thread in the v1 API are built on top of the general run-listing query path. They fetch full Run objects and group them by thread on the way out. There is no dedicated server-side thread index in v1, so the cost of a call scales with how many runs fall inside the time window you scan.
list_threads: silent 1-day default, expensive when widened. If you omit start_time, the SDK substitutes "now minus 1 day" before sending the request. This is documented but easy to miss. Two things go wrong from there:
If your project has activity older than a day and you didn't pass
start_time, threads outside that 24-hour window are silently excluded. The response looks valid, just incomplete.If you pass a far-past
start_time(weeks or months) together with a largelimit, the backend has to scan, group, sort, and marshal a very large number of runs in one synchronous call. That is where the timeouts come from.
read_thread: no default time bound at all. Unlike list_threads, read_thread does not apply a default start_time. With no time bound, it scans the entire project history to find runs matching the given thread_id. On a project with hundreds of thousands of traces, that is an unbounded scan, which is why even a thread with two or three runs can take a long time to return. It also returns full Run objects, so the response payload is heavy.
Resolution
The single most impactful change on the v1 API is to always pass a start_time, sized to the smallest window that covers the data you actually need.
list_threads: pass start_time explicitly.
import datetime
threads = client.list_threads(
project_name="my-project",
start_time=datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=7),
)
read_thread: narrow the underlying scan with a filter expression using LangSmith trace query syntax.
for run in client.read_thread(
thread_id="<thread-id>",
project_name="my-project",
filter='gt(start_time, "2024-01-01T00:00:00Z")',
):
...
Two habits that keep v1 calls fast:
Keep the time window as narrow as the use case allows. For near-real-time work (for example, aggregating the current session), hours is usually enough.
Avoid pairing a large
limitwith a wide window. Paginate with smaller pages over smaller windows instead of asking for hundreds of threads in one call.
Upgrade path: v2 thread methods (Python SDK 0.10.0+)
The v2 methods were built to address these performance issues directly. They use a dedicated server-side threads index and return lightweight thread objects instead of full Run payloads.
v1 | v2 |
|---|---|
|
|
|
|
client.threads.query()usesmin_start_timeandmax_start_time(both default to a 1-day window, with no silent over-scan) and cursor pagination withpage_sizefrom 1 to 100 (default 20).client.threads.list_traces(thread_id, project_id)returns lightweightThreadTraceobjects with preview fields. Aselectsparameter lets you request only the fields you need.
The v2 thread methods on the Python Client are async-only: use await and async for. The v1 methods remain sync-only.
Watch for these parameter changes when migrating:
project_nameis not accepted. Resolve toproject_idfirst viaclient.read_project(project_name=...)and passproject_id.Single
start_timebecomes an explicitmin_start_timeandmax_start_timepair.offset+limitbecomescursor+page_size.