← Back to blog
Sep 2, 2026·13 min read

Why AI Coding Agents Fail at Backends

Why AI Coding Agents Fail at Backends

The short answer: it isn't a capability gap, it's a feedback gap. When an agent gets a frontend wrong, you see it. The button doesn't render, the layout breaks, the colour is off. When it gets a backend wrong, everything looks correct. The login screen appears, the data loads, the tests pass. It works perfectly right up until there is a second user, and then it turns out the auth was a login form with nothing behind it.

Coding agents hallucinate at roughly 17.8% on code tasks, against an 8.2% average across all task types. But the rate matters less than where the errors land, and backends are where they land silently.

The verification loop is the whole difference

Building a frontend has an immediate, honest feedback loop. You look at it. If the agent invented a CSS property or referenced a component that doesn't exist, the page tells you within seconds.

Backends have no equivalent. The failure modes are:

  • An authorization check that returns true regardless of input
  • A database query missing its tenant filter
  • A row-level security policy that was written but never enabled
  • An API endpoint that validates the session but not the ownership of the record

Every one of those produces working software. The app runs. The tests pass, because the tests were written by the same agent against the same misunderstanding. And you're the only user, so nothing looks wrong.

This is why the same tool that builds you a beautiful dashboard in twenty minutes will hand you an auth system that leaks every record the moment someone changes an ID in a URL.

The specific things that go wrong

Phantom imports and invented signatures

The most-studied failure. In the definitive baseline study, researchers evaluated 16 LLMs across 576,000 generations and found package hallucination rates of at least 5.2% on commercial models and 21.7% on open-source ones, identifying 205,474 unique hallucinated package names.

A separate multi-language study found rates ranging from 0.22% to 46.15% depending on model and language, with JavaScript lowest at 14.73%, Python at 23.14% and Rust at 24.74%.

This has become a supply-chain problem, not just a quality one. A researcher who received the hallucinated package name huggingface-cli from ChatGPT registered an empty package under that name on PyPI. It received over 30,000 downloads in three months. In January 2026, an npm package called react-codeshift - a name corresponding to no real project - had propagated to 237 repositories through forks and was still receiving daily download attempts from AI agents.

Attackers now register hallucinated package names deliberately. Trend Micro named the technique slopsquatting.

Why it hits backends harder: a phantom frontend import breaks the build. A phantom backend dependency might install fine, do roughly what the name suggests, and quietly not do the security part.

Auth that authenticates but doesn't authorise

The single most common backend failure in agent-generated code, and it's a category error rather than a bug.

Authentication answers "who is this?" Authorization answers "may they have this?" An agent asked to "add login" produces the first and frequently skips the second, because the prompt didn't distinguish them and neither did most of the tutorials in its training data.

The result is a system where every logged-in user can read every record if they change an identifier.

A 30-second test on any app you've built this way:

  1. Open devtools, Network tab
  2. Find the request that fetches your data
  3. Change the user or record ID in the request
  4. Replay it

If you get somebody else's data back, your auth is a login screen with nothing behind it. This test fails on a surprising number of shipped applications, and it's not a skill gap - it's the default the tools produce.

Row-level security that was written but never enforced

An agent asked to add multi-tenancy in Postgres will typically write a policy that looks correct:

CREATE POLICY tenant_isolation ON records
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

And omit the two lines that make it do anything:

ALTER TABLE records ENABLE ROW LEVEL SECURITY;
ALTER TABLE records FORCE ROW LEVEL SECURITY;

Without ENABLE, the policy exists and is never applied. Without FORCE, the table owner bypasses it, which means your migration tooling and any admin connection see everything.

The application behaves identically either way during development. You find out when a customer does.

Tenant context taken from the request

The subtlest one. An agent will happily write an endpoint that accepts tenant_id as a parameter and filter on it:

@app.get("/records")
def get_records(tenant_id: str):
    return db.query("SELECT * FROM records WHERE tenant_id = %s", tenant_id)

This is correct-looking, testable, and completely broken. The caller supplies the tenant. Anyone can supply any tenant.

The fix is to take it from the verified session and never from the request:

@app.get("/records")
def get_records(session = Depends(verify_session)):
    with scoped_connection(session.tenant_id) as conn:
        return conn.query("SELECT id, name, status FROM records")

An agent won't make this distinction unless you make it, because both versions satisfy the instruction "return the records for this tenant."

Why more capable models don't fix it

Hallucination rates have fallen sharply - from 15-45% in 2024 baselines to between 3.1% and 19.1% in 2026 depending on model and task. Progress is real.

But three things haven't changed.

The mechanism is the same. Next-token prediction optimises for the most likely continuation. When the most likely continuation is wrong, the model is wrong, and it is wrong confidently.

Errors compound across agent steps. Agentic coding tasks consume roughly 1,000 times more tokens than single-turn code reasoning, because full context is re-read at every step. More steps means more opportunities for a small early error to become the premise of everything after it.

Harder evaluations reveal what easy ones hide. When Vectara moved to a more difficult dataset, hallucination rates for top models jumped from under 2% to over 10%. Benchmarks measure what they measure.

And none of this addresses the actual problem, which is that backend errors don't surface. A model that hallucinates half as often still produces auth with no authorization check, because that isn't a hallucination. It's a correct implementation of an underspecified request.

The four failures, side by side

FailureLooks likeSurfaces whenWhere the fix belongs
Phantom dependencyA plausible import that installsAudit, or a supply-chain incidentLockfile review, allow-listed registries
Auth without authorizationWorking login, loading dataA second user changes an IDPer-record ownership check on every read
RLS written, not enabledA policy visible in the schemaA customer sees another customer's rowENABLE plus FORCE ROW LEVEL SECURITY
Tenant from the requestA clean, testable endpointAnyone edits the parameterContext set from a verified session token

What actually works

Constrain the surface, not the prompt

Adding "make sure to check permissions" to a prompt is a filter. It changes what usually happens. It doesn't change what can happen, and it fails silently.

The alternative is making the wrong thing impossible. If tenant isolation is enforced by a database policy rather than by application code, an agent writing a query that forgets the filter gets no rows back rather than everyone's rows. The mistake becomes visible instead of dangerous.

That's the general principle: move the guarantee below the layer the agent is generating.

Give the agent a schema it can't circumvent

An agent that writes raw SQL has an unbounded surface. An agent that calls declared tools has whatever surface you declared. This is why MCP has become the default integration layer for agent tooling - not because it's better plumbing, but because the tool list is something you can review.

The caveat: a tool taking a table name as an argument reintroduces the unbounded surface. run_query(sql) and fetch_record(table, id) are enormously useful and they collapse the whole model.

Review for the right things

Code review has shifted when the author is a model. What matters now is confident-sounding code using unfamiliar APIs, suspiciously precise but unverified function signatures, and dependencies the team hasn't used before - those signals matter more than stylistic feedback.

A practical checklist beats individual diligence: do all imports point to packages that exist, do all function calls match real signatures, and does the business logic match the written requirement.

For backends specifically, add three:

  • Is there an authorization check, distinct from authentication, on every endpoint that returns data?
  • Does any endpoint take a tenant, org or user identifier from the request rather than the session?
  • If row-level security is used, is it ENABLEd and FORCEd?

Test with two users, always

The single highest-value change to how you verify agent-built backends. Create a second account. Log in as both. Try to reach the first account's data from the second.

Most backend failures in agent-generated code are invisible with one user and obvious with two.

The honest summary

Coding agents are genuinely good at backends in one sense: they write correct, idiomatic code quickly. What they don't do is make the judgement calls that separate a working system from a safe one, because those calls aren't in the prompt and the consequences of getting them wrong don't show up in any test the agent would think to write.

Frontends give you a feedback loop that catches this. Backends don't.

So either you supply the judgement yourself, on every project, forever - or you build on something where the constraints already exist and the agent generates within them.

Butterbase is the second option. Tenant isolation enforced in Postgres rather than in generated code, session-bound context that no tool argument can override, and an MCP server exposing a declared tool surface rather than raw database access. An agent building on it can still write a query that forgets the filter. It just won't get anyone else's data back.

Frequently asked questions

Frontend errors are visible immediately - the page renders wrong, the component fails to mount, the build breaks. Backend errors produce working software that fails only under conditions you don't test for, like a second user. The gap is in the feedback loop, not in the model's capability.

It is when a model generates an import for a package that doesn't exist. A study of 16 LLMs across 576,000 generations found rates of at least 5.2% on commercial models and 21.7% on open-source ones, with 205,474 unique hallucinated package names. Attackers register those names deliberately, a technique Trend Micro calls slopsquatting.

Prompt instructions are filters, not boundaries. They change what usually happens, not what can happen. For anything where a single failure is unacceptable - cross-tenant reads, payment state, deletion - the constraint has to be enforced somewhere the agent cannot reach, which in practice means the database.

Create two accounts, log in as both, and try to reach the first account's data from the second by changing IDs in requests and replaying them in devtools. Then check that every row-level security policy is both ENABLEd and FORCEd, and that no endpoint takes a tenant identifier from the request body.

They reduce hallucination but not the underlying issue. Rates fell from 15-45% in 2024 baselines to 3.1-19.1% in 2026, yet auth without authorization is not a hallucination - it is a correct implementation of an underspecified request, so a more accurate model produces the same gap.

It moves the guarantee below the layer the agent writes. Isolation is a Postgres row-level security policy with FORCE enabled, tenant context is bound to a verified session rather than a tool argument, and the MCP server exposes declared tools instead of raw SQL. An agent that forgets a filter gets zero rows rather than everyone's.