Skip to content

fix(core): prevent RuntimeError in Serializable.to_json() under thread concurrency#35340

Open
Balaji Seshadri (gitbalaji) wants to merge 3 commits intolangchain-ai:masterfrom
gitbalaji:fix/core-serializable-thread-safe
Open

fix(core): prevent RuntimeError in Serializable.to_json() under thread concurrency#35340
Balaji Seshadri (gitbalaji) wants to merge 3 commits intolangchain-ai:masterfrom
gitbalaji:fix/core-serializable-thread-safe

Conversation

@gitbalaji
Copy link
Contributor

@gitbalaji Balaji Seshadri (gitbalaji) commented Feb 19, 2026

Summary

  • Root cause: Serializable.to_json() iterated over self (which calls Pydantic __iter__, i.e. self.__dict__.items() live). A concurrent thread that accesses a @cached_property for the first time (e.g. _serialized on BasePromptTemplate.invoke()) writes a new key into self.__dict__, causing RuntimeError: dictionary changed size during iteration.
  • Fix: Copy __dict__ before iterating — self.__dict__.copy().items(). dict.copy() is atomic under CPython's GIL, so it produces a consistent snapshot that cannot race with a concurrent @cached_property write. Keeps the original iteration logic intact with minimal diff.
  • Regression test: test_to_json_thread_safe — spawns 4 threads running concurrent dumpd() and .invoke() calls against the same prompt instance. Reliably reproduced the crash without the fix.

Fixes #34887.

Areas requiring careful review

  • self.__dict__.copy() is a shallow copy taken atomically under CPython's GIL. Once the copy exists, we iterate over a separate dict object, so any concurrent @cached_property write to the original __dict__ is harmless.
  • Non-model-field keys that may appear in __dict__ (e.g. cached properties like _serialized) are already filtered out by _is_field_useful, so the final lc_kwargs is identical to before.
  • No behaviour change for single-threaded callers.

Test plan

  • test_to_json_thread_safe passes and would fail on the unfixed code
  • Full tests/unit_tests/load/test_serializable.py suite passes (44 tests)
  • ruff check and ruff format clean
  • mypy clean

This PR was developed with the assistance of Claude Code (AI agent).

🤖 Generated with Claude Code

@github-actions github-actions bot added core `langchain-core` package issues & PRs external fix For PRs that implement a fix labels Feb 19, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 19, 2026

Merging this PR will improve performance by 34.02%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚡ 13 improved benchmarks
⏩ 23 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime test_import_time[InMemoryRateLimiter] 184 ms 161.6 ms +13.91%
WallTime test_import_time[Document] 201 ms 168.7 ms +19.12%
WallTime test_import_time[CallbackManager] 344.5 ms 292.4 ms +17.81%
WallTime test_import_time[ChatPromptTemplate] 680.4 ms 557.9 ms +21.98%
WallTime test_import_time[InMemoryVectorStore] 659.2 ms 530.1 ms +24.36%
WallTime test_import_time[LangChainTracer] 487.6 ms 416.8 ms +16.98%
WallTime test_import_time[BaseChatModel] 580.6 ms 484.8 ms +19.76%
WallTime test_async_callbacks_in_sync 24.5 ms 18.3 ms +34.02%
WallTime test_import_time[Runnable] 533.9 ms 443.3 ms +20.45%
WallTime test_import_time[RunnableLambda] 540.3 ms 446.2 ms +21.09%
WallTime test_import_time[HumanMessage] 287.6 ms 239.6 ms +20.04%
WallTime test_import_time[tool] 601.5 ms 478.3 ms +25.76%
WallTime test_import_time[PydanticOutputParser] 583.2 ms 478.4 ms +21.91%

Comparing gitbalaji:fix/core-serializable-thread-safe (5134cb4) with master (d6e46bb)

Open in CodSpeed

Footnotes

  1. 23 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions github-actions bot added fix For PRs that implement a fix and removed fix For PRs that implement a fix labels Feb 19, 2026
@gitbalaji Balaji Seshadri (gitbalaji) force-pushed the fix/core-serializable-thread-safe branch 2 times, most recently from 5134cb4 to 2ec5887 Compare February 26, 2026 01:16
…rrency

Fixes langchain-ai#34887.

Pydantic's `__iter__` walks `self.__dict__.items()` live. A concurrent
thread that triggers a `@cached_property` for the first time (e.g.
`_serialized` on `BasePromptTemplate`) writes a new key into `__dict__`,
racing with the loop and raising:

    RuntimeError: dictionary changed size during iteration

Fix: snapshot the iteration with `list(self)` before the loop so any
concurrent write is safe.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous attempt (`list(self)`) was insufficient: Pydantic's
`__iter__` still walks `self.__dict__.items()` internally, so the
RuntimeError fires inside `list()` when a concurrent @cached_property
write races with that inner iteration.

Iterate `type(self).model_fields` (a class-level dict, immutable at
runtime) instead, fetching each value with `getattr`. This avoids
touching `self.__dict__` as an iterator target entirely.

Fixes langchain-ai#34887.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Simpler fix for langchain-ai#34887.
dict.copy() is atomic under CPython's GIL, so it produces a consistent
snapshot that cannot race with a concurrent @cached_property write.
Keeps the original iteration logic intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gitbalaji Balaji Seshadri (gitbalaji) force-pushed the fix/core-serializable-thread-safe branch from 2ec5887 to 31b7c3a Compare February 27, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core `langchain-core` package issues & PRs external fix For PRs that implement a fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RuntimeError: dictionary keys changed during iteration during serialization under thread concurrency

2 participants