LangChain vs LangGraph: The Difference and When to Use Each
The confusion is understandable, and it's costing teams real time
If you've spent any time building with LLMs in the last year, you've probably hit this question: do I need LangChain, LangGraph, or both? The confusion is fair. The naming suggests LangGraph is the "next version" of LangChain, the way Vue 3 replaced Vue 2. It isn't. LangGraph is not a LangChain upgrade, and it's not a competitor either. It's a separate orchestration layer that sits on top of (and alongside) LangChain, built specifically for the moment your application stops being a straight line and starts being a loop.
Here's the short version, which we'll unpack for the rest of this article: LangChain gives you the building blocks — model wrappers, prompt templates, retrievers, tool interfaces, memory abstractions. LangGraph gives you a graph-based state machine for wiring those building blocks into flows that branch, loop, pause for human approval, and resume after a crash. Most production agent systems end up using both. LangChain provides the parts; LangGraph provides the wiring diagram for when the wiring gets complicated.
This matters because picking the wrong abstraction early costs you a rewrite later. Teams that reach for LangGraph on day one for a two-step summarization pipeline end up with unnecessary ceremony — nodes and edges and state schemas for something a single function could do. Teams that stay on plain chains past the point where they need conditional loops end up bolting on manual while loops and ad hoc retry logic that a graph would have handled natively. Let's get the mental model right so you can make that call correctly the first time.
What LangChain actually is
LangChain is a library for composing LLM calls with the surrounding scaffolding: prompts, models, output parsers, retrievers, tools, and memory. Its core abstraction for building pipelines is LCEL (LangChain Expression Language), which lets you chain components with the pipe operator.
The mental model for LCEL is a directed, acyclic pipeline. Data flows in one direction, through a fixed sequence of steps, and comes out the other end. There's no native concept of "go back to step 2 if the output of step 4 looks wrong" — LCEL isn't built for cycles. It's built for composition: take a prompt, feed it to a model, parse the output, maybe feed that into another prompt.
Here's a simple LCEL chain — a support-ticket classifier that takes raw customer text and returns a structured category:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "Classify the support ticket into: billing, bug, feature_request, or other. Reply with one word only."),
("human", "{ticket_text}"),
])
chain = prompt | model | StrOutputParser()
result = chain.invoke({"ticket_text": "My invoice charged me twice this month"})
print(result) # "billing"That's the whole shape of LangChain's core value proposition: composable pieces, glued with |, executed top to bottom. You can make this more elaborate — add a retriever for RAG, add a tool-calling model, add structured output with Pydantic — but the execution model stays linear. Input goes in one end, transformations happen in sequence, output comes out.
This is enough for a huge share of real LLM features: classification, summarization, extraction, single-shot RAG question answering, simple tool-augmented chat. If your flow can be described as "step 1, then step 2, then step 3, done," you don't need anything more than LangChain and LCEL.
Where the linear model breaks down
The cracks show up the moment your application needs to make a decision about its own next step, based on something that happened mid-flow. A few concrete examples:
- A research agent that searches, reads results, decides the results are insufficient, and searches again with a refined query — potentially several times, with no fixed number of iterations.
- A coding agent that writes code, runs tests, and loops back to fix errors until tests pass or a retry budget is exhausted.
- A support agent that tries to resolve a ticket automatically, but routes to a human reviewer when confidence is low, then resumes automated handling once the human responds.
- A multi-agent system where a planner agent hands off to a specialist agent, which may hand back to the planner if the task changes.
None of these are "step 1 then step 2 then step 3." They're graphs. They have branches (which path do we take based on this output?), loops (retry until some condition holds), and often pauses (wait for a human, wait for an external event, resume later — potentially after the process itself has restarted).
You can force this into LCEL with escape hatches — a Python while loop wrapping chain.invoke(), manual state dictionaries passed around, custom retry logic. It works for a while. But you're now hand-rolling the exact thing LangGraph was built to formalize: explicit state, explicit transitions, and a runtime that understands both well enough to persist and resume them.
What LangGraph actually adds
LangGraph models your application as a graph: a set of nodes (units of work — usually a function or an LLM call) connected by edges (which node runs next). Some edges are conditional — the graph inspects the current state and decides where to go, which is how you get branching and looping without writing your own control-flow logic on top of LCEL.
The three things LangGraph gives you that LCEL fundamentally doesn't:
- Explicit, typed state. You define a state schema up front (often a
TypedDictor Pydantic model), and every node reads from and writes to that shared state. There's no ambiguity about what data is available at any point in the flow. - Conditional edges and cycles. A node's output can determine which node runs next, including looping back to a previous node. This is the actual mechanism behind "retry until tests pass" or "keep researching until confidence is high enough."
- Checkpointing and persistence. LangGraph can snapshot the state after every step. That means you can pause a run — for a human-in-the-loop approval, for example — and resume it later, even after a process restart, from exactly where it left off. This is the piece that's genuinely hard to build yourself and is arguably LangGraph's biggest practical advantage over hand-rolled loops.
Here's the same underlying idea as the classifier above, but extended into something that actually needs a graph: a ticket handler that classifies a ticket, attempts an automated resolution, and loops back for a retry if the resolution attempt fails validation — capped at three attempts, otherwise escalating to a human.
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
class TicketState(TypedDict):
ticket_text: str
category: str
resolution: str
attempts: int
resolved: bool
def classify_node(state: TicketState) -> dict:
category = chain.invoke({"ticket_text": state["ticket_text"]})
return {"category": category}
def resolve_node(state: TicketState) -> dict:
resolution = resolution_chain.invoke(state)
return {"resolution": resolution, "attempts": state["attempts"] + 1}
def validate_node(state: TicketState) -> dict:
is_valid = validate_resolution(state["resolution"])
return {"resolved": is_valid}
def route_after_validation(state: TicketState) -> Literal["retry", "escalate", "done"]:
if state["resolved"]:
return "done"
if state["attempts"] >= 3:
return "escalate"
return "retry"
graph = StateGraph(TicketState)
graph.add_node("classify", classify_node)
graph.add_node("resolve", resolve_node)
graph.add_node("validate", validate_node)
graph.add_node("escalate", escalate_to_human_node)
graph.set_entry_point("classify")
graph.add_edge("classify", "resolve")
graph.add_edge("resolve", "validate")
graph.add_conditional_edges("validate", route_after_validation, {
"retry": "resolve",
"escalate": "escalate",
"done": END,
})
app = graph.compile()Notice what's happening: classify_node and resolve_node are still calling LangChain chains internally — chain.invoke() is the exact LCEL chain from the earlier example. LangGraph isn't replacing that logic; it's providing the outer skeleton that decides when resolve runs again versus when the whole thing escalates. That add_conditional_edges call is the piece LCEL has no equivalent for — a loop back to resolve that runs an unbounded number of times until a condition changes.
Do you actually need LangGraph? A practical test
Before reaching for LangGraph, run your planned flow through three questions:
- Does the number of steps depend on runtime output? If you can draw your flow as a fixed sequence of boxes before you've run it once, you probably don't need a graph. If the number of iterations depends on what the model says at runtime, you do.
- Do you need to pause and resume — especially across a process restart? Human-in-the-loop approval, async webhook callbacks, or "come back to this conversation tomorrow" all need durable state. LCEL has no built-in persistence story; you'd be building your own.
- Do multiple independent actors need to hand control back and forth? Multi-agent handoff (planner to specialist to planner) is naturally a graph with named nodes, not a chain.
If the honest answer to all three is no, stick with LCEL. A single prompt | model | parser chain wrapped in a normal Python function is easier to read, easier to test, and easier for the next engineer to onboard onto than a graph with one node and no branches. Don't add a state machine to justify having installed the library — that's solving a problem you don't have yet at the cost of one you do have now: unnecessary complexity.
The overkill failure mode is real and common. A StateGraph with a single linear path from entry to END is just a chain wearing a costume. If your conditional edges always resolve to the same next node, you've built infrastructure for branching you never use.
How they compose in a real project
In practice, a mature LLM application isn't "LangChain OR LangGraph" — it's LangChain inside LangGraph, at multiple levels of granularity. A realistic architecture looks like this:
- Individual nodes are LangChain chains. Each node in your graph — the retriever step, the generation step, the tool-calling step — is typically built with LCEL, because that's the right tool for "take this input, run it through a model and a parser, get structured output."
- The graph is the orchestrator. LangGraph decides which node runs next, holds the shared state, and owns retry/loop/branch logic.
- Tools and retrievers are shared LangChain objects referenced from multiple nodes. You don't rebuild your vector store retriever per node — you define it once with LangChain's retriever interface and call it from whichever node needs it.
- Checkpointing lives at the graph level. You attach a checkpointer (LangGraph ships adapters for SQLite, Postgres, and others) to the compiled graph, and every node's state update gets persisted automatically. Your LCEL chains inside the nodes don't know or care that this is happening.
This is the healthiest way to think about the relationship: LangChain is your component library, LangGraph is your runtime for wiring components into a stateful application. You wouldn't ask "do I use React components or Redux?" — you use React components, and Redux (or whatever state layer) governs how state flows between them when the app gets complex enough to need it. LangChain and LangGraph split responsibilities the same way.
The migration path: starting simple, growing into a graph
You don't need to decide this on day one. The realistic path most working systems take:
- Start with a single LCEL chain. Prompt in, model, parser, done. Ship it. Most features never need more than this.
- Add tool calling and a basic agent loop. When the model needs to call functions and react to their output, you're still often fine with LangChain's built-in agent executors for simple cases — one tool call, one response.
- Notice the loop growing unbounded or conditional. This is the signal. If you find yourself writing
for attempt in range(N):around a chain invocation, checking a condition, and deciding whether to call it again — that's a graph, informally. You've already built the state machine; you just haven't named the states. - Port the informal loop into a `StateGraph`. Take your existing LCEL chains — don't rewrite them — and wrap them as node functions. Define the state schema based on what your manual loop was already tracking (attempts, intermediate results, flags). Replace the
forloop andifstatements withadd_conditional_edges. - Add a checkpointer once you need durability. If a run needs to survive a process restart or wait on a human for an indeterminate amount of time, attach a checkpointer to the compiled graph. This is usually the last piece added, because it's the piece you don't need until you're running in production with real users waiting on real approvals.
The reassuring part of this path: nothing you built in step 1 gets thrown away in step 4. Your prompts, your parsers, your retrievers — all of it slots directly into LangGraph nodes unchanged. The migration is additive, not a rewrite. That's the practical payoff of LangGraph being built to compose with LangChain rather than replace it.
Common mistakes teams make with this decision
A few patterns show up repeatedly when teams get this wrong in either direction:
- Reaching for LangGraph before there's a loop to justify it. If your onboarding doc for a new hire needs a diagram to explain a flow that has no branches, you've added abstraction without adding capability. Simplify back to LCEL.
- Staying on manual `while` loops long after checkpointing became a requirement. This shows up as "why does the agent restart from scratch when the server redeploys" — a symptom of state living in memory instead of being checkpointed. That's the exact problem LangGraph's persistence layer solves.
- Treating the state schema as an afterthought. In LangGraph, the state schema is the contract between all your nodes. If it's loosely typed (a generic dict with no schema), you get the same class of bugs untyped global state always causes — a node silently expecting a key that a previous node forgot to set.
- Forgetting that nodes can be arbitrarily simple. A node doesn't have to be an LLM call. It can be a plain Python function that validates something, calls an external API, or transforms data. Overloading every node with an LLM call when a deterministic function would do is slower and more expensive than it needs to be.
- Not setting a hard cap on loops. Conditional edges make cycles trivial to write and just as trivial to leave unbounded. Always have an explicit exit condition — a max retry count, a max iteration count — independent of whatever "success" condition you're hoping the loop reaches. Runaway loops on a paid model API are an expensive bug to discover in production.
Cost and latency considerations
Neither library changes the fundamental cost math of calling an LLM, but the way you use them can make the bill and the latency worse or better in the same architecture. A LangGraph agent that loops five times before deciding it's done is making five (or more) model calls where a well-scoped LCEL chain might make one. That's not a reason to avoid LangGraph — sometimes the loop is genuinely necessary for the quality bar you need — but it is a reason to instrument it. Log the node sequence, track attempt counts, and set explicit budgets (max iterations, token ceilings, timeouts) on any graph that can cycle. The graph structure that gives you flexibility is the same structure that can silently run longer and cost more than a linear chain, if you don't cap it.
This is also where the distinction pays off in code review: when someone proposes adding a loop to a LangChain agent by hand, the right response is usually "that's a graph — model it as one," because a named, capped, checkpointed loop in LangGraph is far easier to reason about and debug than a while True buried inside application code.
Quick reference: which one for which job
- Single-shot classification, extraction, or summarization — LangChain / LCEL. No loop, no branching, ship the chain.
- Basic RAG question answering — LangChain. Retriever plus prompt plus model is a straight line.
- Simple tool-calling assistant, one or two tool calls per turn — LangChain is usually sufficient; escalate to LangGraph only if tool selection needs multi-step planning.
- Agent that retries until a quality bar is met — LangGraph. You need a conditional loop with a cap.
- Multi-agent handoff (planner, specialist, reviewer) — LangGraph. Each agent is a node; routing between them is the graph's job.
- Anything requiring human approval mid-flow, or resuming after a restart — LangGraph, with a checkpointer attached. There's no clean way to get durable pause/resume out of LCEL alone.
- Coding agents, research agents, or anything with an unbounded "keep going until done" shape — LangGraph, always. This is the category LangGraph was purpose-built for.
The pattern across all of these: the question is never "LangChain or LangGraph," it's "does this specific piece of my system need to loop, branch, or persist state across a pause." Answer that per component, not per project — a single application will usually have some parts that are plain chains and some parts that are graphs, and that's the correct outcome, not a compromise.
Getting this distinction right early saves you from two failure modes that both waste real engineering time: over-engineering a simple chain into a graph nobody needed, and under-engineering a genuinely cyclical agent into a brittle pile of manual retry logic that breaks the first time it needs to survive a restart. Neither library is "better" than the other because they're not answering the same question — LangChain answers "what are my components," LangGraph answers "how does control flow between them when that flow isn't a straight line."
If this is the kind of system-design judgment you're trying to build — not just which function to call, but when a linear chain stops being enough and a stateful graph becomes the right tool — that's exactly the muscle we train in depth inside Advanced AI Agents, where we walk through this exact LangChain-to-LangGraph migration on production-shaped agent systems, not toy examples.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.