AI Agents vs Traditional Backends: Control Flow, Cost and Failure Modes
Compare a deterministic backend with an LLM agent loop using a refund example: latency, token costs, context growth, failure modes, and where a hybrid design fits best.
This article is published in English.
Sources: AI Agents vs Traditional Backend Applications: What’s Actually Different?
Backend engineering has long leaned on one reassuring property: determinism. Write an endpoint that takes an order, checks stock in PostgreSQL and charges a card through Stripe, and the same sequence runs every time. With the same inputs and the same database state, line 42 always executes before line 43.
Then the industry began calling AI agents the next stage of backend systems. It's easy to wave that off as marketing, since an agent also looks like a service in a container that calls external APIs and writes to a database. Underneath, though, control flow has flipped. In a classic backend the developer fixes every step, branch and edge case ahead of time. In an agent, the developer supplies an objective and a set of tools, and a probabilistic model chooses the steps, their order and the moment the job is done. Treat an agent as just another microservice and you'll hit serious trouble with latency, reliability, testing and cloud bills.
Fixed pipeline versus reasoning loop
A standard backend handles a request through a directed acyclic graph that the programmer hardcoded:
[Request] ──> [Validate Input] ──> [Database Query] ──> [Transform Data] ──> [Response]
│
└── (Invalid) ──> [Return 400]
Each branch, whether if/else, switch or try/catch, is decided beforehand. The code can't invent a step you didn't write. Unexpected input produces an error or a predefined fallback.
An agent doesn't follow a static line. It lives in a runtime loop, often built on the ReAct pattern, short for Reason plus Act. The backend hands control flow to a large language model:
┌────────────────────────┐
▼ │
[Objective] ──> [LLM Evaluates State] │
│ │
Does it need more info? │
├── Yes ──> [Select Tool & Arguments]
│ │
│ [Execute Tool] ────────┘
│ (API, DB, Search)
│
└── No ──> [Return Final Answer]
One pass through the loop has four parts:
- The model is given the goal, the context and the actions taken so far.
- It asks whether it has enough information or needs an outside tool.
- If it needs data, it emits a structured tool call, such as reading a file, calling an API or running a query.
- Your runtime executes the tool, puts the raw output into the model's context window and restarts the loop.
What matters is that the model picks the sequence of steps dynamically at runtime.
Example: refunding a delayed shipment
Consider an internal tool that refunds a customer whose shipment ran late.
The traditional way
In an Express or FastAPI service you might write a handler that looks up the order, applies a strict rule, issues the refund and sends a notice:
// Traditional Backend: Strict, deterministic logic
async function handleRefund(orderId: string, customerReason: string) {
const order = await db.orders.findById(orderId);
if (!order) {
throw new NotFoundError("Order not found");
}
// Strict business rule written by an engineer
const isEligible = order.status === "DELAYED" && order.daysDelayed > 5;
if (!isEligible) {
return { status: "rejected", reason: "Delay does not meet refund criteria." };
}
const refund = await paymentGateway.refund(order.paymentIntentId);
await db.orders.update(orderId, { status: "REFUNDED" });
await emailService.sendRefundNotice(order.customerEmail);
return { status: "success", refundId: refund.id };
}
This code finishes in under 100 milliseconds, costs almost nothing in compute and behaves identically every time. It is also brittle. Suppose a customer writes that the package came on time but the driver ran over the mailbox. The handler rejects the refund because daysDelayed is zero, since nothing in its hardcoded rules lets it reason beyond them.
The agent way
An agent decides which tools to call based on the customer's explanation:
// Agent Loop: The model decides which tools to call
import { generateText, tool } from "ai";
import { z } from "zod";
const tools = {
fetchOrder: tool({
description: "Fetch order details and shipping history",
parameters: z.object({ orderId: z.string() }),
execute: async ({ orderId }) => db.orders.findById(orderId),
}),
issueRefund: tool({
description: "Issue a monetary refund to the customer",
parameters: z.object({
paymentIntentId: z.string(),
amount: z.number(),
reason: z.string()
}),
execute: async (args) => paymentGateway.refund(args.paymentIntentId, args.amount),
}),
escalateToHuman: tool({
description: "Escalate edge cases or physical damage to a support manager",
parameters: z.object({ ticketSummary: z.string() }),
execute: async ({ ticketSummary }) => supportDesk.createEscalation(ticketSummary),
})
};
// The execution is governed by an evaluation loop
const response = await generateText({
model: yourConfiguredLLM,
tools,
maxSteps: 5, // Prevents infinite execution loops
prompt: `Customer Issue: "The package arrived on time, but the driver ran over my mailbox. Order #1234."
Evaluate policy guidelines and take appropriate action.`,
});
Here the model reads the complaint, fetches the order, sees that the issue is property damage and not shipping logistics, skips the usual refund check and calls escalateToHuman. That flexibility has a price. The run needed three trips to the LLM, burned a few cents of tokens and took 4 seconds where the handler needed 50 milliseconds.
How the two compare
Almost every operational assumption changes when you go from a deterministic backend to an agentic system.
- Control flow: code defines the steps in one case, while the LLM picks them inside a loop in the other.
- Execution path: known at compile time versus discovered at runtime.
- Latency: about 10 to 200 ms and predictable for a backend, 2 to 30 seconds or more and variable for an agent, depending on iterations.
- Failure modes: syntax errors, unhandled exceptions and nulls for backends, against hallucinated arguments, endless loops and missed constraints for agents.
- Testing: unit, integration and end-to-end assertions on one side, LLM-as-a-judge, eval benchmarks and semantic assertions on the other.
- State: transactional SQL or NoSQL data versus conversation history, context window limits and working memory.
- Cost: CPU and memory, which are flat and cheap, versus input and output tokens on every loop iteration, which can climb fast.
The context window as working memory
A backend keeps state in variables, Redis keys or database rows, and a function reads only the fields it needs. For an agent the context window is short-term working memory. Whenever a tool runs, its output is serialized to text, usually JSON, and appended to the prompt history. A typical run grows like this:
- Step 1: the user prompt, around 50 tokens.
- Step 2: the model's tool call request, around 40 tokens.
- Step 3: the tool result, adding roughly 500 tokens.
- Step 4: the model analyzes the larger prompt and issues a second call, adding about 80.
- Step 5: the second result arrives, adding 1,200 or so.
The prompt swells with every cycle. Since price and latency both scale with input size, the fifth step is noticeably slower and costlier than the first.
A frequent production mistake is returning whole database rows or enormous API payloads to the agent. If a searchUsers() tool dumps 50 complete profiles into the context, you'll soon exhaust the model's effective reasoning window. Design agent tools like public APIs: trim and sanitize outputs so they carry only the fields the model needs for its next decision.
Why agents are harder to keep running
A failing backend gives you a stack trace. You replay the same payload locally, inspect variables and patch the bug. An agent fails probabilistically.
Endless loops. Say the agent calls an API and gets a subtle validation error like invalid_date_format. It retries with a slightly different date string, then tries again. With no hard ceiling on iterations (maxSteps), it can burn hundreds of dollars in API credits arguing with a validator.
Invented tool arguments. Schemas and validation give ordinary code type safety at runtime. An LLM may produce valid JSON matching your schema 98% of the time and occasionally slip in an imaginary parameter. What you expect:
// Expected:
{ "customerId": "cust_123", "amount": 50 }
What the model sometimes emits at high temperature:
// What the model occasionally outputs under high temperature:
{ "user_id": "cust_123", "refund_value": "$50.00" }
If the runtime doesn't enforce strict schema parsing with something like Zod or Pydantic before running the real function, your backend will crash.
Unpredictable routing. A query that took two steps yesterday may take four today because the user phrased it a little differently. That makes response-time SLAs hard to promise.
Choosing the right architecture
Agents attract so much attention that developers try building entire systems out of them, which is an expensive mistake.
Stay with ordinary backend code when:
- The business logic is known and stable, as with invoicing, authentication, CRUD and cart calculations.
- Latency has to be low, as in checkout flows, search autocomplete and real-time messaging.
- Errors carry compliance or financial risk, as in ledgers, health records and permission enforcement.
- Cost per request matters at scale, such as processing millions of events an hour.
Reach for an agent when:
- The route to a solution is ambiguous, for instance finding a flight with a three-hour layover, vegetarian meals and a price under $800.
- Inputs are messy and unstructured, like varied PDF invoices, freeform emails or customer bug reports.
- The system must coordinate disparate tools with no shared API, for example browsing external sites, exploring documentation it finds along the way or triaging issues across several products.
The hybrid design that works
In real applications the most dependable design is rarely a pure agent. It is a deterministic backend with an agent placed inside a controlled boundary:
[Incoming Request] ──> [Traditional API Gateway] ──> Authentication / Rate Limiting
│
▼
[Standard Business Logic] ──> Fast DB Reads / Validation
│
▼
[Targeted Agent Boundary] ──> Handles unstructured task
│ (Restricted to 3 safe tools)
▼
[Standard Business Logic] ──> Validates agent output
│
▼
[Final Response] <─── [Structured DB Write]
Keep routing, authorization, persistence and critical business validation in deterministic code. Call on the agent only for the sub-task that truly needs human-style reasoning and loose tool orchestration.
The takeaway
An agent isn't better than a traditional backend. It's a different compute paradigm. Backends favor predictability, speed and low cost by following explicit instructions, while agents favor adaptability and problem-solving by letting an LLM choose the execution path in a reasoning loop.
So before your next feature, skip the question of whether an agent could do it, because given enough tokens it probably could. Ask whether you already know the steps needed to solve the problem. If you do, write the code yourself, and save agents for the open-ended problems that ordinary code was never flexible enough to handle.