Media Storage
Offload agent media to object storage and keep only a reference in the database.
Media on a run is stored in the database as base64 by default, both what you send in and what the run produces. Set media_storage and the bytes go to object storage instead, leaving a MediaReference in the row.
For this S3 example, configure AWS credentials and an existing bucket with read, write, and delete permissions as described in the S3 setup, then install:
uv pip install "agno[s3]" openai sqlalchemy
export OPENAI_API_KEY="your-api-key"
export MEDIA_S3_BUCKET="your-existing-bucket"import os
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.media.storage import S3MediaStorage
from agno.models.openai import OpenAIResponses
agent = Agent(
model=OpenAIResponses(id="gpt-5.5"),
db=SqliteDb(db_file="tmp/agent.db"),
media_storage=S3MediaStorage(bucket=os.environ["MEDIA_S3_BUCKET"]),
)The model still receives the media, tools still process it, and history still replays it on later runs. Only the storage location changes. media_storage works the same way on Agent, Team, and Workflow.
Backends
| Backend | Class | Install |
|---|---|---|
| Local | LocalMediaStorage | Built in |
| S3 | S3MediaStorage | uv pip install "agno[s3]" |
| GCS | GCSMediaStorage | uv pip install "agno[gcs]" |
Each backend has an async twin: AsyncLocalMediaStorage, AsyncS3MediaStorage, AsyncGCSMediaStorage. A sync backend also works inside arun(), where the upload runs in a worker thread to keep the event loop free.
What Gets Offloaded
Every media object on a run, whichever direction it came from:
| Source | Example |
|---|---|
| Attached to the run | An image or PDF you pass to run(), or an AgentOS multipart upload |
| Produced by the run | An image the model generates, or a file from FileGenerationTools |
| Spoken by the model | Generated audio, on both response_audio and the message that carried it |
| Read from disk | Image(filepath=...), or a code-execution artifact |
| Carried on messages | Media replayed from history, and on additional_input or reasoning messages |
| From a team member or workflow step | A member's generated chart, including nested teams and container steps |
How It Works
- Media is uploaded to the backend before the run is written to the database, including any background status row or mid-run checkpoint written before the final one.
- The row stores a
MediaReference(key, bucket, mime type, size, SHA-256) instead of the bytes. - On a later run, the media is read back from the backend so the model sees it as before.
Offload runs on a deep copy, so the RunOutput you are handed keeps its bytes. Only the persisted copy carries the pointer.
media_storage requires store_media=True, which is the default. With store_media=False no new media is persisted, and an Agent or Team still reads back and deletes media stored earlier. A Workflow resuming a paused run needs store_media=True to refresh its executor's media.
A missing bucket or insufficient write permission makes uploads fail. Offload falls back to inline base64 and the run still succeeds, so the failure is easy to miss. Check that media reaches the bucket the first time you configure it.
Teams and Workflows
Set media_storage on the team or workflow, not on its members. The parent owns the write, so its backend uploads the whole run including member and step rows. A member pointed at a different bucket cannot resolve the parent's references and its media is skipped on the next turn.
A member with store_media=False has its media dropped before the parent uploads anything. A restriction travels down the tree; store_media=True on the parent does not override a member that turned it off.
URL-only Media
Media that arrives as a bare URL is skipped during offload. Agno stores the URL and never downloads the file. Set persist_remote_urls=True on the backend to fetch the URL from your process and store the bytes as well. Enable it only for URLs you trust, since the fetch runs with your network reach.
storage = S3MediaStorage(bucket=os.environ["MEDIA_S3_BUCKET"], persist_remote_urls=True)Deleting Media
Offloaded media outlives the session by default. The reference in the row is the only record of which object belongs to which session, so deleting rows first leaves orphaned objects.
Pass delete_media=True to read the keys off the rows before deleting them, then sweep the objects.
agent.delete_session(session_id="abc123", delete_media=True)The flag exists on Agent, Team, and Workflow, in both sync and async variants. It is opt-in: a plain delete_session() leaves every object in the backend.
Forking a session re-uploads the media under the fork's own keys, so either session can be deleted without affecting the other.
A MediaReference records the backend and bucket that minted it. Media stored elsewhere is not read back, not deleted, and not served, so changing bucket leaves earlier objects reachable only by the old configuration.
AgentOS
AgentOS serves stored media at /sessions/{session_id}/media/{storage_key}. It checks that the storage key belongs to the named session. Caller ownership requires authentication and AuthorizationConfig(user_isolation=True) for ordinary user tokens; a bare AgentOS configuration below does not provide that isolation. Administrators remain unscoped, and non-admin service accounts self-scope. See User Isolation.
from agno.os import AgentOS
agent_os = AgentOS(agents=[agent])AgentOS uses one media backend, either the explicit media_storage or the first backend it discovers. References from a different backend or bucket return 404; explicitly selecting a backend does not combine multiple buckets. Use a shared backend or separate serving configurations for the others.
The route streams the bytes by default, which keeps the bucket private and leaves one CORS surface. Pass redirect=true to get a 307 to a freshly-signed URL instead, which is the cheaper path for embedding media in a page. Backends that sign nothing still stream: local storage always, and GCS with no service-account key.
The same delete_media flag works over HTTP, on one session or a batch:
GET /sessions/{session_id}/media/{storage_key}?redirect=true
DELETE /sessions/{session_id}?delete_media=true
DELETE /sessions?delete_media=true