S3 Media Storage
Store agent media in an S3 bucket with S3MediaStorage.
S3MediaStorage uploads media to an S3 bucket and keeps only a MediaReference in the database. It also supports MinIO and other S3-compatible services through endpoint_url.
Usage
Install the required packages:
uv pip install "agno[s3]" openai sqlalchemySet AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or pass the credentials directly. Pass region explicitly: left unset, boto3 falls back to AWS_DEFAULT_REGION or ~/.aws/config, and it does not read AWS_REGION.
Choose an existing bucket and credentials authorized to read, write, and delete its objects. The backend does not create the bucket. Set the bucket name before running Python and set the model key before any Agent run:
export MEDIA_S3_BUCKET="your-existing-bucket"
export OPENAI_API_KEY="your-api-key"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
storage = S3MediaStorage(
bucket=os.environ["MEDIA_S3_BUCKET"],
region=os.getenv("AWS_REGION"),
prefix="agno/media/",
presigned_url_expiry=3600,
)
agent = Agent(
model=OpenAIResponses(id="gpt-5.5"),
db=SqliteDb(db_file="tmp/data.db"),
media_storage=storage,
)Async
AsyncS3MediaStorage takes the same parameters and uses aioboto3.
from agno.media.storage import AsyncS3MediaStorage
storage = AsyncS3MediaStorage(bucket=os.environ["MEDIA_S3_BUCKET"])URLs
get_url returns a presigned URL that expires after presigned_url_expiry seconds. A presigned URL is never written to the database, since it would expire and it carries credentials, so a fresh one is signed on each read. Above the SigV4 maximum of seven days, no URL is signed and readers stream the bytes instead.
Set acl="public-read" to return the unsigned object URL instead. That URL does not expire, so it is stored on the reference. A bucket with ACLs disabled, the default for new buckets, rejects the argument with a ValueError.
S3-compatible Services
Point endpoint_url at the service and set region to match its own site region.
storage = S3MediaStorage(
bucket="media",
endpoint_url="http://localhost:9000",
region="us-east-1",
)A presigned URL carries the region in its signature. A service whose region differs from the one configured here rejects the URL.
Params
| Parameter | Type | Default | Description |
|---|---|---|---|
bucket | str | - | Name of the destination S3 bucket. |
prefix | str | "agno/media/" | Key prefix for stored objects. |
region | Optional[str] | None | AWS region. Falls back to AWS_DEFAULT_REGION or ~/.aws/config. |
acl | Optional[str] | None | Object ACL applied on upload. "public-read" also makes get_url return the unsigned object URL instead of a presigned one. |
presigned_url_expiry | int | 3600 | Signed URL lifetime in seconds. Above the SigV4 maximum of seven days, URLs fall back to streaming. |
endpoint_url | Optional[str] | None | Endpoint of an S3-compatible service such as MinIO. |
aws_access_key_id | Optional[str] | None | AWS access key ID. Falls back to the standard credential chain. |
aws_secret_access_key | Optional[str] | None | AWS secret access key. Falls back to the standard credential chain. |
persist_remote_urls | bool | False | Download and store media passed as a URL. Skipped when False. |
See the full example here.