

“Set your heart upon your work, but never on its reward.”
Bhagavad Gita

“Set your heart upon your work, but never on its reward.”
Bhagavad Gita
Sometime in late 2024, "vibe coding" went from an ironic Twitter term to an actual development practice. The idea was simple: describe what you want, let the AI write it, ship it. Don't read the code too carefully. Trust the vibes.
For a while, it worked — or at least appeared to. Prototypes shipped faster. Hackathon demos looked impressive. Junior developers felt 10x more productive. Spotify reportedly had senior engineers who hadn't written code themselves since December 2025, relying entirely on AI agents for implementation.
Then the bugs started.
Not the obvious kind. Not syntax errors or missing imports. The insidious kind — race conditions in code nobody on the team fully understood, security vulnerabilities in authentication flows that "looked right," architectural decisions that seemed fine until they didn't scale. The kind of bugs that take weeks to debug because nobody can explain why the code works the way it does.
This is comprehension debt, and it's the real cost of vibe coding that nobody budgeted for.
Technical debt is well-understood: it's the gap between how code should be written and how it was written, usually due to time pressure. You know the code is suboptimal. You made a conscious tradeoff.
Comprehension debt is different. It's the gap between code that exists in your codebase and your team's ability to understand it. You didn't make a tradeoff — you never understood the code in the first place.
The distinction matters because the remediation strategies are completely different:
Every line of AI-generated code that a developer ships without understanding is a deposit into a comprehension debt account that charges compound interest.
The Spotify story — senior engineers fully delegating implementation to AI agents — gets cited as either a triumph or a warning depending on who's telling it. Here's what's actually happening in organizations that went all-in on AI-generated code:
The first 3 months feel incredible. Velocity metrics go through the roof. Sprint commitments are met ahead of schedule. Managers are thrilled. The AI writes clean, well-structured code that passes tests and code review.
Months 4-8 feel normal. Some weird bugs appear, but they get fixed. The codebase grows faster than anyone expected. New team members onboard by reading AI-generated code that's well-commented and reasonably organized.
Month 9 is when things break. A production incident requires understanding a complex interaction between three services. Nobody can explain the retry logic in the message queue consumer. The AI wrote it six months ago based on a prompt that said "handle failures gracefully." It does handle failures — most of the time. But the edge case that's now causing data loss? Nobody can explain why the code behaves the way it does in that scenario, because nobody ever understood it.
This pattern is repeating across the industry. The code is correct until it isn't, and when it isn't, the team lacks the mental model to diagnose the problem.
The obvious objection: "We review all AI-generated code before merging."
Code review catches what you can see. It catches style violations, obvious logic errors, missing error handling. What it doesn't catch — what it can't catch — is whether the reviewer actually understands the code's behavior in all cases.
When a human writes code, the reviewer is checking against their mental model of what the code should do. When AI writes code, the reviewer is often pattern-matching: "Does this look like correct code?" That's a fundamentally different cognitive task. You're evaluating form, not understanding substance.
I've watched experienced engineers approve AI-generated code that used a debouncing pattern they'd never seen before. The code worked. The tests passed. But when asked to explain the timing behavior under high load, they couldn't. They approved the shape of the solution without understanding the mechanics.
// AI-generated debounce with backpressure handling
// A reviewer might approve this because it "looks right"
// But can they explain what happens when events arrive
// faster than the flush interval under memory pressure?
function createAdaptiveDebounce<T>(
fn: (batch: T[]) => Promise<void>,
options: { baseInterval: number; maxBatch: number; backpressureThreshold: number }
) {
let buffer: T[] = [];
let timer: ReturnType<typeof setTimeout> | null = null;
let currentInterval = options.baseInterval;
let inflightCount = 0;
const flush = async () => {
if (buffer.length === 0) return;
const batch = buffer.splice(0, options.maxBatch);
inflightCount++;
if (inflightCount > options.backpressureThreshold) {
currentInterval = Math.min(currentInterval * 2, options.baseInterval * 10);
}
try {
await fn(batch);
} finally {
inflightCount--;
if (inflightCount <= options.backpressureThreshold / 2) {
currentInterval = options.baseInterval;
}
}
if (buffer.length > 0) {
timer = setTimeout(flush, currentInterval);
}
};
return (item: T) => {
buffer.push(item);
if (!timer) {
timer = setTimeout(flush, currentInterval);
}
};
}This code is fine. It handles backpressure, manages batch sizes, and adapts timing under load. But if no one on the team understands the interaction between inflightCount, currentInterval, and the recursive setTimeout in flush, then the team owns code it can't maintain.
Here's where comprehension debt becomes acutely painful: debugging.
Debugging is fundamentally about forming a hypothesis about what the code should do, then finding where reality diverges. If you don't have a mental model of what the code should do — because you never wrote or deeply read it — you can't form that hypothesis. You're reduced to staring at logs and adding print statements.
I've seen debugging sessions that should take an hour stretch into days because the developer is reverse-engineering their own codebase. They're reading AI-generated code for the first time, during a production incident, under pressure. That's the worst possible context for building understanding.
The irony is brutal: the AI that wrote the code could probably explain it. But the AI doesn't have the production context, the deployment configuration, the interaction patterns, or the specific state that's causing the failure. It can explain the code in theory. It can't debug the code in practice — not yet, anyway.
I'm not arguing against AI-assisted development. I use it constantly. The distinction is between AI-assisted coding and AI-replaced coding.
The minimum viable practice: if AI writes it, you read it. Not skim it. Read it like you wrote it. If you can't explain a function's behavior to a colleague, you don't understand it well enough to ship it.
This slows you down. That's the point. The speed gain from vibe coding is an illusion — you're borrowing velocity from your future self.
Let AI handle boilerplate, repetitive patterns, and well-understood implementations. Write the business logic, the state machines, the error handling for complex workflows, and the performance-critical paths yourself.
The heuristic I use: if a bug in this code would wake me up at 3am, I should understand every line. If it would generate a low-priority ticket, AI can handle it.
When you import a library, you read the docs, understand the API surface, and have a mental model of its behavior. You don't read every line of source code, but you understand what it does and how it fails.
Apply the same standard to AI-generated code. You don't need to understand every character, but you need a clear mental model of what it does, what it assumes, and how it behaves at the boundaries.
The teams that use AI effectively spend their time on system design, interface definitions, and architecture decisions — then let AI implement within those constraints. The teams that struggle spend their time crafting prompts and hoping the AI makes good architectural choices.
// Good: You define the architecture, AI implements the details
interface OrderProcessor {
validate(order: Order): Result<ValidOrder, ValidationError>;
calculatePricing(order: ValidOrder): PricingResult;
executePayment(pricing: PricingResult): Promise<PaymentResult>;
fulfillOrder(payment: PaymentResult): Promise<FulfillmentResult>;
}
// Bad: "Write me an order processing system that handles
// validation, pricing, payments, and fulfillment"When you define the interfaces, you control the architecture. The AI fills in implementation details within boundaries you understand.
The backlash against vibe coding isn't about rejecting AI — it's about rejecting the fantasy that understanding code is optional. Every previous wave of abstraction (high-level languages, frameworks, ORMs) succeeded because they reduced what you needed to understand, not because they eliminated understanding.
AI-generated code doesn't reduce what you need to understand. It increases it. There's more code, written faster, with patterns you might not recognize. The only sustainable approach is to match the increase in code volume with an investment in comprehension.
Vibe coding is dead not because it doesn't work, but because the debt it creates is unsustainable. The teams that thrive with AI tools will be the ones that treat understanding as non-negotiable — even when the AI makes it tempting to skip.
The vibes were never enough. They were always a loan.

Developers spent decades wishing for tools that write code. Now they have them. Why does freedom feel like loss?

Shadow IT on steroids, MCP tools nobody asked for, LLMs playing architect, vibe-coded open source, and text-to-SQL fantasies. The antipatterns everyone's falling into — and how to stop.

OpenAI pulled GPT-4o after it became dangerously agreeable. The incident exposes a fundamental tension in how we train AI systems — and it won't be the last recall.