Why I Stopped Using LangChain (And You Should Too)

LangChain was the jQuery of AI — necessary for a moment, then a liability. Modern AI engineering demands less abstraction, not more.

Why I Stopped Using LangChain (And You Should Too)

The Abstraction Tax

LangChain did something remarkable: it made AI accessible to developers who had no business building AI systems. And that's exactly the problem.

I spent two years building production systems with LangChain. I've read the source code end-to-end — every abstract base class, every mixin, every Runnable that spawns three more Runnables. I've contributed PRs. I've debugged production outages at 2 AM caused by framework internals I didn't know existed. And I'm telling you with the hard-won conviction of a recovering addict: it's time to move on.

The framework suffers from what I call "Abstraction Addiction" — every simple operation is wrapped in 3-5 layers of indirection. Want to make an API call to OpenAI? That's a BaseLanguageModel → BaseChatModel → ChatOpenAI → with a RunnableSequence → through an OutputParser. Five classes to do what fetch does in six lines. It's like building a trebuchet to throw a paper airplane.

{
  "type": "comparison",
  "left": {
    "title": "LangChain Way",
    "color": "purple",
    "steps": ["Your Code", "RunnableSequence", "ChatPromptTemplate", "BaseChatModel", "ChatOpenAI", "OutputParser", "Result"]
  },
  "right": {
    "title": "Direct Way",
    "color": "green",
    "steps": ["Your Code", "API Call", "Result"]
  }
}

The Day I Knew It Was Over

Let me tell you about the incident that broke me. We had a production chatbot built on LangChain serving ~2,000 users daily. One Tuesday, after a routine pip install --upgrade langchain, the entire system went down. Not because of a bug in our code — because LangChain renamed ConversationBufferMemory to something else, changed the import path, and deprecated three methods we were using. In a patch release.

I spent four hours reading changelogs, migration guides, and GitHub issues. Four hours. To fix code that does one thing: remember what the user said two messages ago. I could have implemented conversation memory from scratch in 45 minutes with a Python list.

That's when I opened a blank file and started writing the 200 lines of code that replaced our entire LangChain dependency. It took a weekend. The system has been more stable in the 8 months since than it was in the 2 years before.

The Real Cost

Here's what LangChain actually costs you — and I mean costs, with receipts:

  • Debugging hell. Stack traces are 40+ frames of internal framework calls. I once counted 53 frames between my code and the actual OpenAI API call. Good luck finding your bug. It's like playing Where's Waldo in a book that's entirely red and white stripes.
  • Version instability. Breaking changes every minor release. Your code from 3 months ago? Doesn't compile. The migration guides are longer than the actual documentation.
  • Performance overhead. I benchmarked it: LangChain adds 200-400ms of pure framework overhead per chain execution. At scale, that's real money. We were paying an extra $3,200/month in compute costs just to run LangChain's indirection layers.
  • Vendor lock-in disguised as abstraction. The "provider agnostic" promise is a lie. Switching from OpenAI to Anthropic still requires rewriting half your chain logic because the models handle system prompts, tool calling, and streaming differently, and LangChain's "universal" interface can't hide that.
  • Cargo cult complexity. Junior developers see LangChain code and think AI engineering is supposed to be this complicated. It's not. LangChain made simple things complex to justify its own existence.

The "But What About..." Objections

"But LangChain has great integrations!" — So does pip install. Every vector database, every LLM provider, every tool has its own SDK. You don't need a meta-framework to install libraries.

"But LCEL (LangChain Expression Language) is elegant!" — It's not. It's a custom DSL that looks like someone let a Haskell programmer loose in a Python codebase. The | pipe operator is clever exactly once, and then it's confusing for every engineer who reads your code afterward.

# LangChain LCEL — "elegant"
chain = prompt | model | parser | RunnablePassthrough.assign(
    context=itemgetter("question") | retriever
) | final_prompt | model | StrOutputParser()

# Python — actually elegant
async def answer(question: str) -> str:
    context = await retrieve(question)
    response = await client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": f"Context: {context}"},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

Look at those two snippets. Really look at them. The second one is debuggable, type-safe, and every Python developer on your team understands it immediately. The first one requires a PhD in LangChain-ology even to read.

"But Harrison Chase seems smart!" — He is smart. He identified a massive market need early and capitalized on it. That doesn't mean the framework is well-designed. jQuery's creator was smart too, and we still replaced jQuery.

The jQuery Parallel Is Exact

This is the comparison I keep coming back to. jQuery appeared when browsers were inconsistent and JavaScript was painful. It provided a unified interface and made the web accessible to a generation of developers. Sound familiar?

LangChain appeared when LLM APIs were inconsistent and AI engineering was painful. It provided a unified interface and made AI accessible to a generation of developers.

But here's what happened with jQuery: browsers got better, vanilla JavaScript improved, and React replaced the paradigm entirely. jQuery went from essential to a liability to a legacy dependency.

{
  "type": "comparison",
  "left": {
    "title": "jQuery Era (2006–2015)",
    "color": "amber",
    "steps": ["Browser Inconsistency", "jQuery Needed", "Browsers Improve", "jQuery = Liability"]
  },
  "right": {
    "title": "LangChain Era (2023–2026)",
    "color": "amber",
    "steps": ["LLM API Inconsistency", "LangChain Needed", "APIs Mature + Context Grows", "LangChain = Liability"]
  }
}

The same thing is happening now. OpenAI, Anthropic, and Google SDKs are excellent. Structured output is native. Tool calling is standardized. Function calling works out of the box. The problems LangChain solved in 2023 are solved by the providers themselves in 2026.

What To Use Instead

Just write code. Seriously. Here's my stack for production AI systems in 2026:

  1. Direct SDK callsOpenAI, Anthropic, and Google all have excellent SDKs with streaming, tool calling, and structured output built in.
  2. Instructor — For structured output with validation. One library, does one thing perfectly. Jason Liu understands that good libraries are small.
  3. Pydantic — For your data models. You already use it. Your LLM responses should be Pydantic models.
  4. Your own thin wrapper — 200 lines of code that does exactly what your app needs. Retry logic, model fallback, logging. That's it.
# My entire "framework" - ~50 lines for the core
import anthropic
from pydantic import BaseModel

class LLMClient:
    def __init__(self):
        self.client = anthropic.Anthropic()
        self.fallback = anthropic.Anthropic()  # or OpenAI
    
    async def generate(
        self, 
        prompt: str, 
        response_model: type[BaseModel] | None = None,
        max_retries: int = 3,
    ) -> str | BaseModel:
        for attempt in range(max_retries):
            try:
                response = await self.client.messages.create(
                    model="claude-sonnet-4-20250514",
                    max_tokens=4096,
                    messages=[{"role": "user", "content": prompt}],
                )
                text = response.content[0].text
                if response_model:
                    return response_model.model_validate_json(text)
                return text
            except Exception as e:
                if attempt == max_retries - 1:
                    raise
                await asyncio.sleep(2 ** attempt)

That's it. That's the framework. It handles retries, structured output, and it's 100% debuggable because you wrote it. When it breaks, you know exactly why, because there are no mystery abstractions between you and the API call.

The Liberation

The best AI code I've ever written looked like regular Python. No chains. No agents. No runnables. No expression languages. Just functions that call APIs and return data. Functions that a junior developer can read, understand, and modify on their first day.

The day you delete LangChain from your requirements.txt is the day you start actually engineering AI systems instead of configuring someone else's framework. And honestly? It feels fantastic. Like taking off a weighted vest you forgot you were wearing.

You don't need permission. You don't need a migration plan. Just start writing the code you wish you had, and you'll realize the framework was never saving you time — it was spending it.

Related Articles