LangGraph 1.2 shipped May 12, 2026. The headline features — DeltaChannel, per-node timeouts, node-level error handlers, and graceful shutdown — are production reliability primitives that were missing in earlier releases. If you are running LangGraph agents in production or are planning to, this is the version that makes the upgrade argument easier.
This guide covers each feature: what problem it solves, the API signatures, code patterns, and where the limits are.
Background: What Changed in 1.x
LangGraph 1.0 (October 2025) was the stability milestone. It stabilized the checkpointing model and committed to no breaking changes. LangGraph 1.2 (May 2026) builds on that foundation with hardening features that address the pain points teams reported after running 1.0 in production:
- Checkpoint storage bloat: Long agent sessions consumed gigabytes of checkpoint data because every step stored the full accumulated state.
- Runaway nodes: A node that called a slow external API could hold up an entire workflow indefinitely. There was no way to set a per-node time limit without wrapping each function manually.
- Partial failure recovery: When a critical node failed after exhausting retries, the only option was to fail the whole run. There was no in-graph compensation path.
- Ungraceful restarts: In-flight agents lost state when servers restarted. The work was durable (checkpointed), but stopping in-flight runs required external signals with no built-in cooperative protocol.
LangGraph 1.2 addresses all four.
DeltaChannel: 41× Checkpoint Storage Reduction
The biggest storage improvement in the 1.x series.
The problem: Standard LangGraph checkpointing stores the full agent state at every superstep. In a long coding session or research agent — anything that accumulates messages or file contents over many turns — the checkpoint storage grows O(N²). A 200-turn coding agent could require 5.3 GB of checkpoint storage. A 500-turn agent pushed that higher.
The solution: DeltaChannel stores only incremental deltas at each step rather than a full snapshot. It writes a complete snapshot every K steps (configurable via snapshot_frequency) to bound the cost of reconstructing state on resume.
from langgraph.types import DeltaChannel
# Replace standard Annotated state field with DeltaChannel
channel = DeltaChannel(
reducer=lambda s, xs: s + xs, # Must be batching-invariant
snapshot_frequency=50 # Full snapshot every 50 steps
)
Storage benchmarks from the LangChain team:
| Agent Type | Turns | Baseline | DeltaChannel | Reduction |
|---|---|---|---|---|
| Light coding + search | 500 | 4 GB | 110 MB | 41× |
| Multi-file feature implementation | 200 | 5.3 GB | 129 MB | 41× |
The ratio improves as session length increases (roughly 6× at 10 turns, 41× at 500 turns).
The batching-invariance requirement: The reducer function must satisfy:
reducer(reducer(s, xs), ys) == reducer(s, xs + ys)
This ensures that regardless of whether state is reconstructed from a delta sequence or a full snapshot, the result is identical. Standard list append satisfies this. If you have a custom reducer, verify this property before enabling DeltaChannel.
Migration: Transparent for existing threads. No data migration required. Old threads continue working; new threads use delta storage going forward. Deep Agents v0.6+ enables DeltaChannel by default for messages and files channels.
Per-Node Timeouts
Previously, adding timeouts required wrapping each node function with asyncio.wait_for or threading.Timer and wiring up error propagation yourself. LangGraph 1.2 makes this a graph-level declaration.
Simple wall-clock limit:
# Fail the node if it takes more than 60 seconds
builder.add_node("call_model", call_model_fn, timeout=60)
TimeoutPolicy with run_timeout:
from langgraph.types import TimeoutPolicy
# Run timeout: caps total execution time regardless of progress
builder.add_node(
"call_model",
call_model_fn,
timeout=TimeoutPolicy(run_timeout=120)
)
TimeoutPolicy with idle_timeout:
# Idle timeout: resets when the node makes progress
# Useful when you want to allow slow-but-steady nodes
# but catch completely frozen ones
builder.add_node(
"call_model",
call_model_fn,
timeout=TimeoutPolicy(idle_timeout=30)
)
Combined:
builder.add_node(
"call_model",
call_model_fn,
timeout=TimeoutPolicy(run_timeout=120, idle_timeout=30)
)
A node using idle_timeout should signal progress with heartbeats:
def long_task(state):
for chunk in stream_from_api():
runtime.heartbeat() # Reset the idle timeout clock
process(chunk)
Without heartbeats, any node doing IO that pauses mid-stream will time out even if it is making slow progress.
When to use which:
run_timeout: Hard cap. Use for nodes that call external APIs where you have an SLA. The node gets exactly that long, period.idle_timeout+ heartbeats: Flexible cap. Use for nodes doing streaming work where total duration is unpredictable but stalls should not be tolerated.- Both combined: For nodes where you want a hard ceiling but also want to catch intermediate freezes.
Node-Level Error Handlers
Retry policies handle transient failures. Error handlers handle the case where all retries are exhausted and you need to recover rather than fail the whole run.
The pattern is borrowed from saga/compensation architectures: if a step fails after retries, execute a compensating action to undo or route around the failure.
from langgraph.errors import NodeError
from langgraph.types import Command
def payment_error_handler(state: State, error: NodeError) -> Command:
# NodeError fields:
# error.node — name of the failed node
# error.error — the exception that was raised
return Command(
update={"status": f"compensated: {error.error}"},
goto="finalize", # Route to a recovery node
)
builder.add_node(
"charge_payment",
charge_payment_fn,
error_handler=payment_error_handler
)
Execution order:
- Node raises an exception.
- RetryPolicy decides whether to retry. If
max_attemptsnot exceeded, retry. - Once retries are exhausted, the error handler runs.
- The handler returns a
Commandthat updates state and routes to a recovery node.
Combining with RetryPolicy:
from langgraph.types import RetryPolicy
retry = RetryPolicy(
initial_interval=1.0,
backoff_factor=2.0,
max_interval=32.0,
max_attempts=5,
jitter=True,
retry_on=[TimeoutError, ConnectionError]
)
builder.add_node(
"charge_payment",
charge_payment_fn,
retry=retry,
error_handler=payment_error_handler
)
This gives you: retry up to 5 times with exponential backoff, and if all five fail, run the compensation handler instead of failing the run.
Important: retry storms in parallel agents
The default per-node retry policy does not coordinate across parallel agents. If you have 10 parallel workers each configured with max_attempts=10, and a downstream service goes down, you will generate 100 simultaneous retry attempts against that service. To prevent this, implement a circuit breaker via shared graph state:
def worker_node(state: State):
if state.get("failed_services", {}).get("payment_api"):
return {"status": "skipped", "reason": "circuit open"}
try:
return call_payment_api(state)
except Exception as e:
raise
def payment_error_handler(state: State, error: NodeError) -> Command:
return Command(
update={
"status": "failed",
"failed_services": {
**state.get("failed_services", {}),
"payment_api": True # Trip the circuit
}
},
goto="finalize"
)
Parallel workers check failed_services before calling the downstream service. One worker trips the circuit; the rest skip.
Graceful Shutdown
The previous way to stop in-flight runs was external — kill the process, lose the in-flight superstep. With graceful shutdown, you can request a cooperative drain: the graph finishes its current superstep, saves a checkpoint, and stops. The run can be resumed from the checkpoint later.
from langgraph.types import RunControl
from langgraph.errors import GraphDrained
import signal
control = RunControl()
def handle_sigterm(sig, frame):
control.request_drain("sigterm")
signal.signal(signal.SIGTERM, handle_sigterm)
try:
graph.invoke(
{"input": "data"},
control=control
)
except GraphDrained:
# A resumable checkpoint was saved
print("Drained cleanly. Resume with the same thread config.")
Behavior details:
- Cooperative, not preemptive. The drain happens between supersteps; it never cancels a node that is mid-execution.
- Thread-safe.
request_drain()can be called from any thread, including signal handlers. - Saves a resumable checkpoint at the drain point. Use the same
thread_idto resume.
Limitation: Graceful shutdown does not cancel asyncio tasks or threads that are already running inside a node. For hard upper bounds, pair it with a timeout at the node level and external cancellation on the asyncio event loop.
Type-Safe Streaming (v2)
LangGraph 1.2 introduces stream_version="v2" where every emitted chunk is a self-describing StreamPart TypedDict:
for chunk in graph.stream(
{"input": "data"},
stream_version="v2"
):
# chunk is a TypedDict with:
# chunk["type"] — "values" | "updates" | "messages"
# chunk["ns"] — list of node names (namespace path)
# chunk["data"] — the actual payload
if chunk["type"] == "messages":
print(chunk["data"])
Previously, stream output type depended on the stream_mode parameter and required the caller to know the mode to interpret the chunk. The v2 format is self-describing: any consumer can determine what a chunk represents without additional context.
invoke() also gets a typed return:
from langgraph.types import GraphOutput
result: GraphOutput = graph.invoke({"input": "test"})
typed_value = result.value # Type-safe access to output state
interrupts = result.interrupts # Access pending interrupt records
Human-in-the-Loop (Standard Practice in 2026)
Not a new feature in 1.2, but worth covering as context for the error handler and timeout patterns above. As of 2026, 60% of production agent systems include human intervention points. LangGraph’s interrupt mechanism is the standard implementation.
Static interrupt (pause before a node):
from langgraph.checkpoint.postgres import AsyncPostgresSaver
checkpointer = AsyncPostgresSaver(connection_string="postgresql://...")
graph = builder.compile(
checkpointer=checkpointer,
interrupt_before=["execute_code", "write_db"]
)
config = {"configurable": {"thread_id": "run_42"}}
graph.invoke({"query": "..."}, config=config)
# Human reviews, approves, then:
graph.invoke(None, config=config) # Resume
Dynamic interrupt (pause within a node):
from langgraph.types import interrupt
def approve_node(state):
decision = interrupt({
"action": "request_approval",
"cost": "$1000",
"plan": state["plan"]
})
if not decision.get("approved"):
return {"status": "rejected"}
return {"status": "approved"}
Dynamic interrupts are useful when the decision to pause depends on runtime state — for example, pausing only when the estimated cost exceeds a threshold.
LangSmith Deployment (formerly LangGraph Platform)
The deployment surface was renamed and simplified. langgraph deploy is the new command; langgraph up still works but is deprecated.
Deployment options:
| Tier | Description | Who |
|---|---|---|
| Cloud (SaaS) | Fully managed, fastest path | Plus / Enterprise |
| BYOC | Runs in your VPC, LangChain control plane | Enterprise |
| Self-Hosted | Fully on-premises | Enterprise |
| Developer | Free up to 100K nodes/month | Individual |
Pricing:
- LangSmith Plus: $39/user/month
- Deployment runs: $0.005 per end-to-end agent invocation
- Nodes within a single run are not charged separately
Observability via LangSmith:
import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "<key>"
# Graph executions are automatically traced — no instrumentation needed
agent.invoke({"messages": [...]})
LangSmith now connects deployment server logs directly to traces, so you can view system logs from the trace UI without switching contexts.
Production Checklist for LangGraph 1.2
These are the things to verify before shipping a LangGraph agent to production:
- Persistent checkpointer — Use
AsyncPostgresSaver(or equivalent) in production. In-memory checkpointers lose state on restart. - DeltaChannel on accumulating state — If your agent accumulates messages or file contents over many turns, enable DeltaChannel. The storage cost reduction is dramatic.
- Per-node timeouts on external calls — Any node that calls an external API should have at least a
run_timeout. Nodes that stream should addidle_timeout+ heartbeats. - Error handlers on critical nodes — Nodes where failure should trigger compensation (payments, database writes, email sends) should have explicit error handlers rather than relying on run failure.
- Circuit breaker for parallel agents — If you run agents in parallel and they share a downstream dependency, implement circuit breaker via shared state.
- Graceful shutdown on SIGTERM — Register a
request_drain()handler so deployments and rolling restarts do not discard in-flight work. - Interrupts before dangerous operations — Code execution, database writes, and external API calls with side effects should have
interrupt_beforeor dynamic interrupt checks. - LangSmith tracing enabled — Add
tagsandmetadataper run so you can filter traces by customer, session type, or version later.
What Companies Are Running on LangGraph
Uber and LinkedIn use LangGraph for durable agent workflows. JP Morgan, BlackRock, and Klarna use it for financial and payment approval workflows. Cisco runs enterprise multi-agent orchestration on it. Replit uses it for code generation agents.
The production hardening in 1.2 — especially DeltaChannel and per-node timeouts — came directly from feedback from these deployments, where storage costs and runaway nodes were the most-reported pain points.
Summary
LangGraph 1.2 (May 12, 2026) is a production reliability release. If you have been waiting to move a LangGraph prototype to production, the four features in this release — DeltaChannel, per-node timeouts, node-level error handlers, and graceful shutdown — close most of the gap between prototype and production-grade agent infrastructure.
The upgrade from 1.0 or 1.1 is non-breaking. DeltaChannel migration for existing threads is transparent. Start with DeltaChannel on any accumulating state channel, add timeouts to external-facing nodes, and wire up error handlers on nodes where failure has side effects. The rest of the checklist above can be layered in incrementally.