# With v3.0, we went after what actually breaks agents in production

> Agno v3.0 hardens AI agents for production workloads: a dedicated runs table, tool result and media offloading, durable background execution, per-user isolation, Studio governance, and CodeMode. Includes the required migration.

- Published: 2026-08-28
- Author: Cosette Cressler
- Categories: Product, Engineering
- Canonical: https://www.agno.com/articles/what-actually-breaks-agents-in-production
- Markdown: https://www.agno.com/articles/what-actually-breaks-agents-in-production.md

Agno is a platform for building and running AI agents in your own cloud. You build agents, teams, and workflows with the Agno SDK, deploy them to the AgentOS runtime, and manage them from a control plane. Agno v3.0 is a release about running them for real. Rather than adding a headline model capability, it reworks how agents store data, survive failure, isolate users, and how changes are moved into production.

If you're interested in learning more about our vision for an agent platform that builds itself, our CEO, Ashpreet, wrote about that [here](https://www.agno.com/articles/agno-3-0).

## Key takeaways

- Agno v3.0 moves runs out of the sessions blob into a dedicated `agno_runs` table, making each session write constant-cost instead of growing with the run count, and removing the item-size ceilings on DynamoDB and Firestore. A one-time [database migration](https://docs.agno.com/other/v3-migration) is required after you upgrade and before v3.0 serves traffic, and it carries the per-user isolation schema changes with it.
- Tool result and media offloading keep large data out of the model's context and the database. A 113 KB image stored inline as base64 takes roughly 151,000 characters in a session row; offloaded, it takes about 2,900.
- Durable background execution commits each run to the database before it starts, so runs survive crashes, deploys, and restarts, and any replica can finish them.
- Per-user data isolation now spans sessions, metrics, schedules, evals, knowledge, components, entity memory, and 17 vector databases, making single-deployment multi-tenancy practical. Vector collections created before v3 are migrated separately from the main migration.
- Studio 3.0 adds draft-and-publish governance: private drafts, immutable published versions, rollback, and conflict protection.
- CodeMode lets an agent write Python that calls its tools directly through a persistent kernel. It is not a sandbox and requires `pip install 'agno[code]'`.

For the last couple of years, the story of AI agents has been a story about capability. Can the model use a tool? Can it plan a few steps ahead and recover when one fails? Every month, the demos answered yes a little more convincingly, with the same promise underneath: the intelligence has arrived, and the rest is just wiring.

The rest turned out to be most of the work.

Anyone who has pushed an agent past a demo hits the same wall, and it has little to do with how smart the model is. A database that felt fine with one user strains under a thousand. The context window fills with raw tool output nobody needed in full. A background job dies during a deploy. One user's memory surfaces in another user's session. A half-finished edit starts serving production traffic because nothing sits between editing and going live.

None of those are intelligence problems. They are systems problems, and they are why most agents never leave the prototype folder.

Making an agent production-ready is not a milestone you pass once. It is a running list of specific failures you have to eliminate one by one. We built Agno v3.0 to tackle several of them.

## How Agno v3.0 reduces context and token costs

Agno v3.0 reduces token costs and database bloat by offloading large tool results and media out of the model's context. Take the way an agent handles an image. By default, the bytes are stored as base64 in the session record, so a single 113 KB JPEG becomes roughly 151,000 characters in your database, and it replays into the model's context on every run that touches the session. Across a real workload, your rows bloat and you keep paying to send the same picture through the model again.

Tool results have the same problem in another form. A tool returns a 60,000-character API response, and all of it lands in context whether the model needs the whole thing or not.

v3.0 treats context and storage as budgets you spend on purpose.

| Property                 | Stored inline (2.x default) | Offloaded (v3.0)                             |
| ------------------------ | --------------------------- | -------------------------------------------- |
| Media location           | base64 in the session row   | object storage, a small reference in the row |
| A 113 KB JPEG in the row | ~151,000 characters         | ~2,900 characters                            |
| Large tool result        | full payload in context     | preview plus an id, fetched on demand        |
| Storage backends         | one                         | local, S3, and GCS, each with an async twin  |

Turning media offloading on is a single field:

```python
from agno.agent import Agent
from agno.media.storage import S3MediaStorage

agent = Agent(media_storage=S3MediaStorage(bucket="my-bucket"))
```

Tool result offloading works the same way. Set `offload_tool_results=True` and anything past a threshold is written to the agent's filesystem, with the model getting a preview and the tools to read or search the full result when it matters. The model still receives the media, history still works as expected, and the only thing that changes is what stops living in your context window and database.

## Why runs moved to their own database table

Agno v3.0 gives each run its own row in a dedicated `agno_runs` table instead of packing runs into the session record. In 2.x, every run a session accumulated was packed back into one record, so the cost of a single write grew with the number of runs already sitting there, and writing N runs cost O(N²) in total. On DynamoDB and Firestore it eventually hit hard item-size limits. Giving runs their own table makes each write constant-cost regardless of session length, removes the ceiling, and lets you query and page runs directly instead of loading a whole session to reach them.

Reading is unchanged. `session.runs` is still populated, now from the runs table, and you can also fetch runs directly with `db.get_runs(session_id=...)` without loading the whole session. If you query the sessions table's `runs` column from SQL, a dashboard, or an export, point those at `agno_runs` instead.

## How durable background execution survives crashes and deploys

That storage model matters because an agent is not a function call that happens once and returns. A production agent keeps running, and it accumulates history the whole time. Durable background execution follows from that. An accepted run is committed to your database before it starts, so a crash or a deploy no longer takes it down, and any replica can pick it up and finish it.

The durable queue includes the controls a production system needs:

- Bounded concurrency, tunable per deployment
- Cancellation while a job is still waiting
- Idempotency keys, so a retried request never runs twice
- Backpressure when the queue is full
- Reconnectable streaming after a client drops

Redis is optional and used only for coordination. Your database stays the source of truth.

## How Agno v3.0 handles multi-tenant data isolation

Agno v3.0 extends per-user data isolation from sessions to almost everything an agent touches, so one deployment can serve many users with real boundaries between them. You cannot serve a thousand customers on shared state and hope their data stays in the right lane, and multi-tenancy is the line every internal tool crosses before it becomes a product.

Per-user isolation in v3.0 now covers:

- Metrics, aggregated per user per day
- Schedules and evals
- Knowledge and components
- Entity memory
- 17 vector databases

Each user's data is scoped to them, and the boundary holds across the platform rather than just the conversation. Components and knowledge you never assigned to a user are treated as shared, readable by everyone and editable by an admin, so turning isolation on doesn't hand you a wall of 404s for the things you built before it existed.

Existing data needs a migration to get the same boundary. On the SQL adapters, the built-in migration adds the `user_id` columns behind isolation and re-keys entity memory per user. Vector databases are the exception. Collections created before v3 have no per-user scoping and migrate through their own scripts. On schema-based stores, an un-migrated collection fails loudly on a user-scoped search rather than quietly returning nothing.

## How Studio 3.0 governs agents from draft to production

Studio is Agno's component builder, and Studio 3.0 adds a draft-and-publish workflow so a change is never live until you publish it. If an agent serves real users, editing it is a deploy, and deploys need a boundary between work-in-progress and live.

Creating a component now writes a private draft that serves nobody until you publish it. From there, you get the controls you expect around anything that ships:

- Immutable published versions with rollback
- Archive and restore
- Compare-and-set guards that reject a stale write with a typed conflict error instead of overwriting a teammate's change
- Tombstoned deletes with dependent-tracking, so you can't pull a component another one relies on

Every Studio operation returns a structured result that says whether it worked and, when it didn't, why, across its roughly thirty-one tools. This is change management applied to agents, and the teams that run agents at scale are the ones who stop editing them live.

## What CodeMode is and how it works

CodeMode is an Agno toolkit that gives an agent a persistent Python kernel so the model can write code that calls its tools directly. The default way an agent uses tools is one call at a time: pick a tool, wait for the result to come back through the transcript, read it, pick the next. Chain a few of those together, and you've burned several turns and filled your context with intermediate output.

CodeMode replaces that with a persistent Python kernel. The model writes code and calls your tools as awaitable handles, composing them in one step and holding intermediate values in variables the way any script would. The kernel lives for the session, so what the model sets up in one turn is still there on the next, and the in-between values stay in the kernel instead of the context window.

```python
# pip install 'agno[code]'
from agno.agent import Agent
from agno.tools.code.code_mode import CodeMode

agent = Agent(tools=[CodeMode(tools=[...])])
```

For work that fans out across many tools, code is a better interface for composition than a string of isolated calls. CodeMode is not a sandbox. It runs Python with the same permissions as the process behind your agent, and because it restores sessions by unpickling them, resuming a session is itself code execution. Use it only with trusted inputs and operators, and run it inside an isolated container.

## What it adds up to

The features in this release point in the same direction. Capability is no longer the thing standing between an agent and production. The system around it is.

The model can already do the parts that make a good demo. What breaks is everything around it, and that is what v3.0 is built for: databases that scale, results that don't drown the context window, jobs that survive a deploy, users whose data stays separate, and components that reach production through a real gate.

None of that makes the agent smarter. It makes the agent something you can run in production, for many users, over long stretches, without it falling over. That is a less exciting promise than most agent announcements make. It is also the kind of work that decides whether an agent holds up once real traffic reaches it.

## Get started with Agno v3.0

Upgrade first with `pip install -U agno`, then run the migration before v3.0 serves traffic. The migration moves runs into the `agno_runs` table and, on SQL adapters, adds a `user_id` column and index to the evals, components, knowledge, schedules, schedule-runs, and metrics tables. It also re-keys entity memory per user. Re-keying entity memory is the reason not to wait: until the migration runs, pre-v3 entity rows are still keyed without a user component, so one user's facts can surface in another user's context.

Two things can trip it up. Schedule names are now unique per user, so if duplicate names already exist, the migration aborts rather than marking itself done. Resolve the duplicates and re-run. And on the async adapters, `get_runs` and the cleanup method are coroutines, so await them. Otherwise the migration runs unchanged across 12 sync and 4 async backends.

```python
import asyncio
from agno.db.postgres import PostgresDb    # or any sync adapter: SqliteDb, MongoDb, RedisDb, ...
from agno.db.migrations.manager import MigrationManager

db = PostgresDb(db_url="postgresql+psycopg://...")

# 1. Apply the v3 migrations
asyncio.run(MigrationManager(db).up())     # or POST /databases/all/migrate on AgentOS

# 2. Verify the runs landed before any cleanup
assert len(db.get_runs(limit=5)) > 0       # do NOT clean up if this fails

# 3. Optional, and only once step 2 passes: reclaim the legacy blob
db.cleanup_legacy_runs_column(force=True)
```

Reads keep working before, during, and after the migration, because a session merges the runs table with any legacy blob. The migration itself is non-destructive and idempotent, and it preserves the legacy `runs` column as a backup. Step 3 is the one destructive move in the sequence: it deletes that column permanently, and if the migration did not actually copy your history, the column was the only copy. Confirm the history looks right in the AgentOS UI before you pass `force=True`. On a deployment with no prior runs there is nothing to verify or reclaim, so skip steps 2 and 3.

- Read the [v3.0 migration guide](https://docs.agno.com/other/v3-migration) before upgrading
- Browse the [v3.0 changelog](https://docs.agno.com/other/v3-changelog) for the full feature list and every breaking change

## Frequently asked questions

### Does upgrading to Agno v3.0 require a database migration?

Yes. Agno v3.0 moves runs out of the sessions record into a dedicated `agno_runs` table, so you must run a [one-time migration](https://docs.agno.com/other/v3-migration) after upgrading and before v3.0 serves traffic. Run `MigrationManager(db).up()`, or `POST /databases/all/migrate` on AgentOS. The same migration adds the `user_id` columns behind per-user isolation and re-keys entity memory. The migration is non-destructive and idempotent, runs on 12 sync and 4 async backends, and keeps the legacy column as a backup until you reclaim it.

### How does Agno v3.0 lower token and context costs?

Agno v3.0 offloads large tool results and media out of the model's context. Tool results past a threshold are written to the agent's filesystem, leaving a preview and an id the model can read or search on demand. Media is stored in object storage with a small reference in the session row, reducing a 113 KB image from about 151,000 characters inline to about 2,900.

### What is CodeMode in Agno?

CodeMode is an Agno toolkit that gives an agent a persistent Python kernel. The model writes Python and calls its tools as awaitable handles, composing them in a single step instead of one call at a time. CodeMode requires `pip install 'agno[code]'` and is not a sandbox, so use it only with trusted inputs and operators, and run it inside an isolated container.

### How does Agno support multi-tenant applications?

Agno v3.0 provides per-user data isolation across sessions, metrics, schedules, evals, knowledge, components, entity memory, and 17 vector databases. Each user's data is scoped to them within a single deployment, so you can build a multi-tenant product on one AgentOS without standing up a separate instance per customer.

### Do I need to migrate my vector database?

Only if you use per-user knowledge on a collection created before v3, and it is a separate step from the built-in migration. Run the matching script from the [`v2_to_v3` migrations directory](https://docs.agno.com/other/v3-migration) in the repository. On the schema-based stores (PgVector, SingleStore, LanceDB, Milvus, ClickHouse, Redis, Cassandra, and Couchbase), an un-migrated collection raises a `ValueError` on a user-scoped search rather than returning empty results. The schemaless stores (Qdrant, Pinecone, Upstash, Chroma, MongoDB, OpenSearch, and SurrealDB) need no migration, and their pre-v3 documents are treated as shared.

### Are Agno background runs durable?

Yes. With durable background execution, an accepted run is committed to the database before it starts, so it survives crashes, deploys, and restarts, and any replica can execute it. The queue includes bounded concurrency, cancellation while queued, idempotency keys to prevent retried requests from running twice, and backpressure when full.

### What are the main breaking changes in Agno v3.0?

The largest breaking change is that runs moved to their own table and require a migration. Others include renamed agent parameters, `MultiMCPTools` removed in favor of one `MCPTools` per server, flat Google tool modules moved under `agno.tools.google.*`, and `JWTMiddleware` replacing `secret_key` with `verification_keys`. The full list is in the [v3.0 changelog](https://docs.agno.com/other/v3-changelog).
