Agent Memory with Cognee on Tigris S3 Storage
An AI agent starts every conversation from scratch unless you give it a memory layer to hold what it has learned. Tigris makes a good home for that memory. It is S3-compatible object storage, and the part that matters here is that a whole memory state becomes something you can snapshot, fork, and roll back the way you branch code.
Cognee is the memory engine that runs on top. Feed it text, files, or URLs and it builds a knowledge graph of entities, relationships, and the embeddings your agent searches against. Point Cognee at a Tigris bucket and that graph, its vector indexes, and the raw source data all live in storage you control. The memory API reads as a lifecycle of four verbs:
| Verb | What it does |
|---|---|
remember | Store data permanently: ingest it and build the knowledge graph. |
recall | Retrieve relevant knowledge, auto-routing across graph, vector, and lexical search. |
improve | Enrich the graph: apply feedback, build indexes, fold session memory into permanent memory. |
forget | Remove memory. One item, a dataset, or everything, through one call. |
The older add, cognify, search, and memify calls still exist as
lower-level building blocks, but the four verbs above are the surface you reach
for.
Here is what storing that memory on Tigris buys you. In Cognee's default
file-based mode, the entire memory lives as files under a single bucket: the raw
data, the vector indexes, and the knowledge graph together. Snapshot the bucket
and you capture all of it at one instant. Fork it and an agent gets a private
copy to work in, where it can trial a new embedding model, test a risky
improve, or explore a what-if without touching the memory that production
depends on. A bad learning becomes a rollback rather than a re-ingestion, and
global distribution and zero egress fees come along on top.
This guide starts with those file-based defaults, which need no servers to run, then covers the Postgres backend for distributed, multi-instance deployments. The snapshot and fork workflow comes later.
Here is a basic agent memory architecture:
Agent → Cognee → Tigris: remember, recall, and forget with memory stored on Tigris.
Prerequisites
- A Tigris account with an Access Key ID and Secret Access Key (keys start with
tid_andtsec_). Create them via the Tigris Access Key guide. - A Tigris bucket for storing agent memory.
- Python 3.10+ installed.
- An LLM API key, OpenAI by default, though Cognee also supports Anthropic, Gemini, Ollama, and others.
Step 1: Install Cognee
pip install "cognee[aws]"
The [aws] extra adds S3 support through s3fs. The default database backends
ship with core Cognee: LanceDB for vectors, Ladybug for the knowledge graph, and
SQLite for metadata. This one install covers the full stack. For the Postgres
backend (see Choosing a database backend),
install "cognee[postgres,aws]" instead.
Step 2: Create a Tigris bucket
- AWS CLI
- Python
aws s3api create-bucket \
--bucket my-agent-memory \
--endpoint-url https://t3.storage.dev \
--region auto
import boto3
from botocore.config import Config
s3 = boto3.client(
"s3",
endpoint_url="https://t3.storage.dev",
aws_access_key_id="tid_YOUR_ACCESS_KEY_ID",
aws_secret_access_key="tsec_YOUR_SECRET_ACCESS_KEY",
region_name="auto",
config=Config(s3={"addressing_style": "virtual"}),
)
s3.create_bucket(Bucket="my-agent-memory")
Tigris requires virtual addressing style when using boto3. The endpoint is
https://t3.storage.dev.
Step 3: Configure environment variables
Create a .env file in your project root. Cognee loads .env automatically on
import, so it must exist before you import cognee.
# Tigris credentials (boto3/s3fs read these automatically)
AWS_ACCESS_KEY_ID=tid_YOUR_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=tsec_YOUR_SECRET_ACCESS_KEY
AWS_REGION=auto
AWS_ENDPOINT_URL=https://t3.storage.dev
# LLM provider
LLM_API_KEY=sk-your-openai-api-key
LLM_MODEL=openai/gpt-4o-mini
# Store everything on Tigris instead of the local filesystem
STORAGE_BACKEND=s3
STORAGE_BUCKET_NAME=my-agent-memory
DATA_ROOT_DIRECTORY=s3://my-agent-memory/cognee/data
SYSTEM_ROOT_DIRECTORY=s3://my-agent-memory/cognee/system
| Variable | Purpose |
|---|---|
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY | Tigris credentials. Cognee and boto3 read these automatically. |
AWS_REGION | Set to auto. Tigris handles region routing for you. |
AWS_ENDPOINT_URL | Points S3 requests at Tigris instead of AWS. Required for any S3-compatible endpoint. |
STORAGE_BACKEND | Set to s3 to use Tigris instead of the local filesystem. Without it, Cognee writes to disk even if you've set s3:// URIs elsewhere. |
STORAGE_BUCKET_NAME | Your Tigris bucket name. Cognee uses it to auto-configure the cache directory on S3 (e.g. s3://my-bucket/cognee/cache). |
DATA_ROOT_DIRECTORY | Where Cognee stores raw ingested data and file uploads. This is your durable source of truth. |
SYSTEM_ROOT_DIRECTORY | Where Cognee stores its databases: vector indexes (LanceDB), the knowledge graph (Ladybug), and metadata (SQLite). |
Step 4: Build agent memory
Here's a complete example that teaches your agent a couple of facts and then queries what it knows. Run it once to confirm everything is wired up before integrating into your agent loop.
import asyncio
import cognee
async def main():
# Start fresh during development (drops all memory for the current user).
await cognee.forget(everything=True)
# Teach the agent. remember() ingests the text AND builds the graph,
# so there's no separate "process" step.
await cognee.remember(
"Tigris is a globally distributed, S3-compatible object storage "
"service. It distributes data to regions closest to your users and "
"caches frequently accessed data at the edge.",
dataset_name="facts",
)
await cognee.remember(
"Cognee is a memory engine for AI agents. It builds knowledge graphs "
"from unstructured data and retrieves with combined vector similarity "
"and graph traversal.",
dataset_name="facts",
)
# Recall. Cognee auto-routes to the best retrieval strategy.
results = await cognee.recall("How does Tigris distribute data globally?")
for i, result in enumerate(results, 1):
print(f"[{i}] {result}")
return results
if __name__ == "__main__":
asyncio.run(main())
What each verb does
cognee.remember()ingests raw content and builds structured memory in one call: chunking, embedding generation, entity extraction, and knowledge graph construction. WithSTORAGE_BACKEND=s3, the raw input is persisted to Tigris immediately, so your source of truth is safe even if a later step fails.cognee.recall()retrieves relevant information, querying vector indexes for semantic matches and traversing the knowledge graph for related entities. It auto-selects the retrieval strategy, so you usually just pass a question.cognee.improve()(optional) enriches an existing graph. It applies feedback weights and builds indexes.remember()runs a light improvement pass by default. Call it directly when you want heavier enrichment.cognee.forget()removes memory. See Managing memory with forget.
For short-term context, pass session_id="..." to remember() and recall().
Session data is stored for fast retrieval and bridged into the permanent graph
in the background. This is how per-conversation memory graduates into long-term
knowledge.
Step 5: Feed memory from files and S3
Your agent can learn from local files, S3 objects, or a mix. This is useful for bootstrapping an agent with an existing document corpus.
import asyncio
import cognee
async def feed_documents():
await cognee.remember("/path/to/research-paper.pdf", dataset_name="corpus")
await cognee.remember("/path/to/documents/", dataset_name="corpus") # whole directory
await cognee.remember("s3://my-agent-memory/uploads/report.txt", dataset_name="corpus")
await cognee.remember("s3://my-agent-memory/uploads/", dataset_name="corpus") # all files under a prefix
results = await cognee.recall("key findings about distributed storage")
for result in results:
print(result)
asyncio.run(feed_documents())
An s3:// URI to a single file ingests that file. A prefix URI (ending in /)
recursively discovers everything underneath it, so you can point Cognee at an
entire document library in one call. Inputs already on s3:// are read in
place. Cognee records them by reference rather than copying them.
Choosing a database backend
Cognee is three stores plus a raw-data layer. The stores are relational (state), vector (embeddings), and graph (entities). Raw data belongs on Tigris in every configuration. The structured stores are pluggable. The right choice depends on whether you run one Cognee process or many.
Default: file-based stores (single instance)
SQLite, Ladybug, and LanceDB are embedded engines. With the Step 3 config they live entirely on Tigris, with Cognee syncing the database files to and from S3, pulling on open and pushing on commit. This is ideal for development and single-worker agents, since there is no infrastructure to run and everything sits in one bucket.
This sync is last-writer-wins. It is safe for a single Cognee process, but multiple instances writing concurrently can clobber each other's database files. For distributed deployments, use Postgres below.
Postgres: networked stores (distributed production)
When several stateless Cognee instances share memory, the structured stores need
a networked database. One Postgres instance backs all three: relational, vector
(pgvector), and graph. Raw data stays on Tigris. Install
"cognee[postgres,aws]" and use this .env instead:
# Raw data still on Tigris
STORAGE_BACKEND=s3
AWS_ENDPOINT_URL=https://t3.storage.dev
AWS_REGION=auto
AWS_ACCESS_KEY_ID=tid_YOUR_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=tsec_YOUR_SECRET_ACCESS_KEY
DATA_ROOT_DIRECTORY=s3://my-agent-memory/cognee/data
SYSTEM_ROOT_DIRECTORY=s3://my-agent-memory/cognee/system
# Structured stores on one Postgres (local, or managed: Neon / Supabase / RDS)
DB_PROVIDER=postgres
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USERNAME=cognee
DB_PASSWORD=cognee
DB_NAME=cognee_db
VECTOR_DB_PROVIDER=pgvector
GRAPH_DATABASE_PROVIDER=postgres
# Embedding dims must stay within pgvector's index ceiling (2000)
EMBEDDING_MODEL=openai/text-embedding-3-small
EMBEDDING_DIMENSIONS=1536
The Postgres graph and pgvector adapters reuse the DB_* credentials, so one
instance serves all three structured stores. The Postgres must have the vector
extension available (managed providers like Neon and Supabase ship it).
The application code does not change between backends. The same remember,
recall, improve, and forget calls run the same way whether memory sits on
the file-based defaults or Postgres. Only the .env differs.
What can live where
| Layer | Tigris (s3://) | Networked DB |
|---|---|---|
| Raw data | Yes, always | — |
| Vectors | Yes (LanceDB) | Yes (pgvector) |
| Relational | Single instance only (file sync) | Yes (Postgres) |
| Graph | Single instance only (file sync) | Yes (Postgres / Neo4j) |
Managing memory with forget
forget replaces the older prune/delete calls with one set of patterns:
# Forget a single item from a dataset
await cognee.forget(data_id=item_id, dataset="corpus")
# Forget an entire dataset (raw data + graph + vectors)
await cognee.forget(dataset="corpus")
# Drop only the graph/vectors but KEEP raw files, so you can re-process
# with different settings (e.g. a new embedding model)
await cognee.forget(dataset="corpus", memory_only=True)
# Forget everything the current user owns (handy as a dev reset)
await cognee.forget(everything=True)
Raw data is the durable source of truth on Tigris, which makes
memory_only=True the safe way to rebuild derived memory. Drop the graph and
vectors, then remember the same data again under new settings, and you never
re-upload the files.
Experimenting with memory: snapshots and forks
Because your memory lives in a bucket, you can treat whole memory states as cheap, branchable objects rather than just durable storage.
- Snapshot before anything destructive. A bulk
forgetor a heavyimproverewrites memory in place, so snapshot the bucket first and a bad run becomes a one-command rollback instead of a full re-ingestion. - Fork to experiment. Fork the bucket and point a throwaway agent at the copy to trial a new embedding model, a different ontology, or an aggressive pruning policy, all against real memory with zero risk to production.
- Branch per tenant or per run. Give each customer or each experiment its own fork so their memories evolve independently from a shared baseline.
In the default file-based config, a single bucket snapshot captures the entire memory, because raw data, vectors, and graph all live under one bucket. A fork is therefore a complete, independent memory the agent can mutate freely. The storagesdk makes both a one-liner:
import { Storage } from "@storagesdk/core";
import { tigris } from "@storagesdk/adapters/tigris";
const storage = new Storage({ adapter: tigris({ bucket: "my-agent-memory" }) });
// Capture the current memory state before a risky change
const snap = await storage.snapshots.create({ name: "pre-improve" });
// ...run cognee.improve(...) / cognee.forget(...)...
// If it goes badly, the snapshot is your rollback.
// Or branch the whole memory so an agent can experiment on a copy
const fork = await storage.forks.create({
name: "exp-new-embeddings",
fromSnapshot: snap.id,
});
With the Postgres backend, the structured stores live in Postgres rather than the bucket, so a bucket snapshot captures only raw data. Pair it with a Postgres backup on the same schedule if you want derived memory restored to the same instant, too.
Per-agent memory isolation
For multi-agent or multi-user systems, Cognee's ENABLE_BACKEND_ACCESS_CONTROL
is True by default. Cognee keeps separate databases per user and dataset, so
agents can't read each other's memory. Scope memory by passing distinct
dataset_name values to remember and datasets=[...] to recall.
Production considerations
Performance
Tigris caches frequently accessed objects at edge locations closest to your users. Agents running repeated lookups against the same knowledge base benefit automatically, with no configuration needed. Cold reads see S3-level latency, and recently accessed data comes back faster.
Cost
Tigris charges for storage but has zero egress fees. That matters for agents that query memory repeatedly, and for multi-region deployments where agents and users sit far apart. Keeping raw data on Tigris also keeps Postgres sized to structured state, instead of bloating it with blobs.
Security
Never commit credentials to version control. In production, prefer IAM roles or instance profiles over static keys.
Lock down Cognee before exposing it to external traffic:
ACCEPT_LOCAL_FILE_PATH=False # Disable local file path access
ALLOW_HTTP_REQUESTS=False # Restrict outbound requests
REQUIRE_AUTHENTICATION=True # Enable API auth
ENABLE_BACKEND_ACCESS_CONTROL=True # Per-agent isolation
Troubleshooting
| Issue | Solution |
|---|---|
| Environment variables not loaded | Ensure .env exists before import cognee (it loads .env on import). Variables set after import won't take effect. |
| Authentication errors | Verify AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are set. Tigris keys start with tid_ and tsec_. Watch for whitespace when copy-pasting. |
| Region errors | Set AWS_REGION=auto. Tigris handles routing automatically. |
| Wrong S3 endpoint | Confirm AWS_ENDPOINT_URL=https://t3.storage.dev is in your .env. |
| Cognee uses local storage | Ensure STORAGE_BACKEND=s3 and that DATA_ROOT_DIRECTORY / SYSTEM_ROOT_DIRECTORY use s3:// URIs. |
| Database locked / corrupted under load | Multiple instances writing file-based stores on S3. Switch to the Postgres backend. |
| pgvector index creation fails | Embedding dimensions exceed pgvector's 2000-dim index ceiling. Use a ≤1536-dim model like text-embedding-3-small. |
| Slow first query | Expected. The first read fetches from Tigris; subsequent queries benefit from edge caching. |
| boto3 addressing errors | Tigris requires virtual addressing style. Use Config(s3={"addressing_style": "virtual"}) with boto3 directly. LanceDB and s3fs handle this automatically. |