hzerrad Logo

AI Agents Are a Distributed Systems Problem

September 10, 2026 by Houssem Eddine Zerrad

Table of Contents

AI Agents Are a Distributed Systems Problem

For the last few years, most of the conversation around AI agents has revolved around models.

Which model reasons better? Which one is cheaper? Which agent framework should you use? How large should the context window be?

Those questions matter, but they become progressively less important once an agent leaves a demo environment and starts doing real work.

The difficult part of operating an agent in production is not getting an LLM to call a tool.

It is building a system that remains correct when an execution lasts twenty minutes, calls twelve external services, partially succeeds, waits for human approval, loses a worker halfway through, retries an operation, and resumes without performing the same irreversible action twice.

At that point, you are no longer dealing primarily with an AI problem.

You are dealing with a distributed systems problem.

The Workload Is Different

Most backend infrastructure has been optimized around some variation of a bounded request-response cycle:

flowchart LR
    A[Request] --> B[Compute]
    B --> C[Response]

Even when the underlying architecture is complicated, the unit of work is usually reasonably bounded.

A request enters the system. A service processes it. Durable state is stored somewhere if necessary. The request eventually succeeds or fails.

Agent execution looks very different:

flowchart TD
    A[Goal] --> B[Reason]
    B --> C[Tool call]
    C --> D[Observe result]
    D --> E[Update state]
    E --> F{Task complete?}

    F -- No --> G{Need external input?}
    G -- No --> B
    G -- Yes --> H[Wait]

    H --> I{Human approval required?}
    I -- Yes --> J[Request approval]
    J --> K[Resume]
    I -- No --> K

    K --> B

    F -- Yes --> L[Result]

An agent may remain logically alive long after the process that was executing it has disappeared.

That distinction has consequences.

The infrastructure problem is no longer simply:

Where do I run the model?

It becomes:

What does execution mean when the process performing the work cannot be trusted to stay alive?

That is a problem software engineers have dealt with before.

Durable Execution Comes Before Intelligence

Consider an agent responsible for resolving a customer support issue.

It might:

  1. read the ticket;
  2. query the customer database;
  3. determine that a refund is appropriate;
  4. request human approval;
  5. wait thirty minutes;
  6. receive approval;
  7. call the payment provider;
  8. update the ticket.

Now suppose the worker crashes immediately after step 7.

The process restarts.

What happens?

If the runtime simply reconstructs the conversation and asks the model to continue, the agent might issue the refund again.

The model did nothing wrong.

The infrastructure did.

This is why production agents need concepts that should already be familiar to engineers building reliable systems:

  • persisted execution state;
  • checkpoints;
  • durable event histories;
  • idempotency;
  • explicit state transitions;
  • retry policies;
  • compensation mechanisms.

A simplified execution model might look like this:

stateDiagram-v2
    [*] --> ReadingTicket
    ReadingTicket --> CheckingCustomer
    CheckingCustomer --> AwaitingApproval
    AwaitingApproval --> IssuingRefund: Approved
    AwaitingApproval --> Cancelled: Rejected
    IssuingRefund --> UpdatingTicket
    UpdatingTicket --> Completed

    IssuingRefund --> IssuingRefund: Retry
    UpdatingTicket --> UpdatingTicket: Retry

    Cancelled --> [*]
    Completed --> [*]

Checkpointing is not merely an optimization for sophisticated agents.

It is part of their correctness model.

An agent runtime should be able to disappear at almost any point without destroying the logical execution it was responsible for.

Once you adopt that mental model, the architecture starts looking less like a chatbot backend and more like a workflow engine, job orchestrator, or event-driven state machine.

The LLM becomes one component inside the workflow.

It is not the workflow.

Retries Become Dangerous When Agents Can Act

Retries are simple when an operation is read-only.

Retrying this is generally harmless:

GET /orders/123 HTTP/1.1
Host: api.example.com

Retrying this may not be:

POST /refunds HTTP/1.1
Host: payments.example.com
Content-Type: application/json

{
  "order_id": "123",
  "amount": 12500
}

Distributed systems already distinguish between operations that can safely be retried and operations requiring idempotency protection.

Agents dramatically increase the importance of that distinction because they autonomously chain actions together.

Imagine an infrastructure agent executing:

flowchart LR
    A[Create VM] --> B[Configure VM]
    B --> C[Update DNS]
    C --> D[Notify customer]

Suppose DNS is successfully updated, but the worker crashes before the new state is durably persisted.

When execution resumes, what should happen?

Blindly replaying the workflow could:

  • create another VM;
  • overwrite DNS;
  • send duplicate notifications;
  • leave multiple resources allocated for the same task.

Every externally visible agent action therefore needs an execution contract.

For each operation, the runtime should be able to answer questions such as:

  • Is this operation idempotent?
  • Can its completion be verified after a crash?
  • Can its result be reconstructed?
  • What happens if the caller times out but the operation succeeds?
  • Can the action be compensated if a later step fails?
  • Does retrying it require human confirmation?

Those are not prompt-engineering questions.

They are transaction semantics.

Idempotency Must Exist Outside the Model

Suppose an agent can create a cloud environment.

A naive implementation might expose:

def create_environment(customer_id: str) -> Environment:
    return cloud.create_environment(customer_id)

A safer interface would make the execution itself part of the operation:

def create_environment(
    customer_id: str,
    idempotency_key: str,
) -> Environment:
    existing = environments.find_by_idempotency_key(idempotency_key)

    if existing is not None:
        return existing

    environment = cloud.create_environment(customer_id)

    environments.save(
        environment=environment,
        idempotency_key=idempotency_key,
    )

    return environment

The exact implementation will vary.

The architectural principle does not:

Do not rely on an LLM remembering that it already performed an irreversible operation.

Correctness belongs in the infrastructure.

Tool Access Is an Authorization Problem

Giving an agent tools is easy.

Giving an agent the correct authority to use those tools is much harder.

Consider a coding agent with access to:

  • GitHub;
  • AWS;
  • Kubernetes;
  • Slack;
  • production logs;
  • databases.

A naive implementation asks:

Can the agent call the AWS tool?

A production architecture should ask:

Which AWS operations may this particular execution perform, against which resources, under which conditions, and for how long?

The useful analogy is IAM.

We would never give every backend service administrator privileges because:

The service needs AWS access.

Agents should not receive broad authority simply because they need tools.

Instead, permissions should ideally be scoped to the execution.

flowchart TD
    A[Agent execution] --> B[Policy engine]

    B --> C[Read repository]
    B --> D[Create branch]
    B --> E[Open pull request]
    B --> F[Merge to main]
    B --> G[Deploy production]

    C --> C1[Allowed]
    D --> D1[Allowed]
    E --> E1[Allowed]
    F --> F1[Requires approval]
    G --> G1[Denied]

Another execution performed by the same agent might receive completely different capabilities.

The security boundary belongs outside the model.

A production system should remain safe even when the model makes a bad decision.

Sandboxing Matters for the Same Reason

Coding agents make this particularly obvious.

If an agent can:

  • run shell commands;
  • install packages;
  • execute downloaded binaries;
  • modify files;
  • access credentials;
  • make arbitrary network requests;

then its execution environment is part of your security architecture.

You would not normally allow untrusted user input to generate unrestricted shell commands on one of your production application servers.

Putting an LLM between the user and the shell does not suddenly make that architecture safe.

A safer architecture looks closer to this:

flowchart LR
    U[User task] --> A[Agent runtime]
    A --> P[Policy layer]
    P --> S[Ephemeral sandbox]

    S --> F[Scoped filesystem]
    S --> N[Restricted network]
    S --> C[CPU / memory limits]
    S --> T[Approved tooling]

    S --> R[Execution result]
    R --> A

The sandbox could be implemented using:

  • containers;
  • Linux namespaces;
  • microVMs;
  • WebAssembly;
  • dedicated ephemeral VMs;
  • another isolation mechanism.

The exact technology is secondary.

The important question is:

If this execution behaves unexpectedly, what can it actually damage?

That is a blast-radius problem.

Observability Means Reconstructing Execution

Traditional service observability helps answer questions such as:

  • How many requests are failing?
  • Which endpoint is slow?
  • Which dependency is timing out?
  • Which deployment introduced a regression?

Agents add another requirement:

How did this execution arrive at this action?

That does not mean trying to persist some hidden internal chain-of-thought from the model.

The system should instead record the observable execution trajectory.

Useful events include:

  • model invocation metadata;
  • input artifact versions;
  • tool requests;
  • tool arguments;
  • authorization decisions;
  • tool responses;
  • state transitions;
  • retries;
  • errors;
  • human approvals;
  • token consumption;
  • execution cost;
  • generated artifacts.

A trace could conceptually look like this:

execution_id: run-9182

10:01:13  model.invoke
10:01:14  model.completed

10:01:15  tool.github.get_issue
          issue=184
          result=success

10:01:18  tool.github.read_file
          result=success

10:01:24  tool.sandbox.run_tests
          result=failed

10:01:26  model.invoke
10:01:29  model.completed

10:01:32  tool.github.write_file
          result=success

10:01:38  tool.sandbox.run_tests
          result=success

10:01:41  policy.evaluate
          capability=merge_to_main
          result=requires_approval

10:01:42  approval.requested

The logical trace becomes:

sequenceDiagram
    participant A as Agent
    participant P as Policy Engine
    participant G as GitHub
    participant S as Sandbox
    participant H as Human

    A->>G: Read issue #184
    G-->>A: Issue contents

    A->>G: Read source files
    G-->>A: Files

    A->>S: Run tests
    S-->>A: Failed

    A->>G: Modify source
    G-->>A: Success

    A->>S: Run tests
    S-->>A: Passed

    A->>P: Request merge_to_main
    P-->>A: Approval required

    A->>H: Request approval

Now an engineer can inspect what happened.

Without this execution history, an incident eventually becomes:

The agent did something weird.

And debugging stops there.

Agent Observability Is Still Distributed Tracing

There is a temptation to treat agent observability as an entirely new discipline.

Some new primitives are certainly useful, but much of the underlying problem should look familiar.

A useful agent execution hierarchy might be:

flowchart TD
    A[Agent run] --> B[Model invocation]
    A --> C[Tool invocation]
    A --> D[State transition]
    A --> E[Approval]
    A --> F[Evaluation]

    C --> C1[HTTP request]
    C --> C2[Database query]
    C --> C3[Sandbox process]

    C1 --> G[Existing distributed trace]
    C2 --> G
    C3 --> G

The agent trace should complement—not replace—your existing telemetry.

If an agent calls an API which calls three microservices and a database, you should still be able to correlate that operation with the normal distributed trace.

Ideally, there is one execution identity connecting both worlds.

Testing Changes Too

Deterministic software gives us a useful abstraction:

input X -> output Y

Agent behavior does not always satisfy it.

Two executions may take different paths while still producing acceptable results.

Suppose a coding agent receives:

Fix the race condition in the cache.

One execution changes the locking strategy.

Another removes the shared mutable state entirely.

Both could be correct.

Testing the exact generated patch therefore makes little sense.

What matters is whether the result satisfies the system's invariants.

For example:

requirements:
  tests:
    unit: pass
    integration: pass
    race_detector: pass

  repository:
    unrelated_files_modified: false

  performance:
    regression_threshold_percent: 5

  security:
    forbidden_capabilities_used: false

This pushes some testing upward from exact output matching toward outcome and invariant evaluation.

But evaluation should not replace conventional testing.

If a component is deterministic, test it deterministically.

Use unit tests for deterministic logic.

Use integration tests for deterministic boundaries.

Use property-based testing when invariants matter.

Use agent evaluations where behavior is genuinely nondeterministic.

The architecture might look like:

flowchart BT
    A[Unit tests] --> B[Integration tests]
    B --> C[Agent evaluations]
    C --> D[Staging / canary execution]
    D --> E[Production]

The higher you move through the stack, the more expensive and probabilistic validation becomes.

That makes deterministic foundations even more important, not less.

Human-in-the-Loop Is Not a Failure of Autonomy

There is a tendency to judge agents by how little human involvement they require.

That is probably the wrong metric.

A better question is:

How much autonomy can this operation safely tolerate?

Reading documentation may require no approval.

Opening a pull request may require very little.

Deploying to production may require more.

Deleting production data should require considerably more.

A mature system can encode this explicitly:

flowchart LR
    A[Read repository] --> A1[Automatic]
    B[Create branch] --> B1[Automatic]
    C[Open PR] --> C1[Automatic]
    D[Merge PR] --> D1[Approval]
    E[Production deploy] --> E1[Approval]
    F[Delete database] --> F1[Prohibited]

Autonomy should be constrained by consequence.

This remains true even if models become dramatically more capable.

A hypothetical model that makes excellent decisions would still need authorization boundaries.

Reliability and permission are different properties.

Protocols Do Not Eliminate Orchestration

Protocols such as the Model Context Protocol are useful because they reduce the integration cost of exposing tools and external systems to agents.

But interoperability should not be confused with execution architecture.

A protocol can help define how an agent discovers or invokes a capability.

It does not automatically solve:

  • durable execution;
  • retries;
  • idempotency;
  • scheduling;
  • authorization;
  • distributed state;
  • auditing;
  • failure recovery.

Those responsibilities belong elsewhere.

A clean architecture might look like this:

flowchart LR
    U[User / Trigger] --> R[Agent Runtime]

    R <--> S[(Execution State)]
    R --> P[Policy Engine]
    R --> O[Observability]

    P --> T1[MCP / Tool Server]
    P --> T2[MCP / Tool Server]
    P --> T3[Internal Capability]

    T1 --> G[GitHub]
    T2 --> C[Cloud Platform]
    T3 --> D[(Database)]

The runtime owns execution.

The state store owns durability.

The policy layer owns authorization.

The tool servers expose capabilities.

The telemetry stack records what happened.

Keeping those responsibilities separate is considerably easier to reason about than turning every tool integration into part of the agent runtime itself.

The Protocol Can Be Stateless While the Workload Is Stateful

This distinction is especially important.

A tool invocation can be logically simple:

tool(args) -> result

while the workflow using that tool is extremely stateful:

flowchart TD
    A[Execution started] --> B[Tool A]
    B --> C[Checkpoint]
    C --> D[Tool B]
    D --> E[Wait 3 hours]
    E --> F[Human approves]
    F --> G[Checkpoint]
    G --> H[Tool C]
    H --> I[Complete]

There is no contradiction here.

In fact, keeping capabilities as stateless as practical while centralizing durable execution state often produces a cleaner system.

The tool should not need to know the entire history of the agent.

The orchestrator should.

We Have Seen Most of These Problems Before

This might be the most useful realization about production agents.

Many supposedly novel agent-infrastructure problems map directly onto mature software-engineering concepts.

Agent problemExisting systems concept
Long-running executionDurable workflows
Resume after failureCheckpointing
Repeated actionsIdempotency
Multi-step tasksState machines
Tool permissionsIAM / capability security
Tool isolationSandboxing
Agent execution historyDistributed tracing
Human confirmationWorkflow approval gates
Partial failureCompensation / sagas
External eventsEvent-driven architecture
Concurrent agentsDistributed coordination

The LLM changes the behavior of the worker.

It does not repeal the fundamental laws of distributed systems.

Networks still fail.

Processes still crash.

Messages still arrive twice.

External APIs still time out after actually completing an operation.

Credentials can still be abused.

State can still become inconsistent.

Humans still need to understand what happened after an incident.

A Possible Production Architecture

Put everything together and a production agent platform starts looking something like this:

flowchart TB
    U[User / Event] --> API[Agent API]

    API --> ORCH[Durable Agent Runtime]

    ORCH <--> STATE[(Execution State)]
    ORCH --> MODEL[Model Provider]
    ORCH --> POLICY[Policy Engine]
    ORCH --> OBS[Telemetry / Audit]

    POLICY --> TOOLGW[Tool Gateway]

    TOOLGW --> GH[GitHub]
    TOOLGW --> AWS[Cloud APIs]
    TOOLGW --> DB[(Databases)]
    TOOLGW --> SANDBOX[Execution Sandbox]

    ORCH --> APPROVAL{Approval Required?}

    APPROVAL -- Yes --> HUMAN[Human]
    HUMAN --> ORCH

    APPROVAL -- No --> TOOLGW

    OBS --> TRACE[(Trace Store)]
    OBS --> METRICS[(Metrics)]
    OBS --> LOGS[(Logs)]

There are many ways to implement each component.

That is not the point.

The important observation is that the model is a relatively small piece of the architecture.

Most of the machinery exists to make autonomous execution:

  • durable;
  • observable;
  • recoverable;
  • constrained;
  • auditable;
  • safe.

That should look very familiar to backend and platform engineers.

The Model Is Only One Failure Domain

Better models will improve agents.

They will select tools more accurately, understand larger codebases, recover from mistakes more effectively, and complete increasingly complex tasks.

But model quality cannot give you exactly-once execution.

It cannot make an unsafe permission model safe.

It cannot reconstruct state your application never persisted.

It cannot determine whether retrying a payment request will charge someone twice unless the surrounding system provides the information needed to answer that question.

And it cannot recover an execution history you never recorded.

Those responsibilities belong to the system around the model.

That is why I think the next stage of agent engineering will look less like prompt engineering and increasingly familiar to backend, platform, and distributed-systems engineers.

The interesting question is no longer:

How do we make an LLM call tools?

We already know how to do that.

The interesting question is:

How do we make autonomous execution reliable when everything around it can fail?

Once agents begin doing real work, that becomes the problem that matters.

HZ

Houssem Eddine Zerrad

Senior Software Engineer | Cloud Architect | Gamer at Heart