RunnableConfig: how configuration flows through the graph
Responsibilities
Running a LangGraph graph requires a runtime context that goes well beyond the input data itself. recursion_limit, thread_id, checkpoint_id, callbacks, tags, metadata, plus the store / stream_writer used by nodes — all of them must be passed in, merged, validated, and finally embedded into the configurable dict when graph.invoke(input, config) is called, so they can be threaded through to each node's RunnableConfig. This unified configuration object is RunnableConfig, which directly reuses the LangChain Core type of the same name (from langchain_core.runnables import RunnableConfig) rather than being a LangGraph reinvention.
It sits at the very entry of the architecture: as soon as Pregel.invoke / Pregel.stream is entered, ensure_config (ensure_config:322) is called to fill in defaults for scattered user-supplied fields and merge the inherited parent-graph configurable with what the caller passed in, producing a complete config that is then handed to PregelLoop. Throughout the superstep loop, this config is both the locating coordinate for checkpoints (thread_id + checkpoint_ns + checkpoint_id) and the entry point for nodes to obtain runtime tools (store / stream_writer / runtime).
Nodes do not reach into the config dict directly to read these fields — LangGraph exposes three shortcut functions: get_config (get_config:17), get_store, and get_stream_writer. They internally read the RunnableConfig from the current contextvar and pull the Runtime instance out of the configurable dict. So end to end, the config passes through five stages: "user passes it in → ensure_config fills defaults → loop threads it through → contextvar injection → node consumes it".
Design motivation
Why pack everything into RunnableConfig instead of defining a brand-new GraphConfig class?
- Reuse the LangChain ecosystem: The Runnable protocol, tracing, and callbacks all understand
RunnableConfig, so graph nodes are natively compatible with the LangChain toolchain.ensure_config(empty = RunnableConfig(...):322-337) even follows LangChain's merge rules fortags/metadata/callbacks. - Inheritable and overridable: When a subgraph runs inside a parent-graph node, the parent's
configurableis automatically threaded through. But as soon as the subgraph explicitly supplies its ownthread_id, that is treated as "resetting the checkpoint lineage" — see the "explicit config" logic inensure_config(explicit checkpoint coordinate:355-367). Otherwise the subgraph would write checkpoints under the parent namespace and never find them again. - Clear field boundaries: The standard fields (
tags/metadata/callbacks/recursion_limit/configurable) areCONFIG_KEYS, while LangGraph's own runtime fields are uniformly given a__pregel_prefix and stashed inconfigurableto avoid colliding with user-defined keys — seeCONFIG_KEY_* constants:33-77. - Defaults are env-tunable:
recursion_limitdefaults to theLANGGRAPH_DEFAULT_RECURSION_LIMITenvironment variable (DEFAULT_RECURSION_LIMIT:32), so the loop ceiling can be raised without a code change.
Key files
get_config:17-29— Reads the currentRunnableConfigfrom thevar_child_runnable_configcontextvar; raisesRuntimeErrorif not inside a runnable context.get_store:32-123— Readsstorefromconfig[CONF][CONFIG_KEY_RUNTIME].store; the docs show bothStateGraphandentrypointusage.get_stream_writer:126-196— Returnsruntime.stream_writer; nodes use it to emit custom stream events (stream_mode="custom").ensure_config:322-420— Merges multiple configs, fills the defaultrecursion_limit=DEFAULT_RECURSION_LIMIT, and handles checkpoint-coordinate resets.merge_configs:147-180— The underlying deep-merge implementation for multiple configs; theconfigurabledict is shallow-merged.patch_configurable:52-62— Patches only theconfigurablekey, preserving the rest — the loop uses it to dynamically inject__pregel_checkpointerand friends.CONFIG_KEY_* constants:33-77— The full set of reserved keys:__pregel_send/__pregel_read/__pregel_checkpointer/__pregel_runtime/__pregel_resumingand so on.CONF constant:91-92—CONF = "configurable"; all reserved keys live underconfig[CONF].recursion_limit validation:2563-2564— In_setup_stream, readsconfig["recursion_limit"]directly and raises if it is less than 1.loop.stop:1701—self.stop = self.step + self.config["recursion_limit"] + 1, translating the recursion ceiling into "the maximum step index to run to".GraphRecursionError:3005-3011— Raised when execution reachesout_of_steps, telling the user to raiserecursion_limit.
Data flow
The pivotal piece is ensure_config: it merges the contextvar-inherited config with the caller-supplied config, and when it sees an explicit checkpoint coordinate it resets the ambient configurable, ensuring recursion_limit / configurable / tags / metadata / callbacks are all in place.
empty = RunnableConfig(
tags=[],
metadata=ChainMap(),
callbacks=None,
recursion_limit=DEFAULT_RECURSION_LIMIT,
configurable={},
)
if var_config := var_child_runnable_config.get():
empty.update(
{k: v.copy() if k in COPIABLE_KEYS else v
for k, v in var_config.items() if _is_not_empty(v)},
)(ensure_config default values:331-345)
After merging, Pregel._setup_stream (_setup_stream:2563) reads CONFIG_KEY_CHECKPOINTER / CONFIG_KEY_RUNTIME / CONFIG_KEY_CACHE from config[CONF] to decide which checkpointer / store / cache to use for this run — the precedence is "config-injected > constructor-supplied".
if self.checkpointer is False:
checkpointer: BaseCheckpointSaver | None = None
elif CONFIG_KEY_CHECKPOINTER in config.get(CONF, {}):
checkpointer = config[CONF][CONFIG_KEY_CHECKPOINTER]
elif self.checkpointer is True:
raise RuntimeError("checkpointer=True cannot be used for root graphs.")
else:
checkpointer = self.checkpointer(checkpointer resolution:2579-2586)
Boundaries and failure modes
get_configraisesRuntimeErroroutside a runnable context (get_config error:29), so callingget_store()directly in a unit test blows up — you must go throughgraph.invokeor a function decorated withentrypointto obtain it.get_store/get_stream_writerare unavailable under async on Python < 3.11 (async warning:53-57), because contextvar propagation depends on theasyncio.create_taskbehavior added in 3.11.recursion_limit < 1raises immediately (validation:2563-2564), not a silent fallback to the default.checkpointer=Truecannot be used on a root graph (True error:2583-2584).Truemeans "inherit from parent", and the root graph has no parent, so you must supply an explicitBaseCheckpointSaverorFalse.- Enabling a checkpointer without supplying
thread_idor other coordinates inconfigurableraises (checkpointer requires coordinates:2589-2593), asking for one ofthread_id/checkpoint_ns/checkpoint_id. - A subgraph that explicitly passes
thread_idresets the ambientconfigurable(explicit coordinate reset:362-367). This is what prevents the subgraph from writing checkpoints under the parent namespace where they cannot be found — but conversely, if you expect the subgraph to inherit a custom key from the parent'sconfigurable, this reset means you will not get it.
Summary
RunnableConfig is the contract between LangGraph and the LangChain ecosystem, and the single carrier for checkpoint addressing, runtime-tool injection, callbacks, and tracing. Once you understand the merge rules in ensure_config and the division of labor among the __pregel_* reserved keys, you have essentially grasped the path "how configuration flows from the user into a node".
Continue with StateSnapshot to see how this config is bound to a checkpoint, and with the Pregel engine to see how loop.stop uses recursion_limit to cap the loop.
See official docs: LangGraph docs · README.