Agno's calculate_metrics() on PostgresDb and AsyncPostgresDb now reads only each run's model and model_provider, so a refresh leaves the legacy sessions.runs column in Postgres. The runs migration keeps that column as a backup until someone calls cleanup_legacy_runs_column(), and earlier releases selected it on every refresh. Every stored run came along, messages and tool calls included, only to be thrown away because the runs table already held the same data.
That hurt most on the first refresh after migrating, which covers the full history and also fires on its own when the metrics page opens. In one reported case, 51 GB of legacy runs ran an 8 GB container out of memory. An earlier metrics fix dealt with bind-parameter limits and the migration's own memory; this one shrinks what the refresh reads.
Runs that never reached the runs table still count. Postgres expands the legacy column with jsonb_array_elements inside the query and sends back two fields per run.
A script seeded an embedded Postgres with 1,500 sessions of 20 runs each, ran the migration, kept the legacy column in place, then called calculate_metrics() in a fresh process and measured peak memory growth.
| Legacy runs JSON | Adapter | Runs counted | Peak memory before | Peak memory now |
|---|---|---|---|---|
| 304 MB | PostgresDb | 30,000 | +687 MB | +20 MB |
| 304 MB | AsyncPostgresDb | 30,000 | +689 MB | +20 MB |
The sessions query shows where the memory went.
# real output, first sessions query of the refresh, captured with a SQLAlchemy event
Before
SELECT demo.agno_sessions.session_id, demo.agno_sessions.user_id, demo.agno_sessions.session_data, demo.agno_sessions.created_at, demo.agno_sessions.session_type, demo.agno_sessions.runs
Now
SELECT demo.agno_sessions.session_id, demo.agno_sessions.user_id, demo.agno_sessions.session_data, demo.agno_sessions.created_at, demo.agno_sessions.session_typeSee the cookbook, and learn more about refreshing AgentOS metrics and migrating your Agno database in the documentation.
Frequently asked questions
The migration keeps the old sessions.runs column as a backup, and earlier releases selected that column on every metrics refresh. The first refresh after migrating covers the full history, so it pulled every stored run into the process. Agno now reads only the model and provider of each run.
No. Refreshes on PostgresDb and AsyncPostgresDb now skip that column for as long as it exists. Dropping it with cleanup_legacy_runs_column() is an optional step that permanently deletes the backup, so verify the migration and keep a restorable backup first.
Yes. Postgres unpacks the legacy column in the query and returns the model and provider of each legacy run for sessions with nothing in the runs table, so every run is counted once wherever it is stored.




