Persist trace_id and parent span id in the checkpoint. On resume, restore them as the active context so post-pause spans land in the same trace.
Imagine a relay race where the runner pauses to wait for a teammate's signal. If they drop the baton, the next leg starts a brand-new race and nobody can tell the two halves apart on the leaderboard. The baton is the trace context. The checkpointer has to hold both the runner's position on the track and the baton, then hand both back when the race resumes. If you only save the position, you finish a new race instead of completing the original one, and the spectators watching the timeline see two unrelated events instead of one paused and resumed run.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
LangGraph's checkpointer was built so a workflow could outlive its process. A graph can pause at a node, write state to durable storage, and resume hours or days later from a different machine. The application logic survives the gap. The tracing context, by default, does not.
That asymmetry shows up the first time someone asks "how long did the whole approval take, end to end?" on the observability dashboard. The pre-pause work is one trace. The post-resume work is another. Nothing connects them. This deep dive walks through why the lifecycle mismatch happens, what to persist to close the gap, and how each major backend stitches the halves back together.
Why the trace context disappears at the pause
An OpenTelemetry context is an in-memory object tied to a thread or async task. The active trace_id and parent span id live there. When the LangGraph node returns control to the runtime and the runtime calls the checkpointer to persist state, only the dictionary you handed in gets serialized. The OTel context sits beside it, in a thread-local or contextvars storage that the checkpointer knows nothing about.
If the process exits after the checkpoint, the context is gone. If the process stays up and a different request comes in next, the context belongs to that other request now. Either way, when the resume eventually happens, the runtime has graph state in hand but no notion of which trace it came from.
The first span emitted after resume calls tracer.start_as_current_span(...) and the SDK does what it always does when there is no active parent: it creates a fresh trace_id and treats this span as the root. From the SDK's view nothing is wrong. From your view the original request and its eventual completion are now two unrelated entries in the trace list.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
from langgraph.checkpoint.sqlite import SqliteSaver
from opentelemetry import trace, context as otel_context
from opentelemetry.propagate import inject, extract
tracer = trace.get_tracer(__name__)
def before_pause(state, span):
carrier = {}
inject(carrier)
state['_trace_carrier'] = carrier
return state
def after_resume(state):
carrier = state.get('_trace_carrier', {})
ctx = extract(carrier)
return otel_context.attach(ctx)
# In the human-approval node
with tracer.start_as_current_span('await_human_approval') as span:
state = before_pause(state, span)
checkpointer.put(thread_id, state)
# On resume in a new process
state = checkpointer.get(thread_id)
token = after_resume(state)
try:
with tracer.start_as_current_span('post_approval_step'):
...
finally:
otel_context.detach(token)Real products, models, and research that use this idea.
- LangGraph 0.3 ships a `MemorySaver` and `SqliteSaver` whose checkpoint rows can carry arbitrary metadata; production teams piggyback the W3C traceparent in that metadata column.
- LangSmith renders checkpoint-resumed runs as a single trace when the `run_id` is reused via `RunnableConfig`, which is the LangChain-native version of context propagation.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow would you handle a workflow that branches into parallel approval sub-flows that each resume independently?
Each branch needs its own carrier persisted under its node id; on rejoin, the merging node opens a span whose parent is the original pre-branch span and adds links to each branch's resume span using OTel span links.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Treating the pause as a span end. The post-resume work then starts a fresh trace with no link back to the request that triggered the approval.
60 second bullets to scan on the way to the call.
Why the trace context is lost when the LangGraph process exits
What two values must be persisted to restore tracing on resume
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.