Skip to main content

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:

VerbWhat it does
rememberStore data permanently: ingest it and build the knowledge graph.
recallRetrieve relevant knowledge, auto-routing across graph, vector, and lexical search.
improveEnrich the graph: apply feedback, build indexes, fold session memory into permanent memory.
forgetRemove 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

  1. A Tigris account with an Access Key ID and Secret Access Key (keys start with tid_ and tsec_). Create them via the Tigris Access Key guide.
  2. A Tigris bucket for storing agent memory.
  3. Python 3.10+ installed.
  4. 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 s3api create-bucket \
--bucket my-agent-memory \
--endpoint-url https://t3.storage.dev \
--region auto
note

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
VariablePurpose
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYTigris credentials. Cognee and boto3 read these automatically.
AWS_REGIONSet to auto. Tigris handles region routing for you.
AWS_ENDPOINT_URLPoints S3 requests at Tigris instead of AWS. Required for any S3-compatible endpoint.
STORAGE_BACKENDSet 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_NAMEYour Tigris bucket name. Cognee uses it to auto-configure the cache directory on S3 (e.g. s3://my-bucket/cognee/cache).
DATA_ROOT_DIRECTORYWhere Cognee stores raw ingested data and file uploads. This is your durable source of truth.
SYSTEM_ROOT_DIRECTORYWhere 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

  1. cognee.remember() ingests raw content and builds structured memory in one call: chunking, embedding generation, entity extraction, and knowledge graph construction. With STORAGE_BACKEND=s3, the raw input is persisted to Tigris immediately, so your source of truth is safe even if a later step fails.
  2. 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.
  3. 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.
  4. cognee.forget() removes memory. See Managing memory with forget.
note

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())
tip

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.

warning

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).

tip

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

LayerTigris (s3://)Networked DB
Raw dataYes, always
VectorsYes (LanceDB)Yes (pgvector)
RelationalSingle instance only (file sync)Yes (Postgres)
GraphSingle 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 forget or a heavy improve rewrites 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,
});
note

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

tip

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

warning

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

IssueSolution
Environment variables not loadedEnsure .env exists before import cognee (it loads .env on import). Variables set after import won't take effect.
Authentication errorsVerify 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 errorsSet AWS_REGION=auto. Tigris handles routing automatically.
Wrong S3 endpointConfirm AWS_ENDPOINT_URL=https://t3.storage.dev is in your .env.
Cognee uses local storageEnsure STORAGE_BACKEND=s3 and that DATA_ROOT_DIRECTORY / SYSTEM_ROOT_DIRECTORY use s3:// URIs.
Database locked / corrupted under loadMultiple instances writing file-based stores on S3. Switch to the Postgres backend.
pgvector index creation failsEmbedding dimensions exceed pgvector's 2000-dim index ceiling. Use a ≤1536-dim model like text-embedding-3-small.
Slow first queryExpected. The first read fetches from Tigris; subsequent queries benefit from edge caching.
boto3 addressing errorsTigris requires virtual addressing style. Use Config(s3={"addressing_style": "virtual"}) with boto3 directly. LanceDB and s3fs handle this automatically.

References