> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognee.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Sessions and Caching

> Learn how Cognee handles short-term memory with sessions and caching.

In Cognee, a session defines the scope for a single conversation or agent run. It maintains a cache of short-term information, including recent queries, responses, and the context used to answer them.

## What Is a Session?

A session is Cognee's short-term memory for a specific user. It is identified by `(user_id, session_id)` and stores an ordered list of recent interactions.

In the v1.0 API, you interact with sessions through [`remember()`](/core-concepts/main-operations/remember) and [`recall()`](/core-concepts/main-operations/recall):

* `cognee.remember(data, session_id="my_session")` — writes content directly into the session cache for fast retrieval.
* `cognee.recall(query_text, session_id="my_session")` — searches session cache entries first, then falls through to the permanent graph if nothing matches.

The lower-level [`cognee.search()`](/core-concepts/main-operations/legacy-operations/search) also accepts `session_id`. Session-aware retrieval is used across the main completion-oriented search paths, including graph-completion variants, RAG, hybrid, triplet, temporal, and agentic retrieval.

For session-aware retrieval, omitting `session_id` still stores the turn when caching is enabled — it does not disable sessions. The session it lands in is **scoped to the dataset**: when Cognee knows which dataset the call runs against, the default resolves to `default_session_<dataset_id>` (the dataset's UUID appended to `default_session`). Because every dataset gets its own default session, two datasets can no longer mix their turns into one shared conversation. Only when no dataset is known at all does the default fall back to the plain global `default_session`. This is different from `remember()`, where omitting `session_id` writes directly to permanent memory instead of creating a session.

Reads follow the same rule as writes, so a turn written without a `session_id` is readable back without one. A bare `cognee.session.get_session()` outside any dataset context resolves to the default session of your existing `main_dataset` — the dataset Cognee uses when no dataset is stated. If no `main_dataset` exists yet, the call raises a `SessionPreconditionError` (a `CogneeValidationError`) rather than silently returning the unscoped global session; add data first, run inside a dataset context, or pass an explicit `session_id`. To scope conversations explicitly, pass your own `session_id` — an explicit value is always stored and read unchanged, with no dataset suffix applied.

Previously, an omitted `session_id` always resolved to the single global `default_session`, so turns from different datasets shared one history. Entries already stored there are not migrated — pass `session_id="default_session"` explicitly to keep reading them.

Sessions only affect completion-oriented search. The completion search types — `GRAPH_COMPLETION` and its variants (`GRAPH_COMPLETION_COT`, `GRAPH_COMPLETION_CONTEXT_EXTENSION`, `GRAPH_SUMMARY_COMPLETION`, `TEMPORAL`), `RAG_COMPLETION`, `HYBRID_COMPLETION`, `TRIPLET_COMPLETION`, and `AGENTIC_COMPLETION` — read and write session history. Retrieval-only types (`CHUNKS`, `SUMMARIES`, etc.) accept `session_id` but do not use or write session history. For multi-tenant or background jobs, pass an explicit `user` so the default user is not used.

Cognee reads from session memory at the start of a retrieval to recover earlier turns. When the retrieval finishes, it writes a new interaction to the session so the history grows over time.

Using the same `session_id` across calls allows Cognee to include previous interactions as conversational history in the LLM prompt, enabling follow-up questions and contextual awareness.

To inspect stored history, use `cognee.session.get_session(session_id=..., last_n=...)`. To annotate a stored entry, use `cognee.session.add_feedback(...)` and `cognee.session.delete_feedback(...)`.

<Note>
  Sessions require caching to be enabled. See the next sections and Configuration Details below. If caching is disabled or unavailable, searches still work but without access to previous interactions.
</Note>

## Session Cache vs Permanent Memory

Cognee keeps two distinct kinds of memory. `remember()` writes to one or the other depending on whether you pass `session_id`:

|                    | Session cache (short-term)                                                                                                                                                                                                                                             | Permanent memory (knowledge graph)                                                                                                                                                                                                                                                 |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **How to write**   | `remember(data, session_id="...")`                                                                                                                                                                                                                                     | `remember(data)` (no `session_id`)                                                                                                                                                                                                                                                 |
| **What happens**   | Raw text is written straight to the cache as a Q\&A entry — no chunking, no entity extraction, no embeddings                                                                                                                                                           | Runs the full [Add](/core-concepts/main-operations/legacy-operations/add) + [Cognify](/core-concepts/main-operations/legacy-operations/cognify) pipeline: chunking, entity/relationship extraction, and embeddings, plus an [Improve](/core-concepts/main-operations/improve) pass |
| **Latency / cost** | Near-instant, no LLM calls                                                                                                                                                                                                                                             | Heavier — LLM and embedding calls scale with input size                                                                                                                                                                                                                            |
| **Scope**          | One conversation, keyed by `(user_id, session_id)`                                                                                                                                                                                                                     | A named dataset, shared across all sessions                                                                                                                                                                                                                                        |
| **Lifetime**       | Expires roughly `SESSION_TTL_SECONDS` (default 7 days) after the session's last write, and is also cut short when the data it was built on is deleted (see *Invalidation when the underlying data is deleted* under [Additional Information](#additional-information)) | Durable until you [Forget](/core-concepts/main-operations/forget) it                                                                                                                                                                                                               |
| **Best for**       | Conversation turns, scratch context, recent interactions                                                                                                                                                                                                               | Documents, facts, anything you want to query later as a graph                                                                                                                                                                                                                      |

Passing `session_id` does **not** run graph extraction on that content — the write is raw and fast by design. This is why `remember(data, session_id=...)` does not, on its own, place data in the permanent graph. When `self_improvement=True` (the default), it additionally kicks off a background [Improve](/core-concepts/main-operations/improve) pass that bridges cached turns, agent traces, and accepted distilled session guidance into the permanent graph; with `self_improvement=False`, the content stays in the cache only until you explicitly call `cognee.improve(dataset=..., session_ids=[...])`. To write straight to permanent memory, call `remember()` without a `session_id`.

## How Sessions Work

Sessions integrate with both the v1.0 operations and the lower-level search pipeline.

**v1.0 session flow (via `recall`):**

When you call `cognee.recall(query_text, session_id="my_session")`:

1. **Check session cache** – Cognee searches the session cache for matching entries using keyword matching
2. **Fall through to graph** – If no session entries match, retrieval continues against the permanent knowledge graph
3. **Return tagged results** – Results include a `_source` field indicating whether they came from `"session"` or `"graph"`

**Lower-level session flow (via `search`):**

When you call `cognee.search()` with a `session_id`:

1. **Retrieve context** – Cognee finds relevant graph elements for your query
2. **Load conversation history** – If caching is enabled, previous interactions for `(user_id, session_id)` are loaded
3. **Generate answer** – The LLM receives the query, graph context, and retrieved history
4. **Save interaction** – A new Q\&A entry is stored in the session cache

## Cache Adapters

Cognee supports three cache adapters for storing sessions: SQL (the default — SQLite or Postgres), Redis, and Filesystem. Redis or Postgres keep the cache in an external service so it outlives the local machine, while SQLite (the default) and Filesystem give you a simple local cache without network dependencies. All provide the same functionality; only the storage backend differs. Below are the configuration options for each adapter with additional details.

<Tabs>
  <Tab title="SQL (default)">
    The default backend stores sessions in a SQL database via SQLAlchemy — SQLite for a zero-setup local cache, or Postgres for a cache hosted in an external database:

    ```dotenv theme={null}
    CACHING=true
    CACHE_BACKEND=sqlite   # default; or postgres
    ```

    With `CACHE_BACKEND=sqlite` and no further configuration, sessions live in a `cache.db` file next to the relational SQLite database. With `CACHE_BACKEND=postgres`, the connection falls back to the relational `DB_*` settings. Either backend can point at a specific database with `CACHE_DB_URL`:

    ```dotenv theme={null}
    CACHE_DB_URL=postgresql+asyncpg://cognee:cognee@localhost:5432/cognee_db
    ```

    * `sqlite`: zero setup, no network dependency; local to one machine
    * `postgres`: hosted in an external database, so the cache survives the local machine
    * Both run the same SQL adapter; only the connection-URL resolution differs
  </Tab>

  <Tab title="Redis">
    Add to your `.env` file:

    ```dotenv theme={null}
    CACHING=true
    CACHE_BACKEND=redis
    CACHE_HOST=localhost
    CACHE_PORT=6379
    ```

    **Start Redis:**

    ```bash theme={null}
    # Using Docker
    docker run -d -p 6379:6379 redis:latest

    # Or using local installation
    redis-server
    ```

    For managed Redis services that require in-transit encryption, add TLS settings to the same Redis configuration:

    ```dotenv theme={null}
    CACHE_HOST=my-cache.example.cache.amazonaws.com
    CACHE_USERNAME=default
    CACHE_PASSWORD=your_password
    CACHE_SSL=true
    CACHE_SSL_CERT_REQS=required
    ```

    * Fast in-memory storage
    * Requires a running Redis instance and network connectivity
    * Optional TLS for managed Redis via `CACHE_SSL` / `CACHE_SSL_CERT_REQS`
  </Tab>

  <Tab title="Filesystem">
    **Configuration:**

    Add to your `.env` file:

    ```dotenv theme={null}
    CACHING=true
    CACHE_BACKEND=fs
    ```

    * Sessions are stored in `{DATA_ROOT_DIRECTORY}/.cognee_fs_cache/sessions_db`.
    * Stores session data on the local filesystem using `diskcache`
    * No network dependency
  </Tab>
</Tabs>

## Additional Information

<AccordionGroup>
  <Accordion title="Invalidation when the underlying data is deleted">
    A session's lifetime is not only bounded by `SESSION_TTL_SECONDS`. Deleting the data a session was built on also removes the cached turns that quoted it, so completions and session context stop asserting content that no longer exists.

    Sessions are attributed to a dataset — through the `dataset_id` recorded on the session, or through the per-dataset default session id (`default_session_<dataset_id>`) — and that attribution decides what a delete reaches:

    * **Dataset-level deletes** — [`forget(dataset=...)`](/core-concepts/main-operations/forget), `forget(dataset=..., memory_only=True)`, and [`datasets.empty_dataset()`](/python-api/datasets#datasets-empty_dataset) — delete every session attributed to that dataset.
    * **Single-document deletes** — [`forget(data_id=..., dataset=...)`](/core-concepts/main-operations/forget) and [`datasets.delete_data()`](/python-api/datasets#datasets-delete_data) — remove only the contaminated entries. A turn is contaminated when the graph elements it recorded using overlap the nodes and edges the delete removed; contamination then follows the provenance chain inside that session (turn → feedback referencing it → session-context lesson distilled from that feedback → later turns that consumed the lesson) and everything on the chain is removed. Unrelated turns in the same session survive.
    * **`forget(everything=True)`** still prunes the whole cache.

    This cleanup is **best-effort by contract**: it must never fail the delete that triggered it. A cache error is logged as a warning and the graph, vector, and relational deletions complete regardless.

    Known limits:

    * Agent-trace entries store context as text without graph element ids, so they are not matched by the targeted pass.
    * The `tapes` cache backend is append-only and never sees deletes.
    * Sessions created before dataset attribution existed are only discoverable through the `default_session_<dataset_id>` naming.
  </Accordion>

  <Accordion title="Session Lifecycle Persistence (Relational DB)">
    In addition to the cache layer, Cognee persists session lifecycle metadata to the relational database (SQLite or Postgres). Running database migrations — either via `await cognee.run_migrations()` or `alembic upgrade head` — creates the required relational tables for this metadata.

    Two tables are created:

    <Tabs>
      <Tab title="session_records">
        One row per `(user_id, session_id)`:

        | Column             | Type                 | Description                                                                 |
        | ------------------ | -------------------- | --------------------------------------------------------------------------- |
        | `session_id`       | String (PK)          | The caller-supplied session identifier.                                     |
        | `user_id`          | UUID (PK)            | The owning user. Same `session_id` from two users is two separate sessions. |
        | `dataset_id`       | UUID (nullable)      | Associated dataset, if any.                                                 |
        | `status`           | String               | Stored status: `running`, `completed`, or `failed`.                         |
        | `started_at`       | Timestamp            | When the session started.                                                   |
        | `last_activity_at` | Timestamp            | When the session last received an LLM call.                                 |
        | `ended_at`         | Timestamp (nullable) | When the session was marked completed or failed.                            |
        | `tokens_in`        | Integer              | Cumulative input tokens across all LLM calls in this session.               |
        | `tokens_out`       | Integer              | Cumulative output tokens.                                                   |
        | `cost_usd`         | Float                | Estimated cumulative cost in USD.                                           |
        | `error_count`      | Integer              | Number of errors recorded in this session.                                  |
        | `last_model`       | Text (nullable)      | Most recently used LLM model name.                                          |
      </Tab>

      <Tab title="session_model_usage">
        One row per `(session_id, user_id, model)`:

        | Column       | Type        | Description                                 |
        | ------------ | ----------- | ------------------------------------------- |
        | `session_id` | String (PK) | The session.                                |
        | `user_id`    | UUID (PK)   | The owning user.                            |
        | `model`      | Text (PK)   | The model name (e.g. `openai/gpt-4o-mini`). |
        | `tokens_in`  | Integer     | Input tokens attributed to this model.      |
        | `tokens_out` | Integer     | Output tokens attributed to this model.     |
        | `cost_usd`   | Float       | Cost attributed to this model.              |
        | `updated_at` | Timestamp   | When this row was last updated.             |
      </Tab>
    </Tabs>

    Splitting per-model usage out of `session_records` allows mixed-model sessions (e.g. a completion model plus an embedding model) to attribute cost correctly.

    Because the token/cost estimate is computed locally from prompt and completion text, this tracking works the same way across every configured [LLM provider](/setup-configuration/llm-providers) — OpenAI, Anthropic, Gemini (Google AI Studio and Vertex AI), Bedrock, Mistral, Ollama, and any `custom` LiteLLM-routed provider. It does not depend on the provider returning usage metadata, and it works in Docker and self-hosted deployments as long as caching is enabled.

    Cost figures come from Cognee's built-in pricing table keyed on `LLM_MODEL`. If your model is not in that table (for example, a Vertex AI custom endpoint or a self-hosted `custom` model), `tokens_in` and `tokens_out` are still recorded, but `cost_usd` may be `0` or based on a fallback rate. Use the token counts for those models and compute cost from your provider's pricing.

    Tracking depends on caching being available, and only reflects **session-scoped completion calls**, not every LLM-capable step in the broader ingestion pipeline — see [Recall](/core-concepts/main-operations/recall) for retrieval and `only_context=True`, and [Cognify](/core-concepts/main-operations/legacy-operations/cognify) for ingestion-time call counts. If you omit `session_id`, usage is still tracked, attributed per dataset to `default_session_<dataset_id>` (see [What Is a Session?](#what-is-a-session) above) rather than accumulating on one shared `default_session`.

    **Session visibility rules**

    Each `session_records` row remains keyed by the `user_id` that actually created the session, but the HTTP read paths can surface any rows visible to the requesting user at read time. That includes the requesting user's own sessions, sessions created by child-agent users whose `parent_user_id` matches the requesting user's `id`, and sessions visible through dataset read permissions. See [Users](/core-concepts/multi-user-mode/permissions-system/users) for how to create agent users with `parent_user_id`.

    ```http theme={null}
    GET /api/v1/sessions
    GET /api/v1/sessions/{session_id}
    GET /api/v1/sessions/stats?range=30d
    GET /api/v1/sessions/cost-by-model?range=30d
    ```

    * `GET /api/v1/sessions` lists sessions visible to the caller
    * `GET /api/v1/sessions/{session_id}` returns per-session fields such as `tokens_in`, `tokens_out`, and `cost_usd`
    * `GET /api/v1/sessions/stats?range=30d` returns aggregate totals for `24h`, `7d`, `30d`, or `all`
    * `GET /api/v1/sessions/cost-by-model?range=30d` breaks usage down by model

    **Session status lifecycle:**

    Sessions move through: `running` → `completed` or `failed`. The `abandoned` status is never written to the database — it is computed at read time: a session whose `last_activity_at` is older than the abandonment threshold and is still in `running` state is reported as `abandoned`. The threshold defaults to 30 minutes and is configurable:

    ```dotenv theme={null}
    SESSION_ABANDON_AFTER_SECONDS=1800  # default: 30 minutes
    ```

    This means no background sweeper is needed to mark stale sessions. Reads include the effective status automatically.

    <Note>
      Token counts use a character-based estimate (`len(text) // 4`) when the LLM client does not return exact usage counts. These are approximate and suitable for dashboard aggregates rather than precise billing.
    </Note>
  </Accordion>

  <Accordion title="Session Data Structure">
    Sessions store interactions as JSON entries in a list. Each item returned by `cognee.session.get_session()` is a `SessionQAEntry` model with the following fields:

    | Field                    | Type                             | Description                                                                                                                              |
    | ------------------------ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
    | `time`                   | `str`                            | ISO 8601 timestamp when the entry was created.                                                                                           |
    | `qa_id`                  | `Optional[str]`                  | Unique identifier for the Q\&A turn. Cognee assigns a UUID when the turn is stored; use this ID with feedback and per-entry update APIs. |
    | `question`               | `str`                            | The user's original query text.                                                                                                          |
    | `context`                | `str`                            | Retrieved context used to answer the question. May be empty if summarization is not enabled.                                             |
    | `answer`                 | `str`                            | The generated answer.                                                                                                                    |
    | `feedback_text`          | `Optional[str]`                  | Free-form feedback text, or `None` if not set.                                                                                           |
    | `feedback_score`         | `Optional[int]`                  | Integer rating from `1` to `5`, or `None` if not set.                                                                                    |
    | `used_graph_element_ids` | `Optional[Dict[str, List[str]]]` | Graph node and edge IDs used during retrieval. Keys are `node_ids` and `edge_ids`.                                                       |
    | `memify_metadata`        | `Optional[Dict[str, bool]]`      | Session persistence and memify status flags, such as `feedback_weights_applied`.                                                         |

    Sessions are keyed by `agent_sessions:{user_id}:{session_id}`.

    Each user can have multiple sessions, each maintaining its own cache of short-term information.
  </Accordion>

  <Accordion title="Reading Session History (get_session())">
    Use `cognee.session.get_session()` to retrieve stored Q\&A entries for a session. Entries are returned in chronological order (oldest first) — use `entries[-1]` for the most recent entry. If the session does not exist or the cache backend is unavailable, this call returns an empty list instead of raising an error.

    <ParamField path="session_id" type="Optional[str]" default="None">
      Identifier of the session to retrieve. When set, it must match the `session_id` previously passed to `cognee.recall()`. When `None`, it resolves to the same dataset-scoped default session the write side uses (see [What Is a Session?](#what-is-a-session) above). Pass the literal `"default_session"` to read the global (legacy) session instead.
    </ParamField>

    <ParamField path="last_n" type="Optional[int]" default="None">
      Maximum number of most-recent entries to return. When `None`, all stored entries are returned.
    </ParamField>

    <ParamField path="user" type="Optional[User]" default="None">
      User that owns the session. When `None`, Cognee resolves it from the current session context or falls back to the default user.
    </ParamField>

    Returns `List[SessionQAEntry]` (see [Session Data Structure](#session-data-structure) above), which may be empty.

    ```python theme={null}
    import cognee

    entries = await cognee.session.get_session(session_id="conversation_1", last_n=5)
    for entry in entries:
        print(f"[{entry.time}] Q: {entry.question}")
        print(f"           A: {entry.answer}")
        print(f"  feedback score: {entry.feedback_score}")
    ```
  </Accordion>

  <Accordion title="Upstream context and the include_context flag">
    Every Q\&A entry stored in the session cache contains a `context` field. Depending on how the completion was generated, this field may be empty or may contain a stored summary of the retrieved context for that turn. You can inspect it programmatically when reading session history.

    **The `include_context` flag:**

    <Note>
      `get_session_manager()` is an internal, lower-level API rather than the usual SDK entry point. Prefer `cognee.session.get_session()` unless you specifically need formatted history control such as `include_context`.
    </Note>

    `SessionManager.get_session()` and `SessionManager.format_entries()` both accept `include_context: bool` (default `True`). When `True`, a `CONTEXT:` line is included for each entry in the formatted history string; when `False`, it is omitted.

    This flag is not exposed on `cognee.recall()` or `cognee.session.get_session()`. If you need it, use the lower-level `SessionManager` directly.

    **Additional information:**

    <AccordionGroup>
      <Accordion title="Example: reading context from past entries">
        ```python theme={null}
        import cognee

        entries = await cognee.session.get_session(session_id="my_session", last_n=5)
        for entry in entries:
            print("Q:", entry.question)
            print("Stored context:", entry.context)   # may be empty or a stored summary
            print("A:", entry.answer)
        ```
      </Accordion>

      <Accordion title="Does the LLM automatically see context from previous turns?">
        No. When Cognee builds conversation history for the LLM during `cognee.recall()`, it uses `include_context=False` internally — previous questions and answers are included in the prompt, but the stored context from those earlier turns is omitted. Fresh graph context is retrieved for the current query only. This keeps prompts compact and avoids re-sending large context blobs.
      </Accordion>

      <Accordion title="Using SessionManager directly">
        If your use-case requires the LLM to see stored context from a prior turn — for example to trace provenance or build a richer prompt — use the lower-level `SessionManager` to retrieve formatted history with `include_context=True`, then pass that history to your own LLM call.

        ```python theme={null}
        from cognee.infrastructure.session.get_session_manager import get_session_manager
        from cognee.modules.users.methods import get_default_user

        user = await get_default_user()
        sm = get_session_manager()

        # Formatted string with CONTEXT included per entry (default)
        history_with_context = await sm.get_session(
            user_id=str(user.id),
            session_id="my_session",
            formatted=True,
            include_context=True,    # includes CONTEXT: line for each Q&A turn
        )
        ```
      </Accordion>
    </AccordionGroup>
  </Accordion>

  <Accordion title="Configuration Details">
    **Environment Variables:**

    * `CACHING` (bool): Enable/disable caching (default: `true`). Set to `false` to disable session storage and conversational memory.
    * `AUTO_FEEDBACK` (bool): Enable automatic session-context guidance and feedback detection on each answered turn (default: `true`). Requires `CACHING` to be on and uses the resolved session (the dataset-scoped default session, `default_session_<dataset_id>`, when `session_id` is omitted). When enabled, every search turn — retrieval-only types included — runs one extra structured-output LLM call to analyze the turn against the previous one (skipped when `only_context=True`). `SESSION_SEARCH_MODE` decides whether that call runs alongside the answer or before retrieval (see [Session-context guidance](#session-context-guidance-auto-feedback) below). Set to `false` to disable the extra call and restore plain history-only sessions.
    * `SESSION_SEARCH_MODE` (str): How one session turn executes — `"concurrent"` (default) or `"sequential"`. Both modes make the same two LLM calls per answered turn; they differ in how those calls are sequenced and therefore in which turn the analysis can influence. `"concurrent"` runs the turn analysis alongside retrieval and answer generation, so a turn costs roughly one answer call of wall-clock time. `"sequential"` runs the analysis first, so its rewritten query drives retrieval and its context updates reach the same turn's answer. The setting is deployment-wide — there is no per-request override. Setting `AUTO_FEEDBACK=false` skips the analysis call in both modes, but does not disable the mode itself: eligible calls in `"concurrent"` mode still run the dual-query retrieval and merge described below. See [Session-context guidance](#session-context-guidance-auto-feedback) below for the full behavioral difference and the cases that always fall back to sequential.
    * `CACHE_BACKEND` (str): `"sqlite"` (default), `"postgres"`, `"redis"`, `"fs"`, or `"tapes"`. `"sqlite"` and `"postgres"` both use the SQL cache adapter and differ only in how the connection URL is resolved; when set to `"fs"`, sessions are stored on local disk; when set to `"redis"`, sessions are stored in Redis and shared across processes; when set to `"tapes"`, sessions are stored locally and new Q\&A turns are mirrored to a running Tapes ingest service.
    * `CACHE_DB_URL` (str, optional): SQLAlchemy async URL for the SQL cache backends (e.g. `postgresql+asyncpg://cognee:cognee@localhost:5432/cognee_db`). When unset, `"sqlite"` uses a `cache.db` file next to the relational SQLite database and `"postgres"` falls back to the relational `DB_*` settings.
    * `CACHE_HOST` (str): Redis hostname (default: `"localhost"`)
    * `CACHE_PORT` (int): Redis port (default: `6379`)
    * `CACHE_USERNAME` (str, optional): Redis username
    * `CACHE_PASSWORD` (str, optional): Redis password
    * `CACHE_SSL` (bool): Connect to Redis over TLS (default: `false`). Enable for managed Redis with in-transit encryption (AWS ElastiCache, GCP Memorystore, Azure Cache for Redis). Applies only to the `redis` backend.
    * `CACHE_SSL_CERT_REQS` (str): TLS certificate verification when `CACHE_SSL` is enabled — `"required"` (default), `"optional"`, or `"none"`. Use `"none"` to skip verification (e.g. self-signed certificates).
    * `SESSION_TTL_SECONDS` (int, optional): Time-to-live for cached session entries in seconds (default: `604800` — 7 days). The TTL is measured from the session's **last write**, not from when an entry was created: every write slides the session's expiry forward. Set to `0` to disable expiry — rows are then stored without an expiry, nothing is ever purged, and the sliding-TTL writes are skipped too, which is the lightest-I/O setting for long-lived agent sessions on the SQLite backend.

    `CACHE_SSL` and `CACHE_SSL_CERT_REQS` are forwarded to both the sync and async Redis clients as `ssl` / `ssl_cert_reqs`. Existing plaintext Redis deployments are unaffected because `CACHE_SSL` defaults to `false`.

    <Note>
      **Upgrading an existing SQL-cache deployment:** the `sqlite` and `postgres` backends now write session-context entries as an upsert keyed on `(user_id, session_id, entry_id)`, which requires a unique index on the `cache_session_context` table. Fresh databases get that index when the cache tables are created, but an existing one must be migrated: run [`cognee.run_migrations()`](/python-api/run-migrations) (or `alembic upgrade head`) to remove duplicate rows accumulated before the fix and create the index.
    </Note>

    **Conversation history window:**

    * Cognee includes up to the last 10 session entries when building LLM conversation history.
    * Each entry is a full question/answer turn — a single `SessionQAEntry` holding both the user's `question` and the generated `answer` (see [Session Data Structure](#session-data-structure) above). So the window covers up to 10 prior exchanges, not 10 individual messages.
    * This 10-entry window is **fixed and not configurable** — there is no environment variable for it. The configurable session settings are the cache toggle and backend (`CACHING`, `CACHE_BACKEND`, `CACHE_DB_URL`, `CACHE_HOST`, `CACHE_PORT`, `CACHE_USERNAME`, `CACHE_PASSWORD`), the Redis TLS options (`CACHE_SSL`, `CACHE_SSL_CERT_REQS`), the entry expiry (`SESSION_TTL_SECONDS`), and the abandonment threshold (`SESSION_ABANDON_AFTER_SECONDS`).

    Sessions expire automatically after `SESSION_TTL_SECONDS` when that value is greater than `0`. The clock runs from the session's last write — each write slides the expiry of the session's entries forward, so an actively used session does not expire underneath you. If you set `SESSION_TTL_SECONDS=0`, sessions persist until the cache is cleared — use `cognee.prune.prune_system(..., cache=True)`, or wipe your cache backend directly (e.g. Redis keys or the filesystem cache directory).

    On the SQL backends (`sqlite` and `postgres`), that slide is applied **lazily**: an entry is only re-stamped once its recorded expiry has fallen more than 5% of the TTL behind the current target. Entries therefore expire somewhere between 0.95 × `SESSION_TTL_SECONDS` and 1.0 × `SESSION_TTL_SECONDS` after the session's last write — with the 7-day default, up to about 8.4 hours before the full TTL. Treat the TTL as a lower bound with a small slack window rather than an exact deadline; if a session must survive a precise interval, size `SESSION_TTL_SECONDS` accordingly or set it to `0`. Per-user usage logs are re-stamped under the same rule. The Redis backend is unaffected and keeps exact `EXPIRE` semantics.

    <Note>
      **Operator note (SQL backends):** because entries are re-stamped at most once per slack window instead of on every write, a write costs roughly its own bytes rather than a rewrite of every row in the session. This removes the write amplification that could grow a SQLite `cache.db-wal` file to many times the size of `cache.db` under sustained agent traffic.
    </Note>

    **Graceful fallback behavior:**

    * If no cache backend is configured or the cache is unavailable, `cognee.session.get_session()` returns `[]`.
    * In the same situation, `cognee.session.add_feedback()` and `cognee.session.delete_feedback()` return `False`.
  </Accordion>

  <Accordion title="Session-context guidance (AUTO_FEEDBACK)">
    When `AUTO_FEEDBACK` is enabled (the default) and `CACHING` is on, session-capable completion searches run a lightweight analysis step under the resolved session (the dataset-scoped default session, `default_session_<dataset_id>`, when `session_id` is omitted). This step is on by default — existing session usage performs one additional structured-output LLM call per answered turn.

    On every turn, the analysis compares the current query against the previous turn's question, answer, and the context that was served for it, and accumulates durable, per-session **guidance** grouped into `goals`, `rules`, `preferences`, and `lessons_learned`, which can be injected into later answers in the same session.

    **When** that analysis runs is set by `SESSION_SEARCH_MODE`, and the mode decides what else the analysis is allowed to do:

    **`concurrent` (default)** — the analysis runs alongside retrieval and answer generation, so an answered turn costs roughly one LLM call of wall-clock time rather than two calls in a row. Its guidance is applied after the answer is produced, so it shapes the *next* turn in the session rather than the current one. In this mode the analysis' routing outputs are ignored: it can neither substitute an effective query nor gate the turn, so every turn is retrieved and answered. The analysis is bounded by a 30-second timeout and falls back to no context updates if it exceeds it.

    Because retrieval cannot wait for a rewritten query in this mode, a concurrent turn retrieves **twice** — once with the raw question, and once with a deterministic, LLM-free rewrite that prefixes the question with up to the last two question/answer turns of the session (capped at 2000 characters). The retriever merges the two result sets into one under its usual `top_k` budget: items found by both lanes rank first, then the raw question's remaining items, and roughly a third of the budget is reserved for items only the rewrite found (nothing is reserved when the limit is 2 or less). The merged total never exceeds `top_k`. If one lane fails, the surviving lane's results are used as-is.

    **`sequential`** — the analysis runs **before retrieval**, so its outputs can take effect on the same turn. It may derive an **effective query** used for retrieval and answer generation instead of the raw query (for example, resolving a terse follow-up into a self-contained question), and it may **gate** the turn: when the analysis determines the turn does not require retrieval, the search returns a short acknowledgement (the analysis-provided reply, or `"Got it."`) instead of running retrieval and completion. Retrieval runs once, with the effective query.

    **Automatic fallback to sequential.** Concurrent mode applies only to `search()` / `recall()` calls whose resolved retriever is exactly `GraphCompletionRetriever` (`GRAPH_COMPLETION`), `HybridRetriever` (`HYBRID_COMPLETION`), `CompletionRetriever` (`RAG_COMPLETION`), or `TripletRetriever` (`TRIPLET_COMPLETION`). The match is by exact class, so subclass-based search types — `GRAPH_COMPLETION_COT`, `GRAPH_COMPLETION_CONTEXT_EXTENSION`, `GRAPH_SUMMARY_COMPLETION`, `TEMPORAL`, `AGENTIC_COMPLETION` — do not qualify. These cases run sequentially with no configuration change on your part:

    * Any retriever outside the four exact classes above.
    * `only_context=True` (which skips the analysis entirely, in either mode).
    * Batch queries.
    * `FEELING_LUCKY`, whose retriever is only resolved after routing.
    * Calls with no session available for the user.

    The step **fails open** in both modes — if analysis errors or no session is available, the original query is answered normally. Because guidance (and, in sequential mode, the effective query) can change retrieval inputs, answers may differ from history-only sessions. To disable this behavior and keep only conversation-history replay, set `AUTO_FEEDBACK=false` — that removes the analysis call in both modes. It does not switch off concurrent mode's dual-query retrieval: eligible calls still retrieve with both the raw question and the deterministic rewrite and merge the results. Set `SESSION_SEARCH_MODE=sequential` as well to restore single-query retrieval.

    <Note>
      This adds one structured-output LLM call per answered turn and its token usage in both modes. In `concurrent` mode that call overlaps the answer, so it adds little to a turn's latency; in `sequential` mode it is serialized ahead of retrieval and its latency adds to the turn. Sessions still work without it; set `AUTO_FEEDBACK=false` to opt out.
    </Note>
  </Accordion>

  <Accordion title="Session distillation into long-term memory">
    Session-context guidance is short-term until it is bridged into the graph. When you run `cognee.improve(dataset=..., session_ids=[...])`, Cognee can distill gated guidance from those sessions into permanent lesson documents.

    Distillation:

    * Loads session Q\&A and active session-context entries.
    * Keeps only guidance that was never rated harmful and has enough confidence.
    * Curates proposed durable lessons, checks them against previously distilled lessons and graph entities, and rejects lessons that are already known, unsupported, or not durable.
    * Writes accepted lessons back into the dataset through `add()` + `cognify()`.
    * Tags distilled lessons with `session_learnings` and a session-specific node set.

    You can run this directly for one finished session:

    ```python theme={null}
    result = await cognee.session.distill_session(
        "my_session",
        dataset="my_dataset",
    )
    ```

    `result.documents` contains the rendered lesson documents when the status is `completed`. Empty output can be normal when the session has no gated entries or no accepted lessons.

    See [Session Distillation](/guides/session-distillation) for a full end-to-end example.
  </Accordion>

  <Accordion title="Adapter Comparison">
    | Feature          | SQLite (default)                 | Postgres                         | Redis             | Filesystem             | Tapes                                    |
    | ---------------- | -------------------------------- | -------------------------------- | ----------------- | ---------------------- | ---------------------------------------- |
    | Storage          | Local `cache.db` file (SQL)      | External SQL database            | In-memory (Redis) | Local disk (diskcache) | Local disk + mirrored ingest             |
    | Performance      | Fast (local I/O)                 | Fast                             | Very fast         | Fast (local I/O)       | Fast local writes + network mirror       |
    | Network required | ❌ No                             | ✅ Yes                            | ✅ Yes             | ❌ No                   | ⚠️ Only for mirroring to Tapes           |
    | Setup complexity | Low                              | Medium                           | Medium            | Low                    | Medium                                   |
    | Best for         | Default local setup, development | Production on existing SQL infra | Production        | Development, local     | Local session cache with Tapes ingestion |
  </Accordion>
</AccordionGroup>

<Note>
  Cached sessions can be persisted into the knowledge graph for long-term retrieval using [`improve()`](/core-concepts/main-operations/improve). The older [session persistence memify pipeline](/guides/memify-session-persistence) documents the legacy Q\&A persistence path.
</Note>

<Columns cols={3}>
  <Card title="Search" icon="search" href="/core-concepts/main-operations/legacy-operations/search">
    Learn how sessions integrate with search
  </Card>

  <Card title="Sessions Guide" icon="code" href="/guides/sessions">
    Practical examples with Redis and filesystem
  </Card>

  <Card title="Setup Configuration" icon="settings" href="/setup-configuration/overview">
    Configure cache adapters
  </Card>
</Columns>
