Skip to main content
The session, cache and rate-limit keys resolve as defaults < config.json < .env < env < code (precedence) and can be set from code with the config API or cognee-cli config. Logging, observability and telemetry are process-wide instead: each is resolved once per process, with its own precedence given in its section (two scopes).

Session / cache & rate limiting

The session (Q&A history) store is chosen by the session section, registered by the session-store kind. Stores are SessionStoreFactorys on the ComponentRegistry, selected by session.store; an external crate adds one with register_session_store. A factory is handed the shared relational database. Built in:
  • relational, also registered as seaorm, sqlite and postgres: the session tables in the relational database (any RelationalDb). This is the default when neither session.store nor CACHE_BACKEND selects another store.
  • fs (crate feature session-fs): JSON files under session.fs.directory.
  • redis (crate feature session-redis): Python’s Redis layout, at session.redis.*.
A store compiled out by its feature brings no keys: in a default build the CACHE_HOST … variables are not read, and setting cache_host is an unknown key. An unregistered store id is an error while session.caching is on; with CACHING=false no store id is resolved.
Behaviour change for cognee-cli and the bindings. cognee-cli (search/recall/remember/improve/bench) and the Python/TS/Java/C bindings used to build the relational store unconditionally and ignore COGNEE_SESSION_STORE. They now honour session.store like every other entry point — but they ship only the relational store (neither session-fs nor session-redis is enabled in their builds). So fs or redis, values the HTTP server accepts, are now an error there (“unsupported session_store …”). If such a process shares the server’s environment, unset COGNEE_SESSION_STORE or set it to relational for it.
Python’s session-cache settings. Python selects its session cache with CACHE_BACKEND (redis, fs, sqlite, postgres, turso, tapes; default sqlite). Here it binds session.cache_backend, a fallback for session.store (COGNEE_SESSION_STORE): it is used only while session.store is empty, and an explicit session.store always wins. sqlite and postgres select the relational store (Rust keeps sessions in the relational database, not a separate cache.db); fs and redis need their build features, and turso / tapes are not built in. Unlike session.store, a CACHE_BACKEND value naming a store this build does not provide is not an error: it is logged as a warning and the relational store is used, so a .env shared with Python (which documents CACHE_BACKEND=redis next to CACHE_HOST) cannot take down a stock cognee-cli or binding, none of which ship the fs or redis store. To insist on a store and fail when it is missing, set session.store (COGNEE_SESSION_STORE) instead. Both unset, the store is relational. session.ttl_seconds expires a session’s entries, trace steps and graph-context snapshot, each that long after its own last write, as Python does. The redis store sets a key expiry; the fs store deletes an expired file the next time it is accessed (read or written). The relational store does not expire sessions: Python’s SQL cache keeps an expiry per row, which the Rust session tables have no column for. A negative SESSION_TTL_SECONDS is accepted and, like 0, never expires a session, as in Python, whose cache adapters set an expiry only for a value > 0. session.caching = false (CACHING=false) switches session memory off, as in Python: whatever the store, session reads are empty, a search answers without session history and stores nothing, and auto_feedback has no effect. remember(data, session_id=…) still succeeds and only logs “session cache not available”, as Python’s _add_to_session does; a typed entry (remember_entry: Q&A, trace or feedback) fails with “session memory is disabled” before doing any work (no session_records upsert, no feedback LLM call), as Python’s _dispatch_session_entry raises “Session cache unavailable”.
cognee-cli migrates a saved config file written before these settings took effect: the old, never-read cache_backend: "fs" default and the old ./target/models/... ONNX path defaults are dropped, so an upgraded CLI keeps the relational store and places the model under cache_root_directory. An explicitly chosen cache_backend other than fs (for example redis) is kept; an explicit fs cannot be told apart from the old default and is dropped too.

How retries and pacing interact

These knobs form one resilience stack, matching Python cognee’s:
  • Two retry counts, one per loop. LLM_MAX_RETRIES (Python’s _MAX_VALIDATION_RETRIES) is the number of attempts at one structured-output call — the first ask plus corrective re-asks, so the default 3 is at most two re-asks. Every adapter floors it at 1. LLM_NETWORK_RETRIES (Python’s stop_after_attempt(2)) is the transport ladder underneath, for 429s, 5xx and connection failures. They are separate because they nest; one shared value multiplied the ladders into each other.
  • Retrying stops only once BOTH floors are met — at least LLM_NETWORK_RETRIES attempts and at least LLM_MIN_RETRY_SECONDS elapsed. The time floor lets a call outlast a provider’s rate-limit window. Backoff is exponential with jitter, 8s doubling to a 128s ceiling, and a Retry-After hint of up to 60s is honoured; a longer hint is ignored and the normal backoff applies. Set LLM_MIN_RETRY_SECONDS=0 to fail fast on attempts alone. Time spent queued for an LLM_MAX_PARALLEL_REQUESTS slot does not count against the floor.
  • Bedrock makes one attempt more. Its attempt floor is LLM_NETWORK_RETRIES + 1 (so 2 buys three attempts there), and the time floor applies to it like everywhere else. Bedrock ignores Retry-After, so its gaps are always the 8s-to-128s ladder.
  • Three timeouts, three scopes. LLM_CONNECT_TIMEOUT_SECONDS bounds the TCP handshake; LLM_REQUEST_TIMEOUT_SECONDS bounds one HTTP request (0 means “no limit” for both, matching curl). Both apply to the OpenAI-compatible (including Azure), Anthropic and Bedrock adapters; the Responses client uses a fixed 600s request / 10s connect pair. LLM_REQUEST_DEADLINE_SECONDS bounds one logical structured-extraction call and is the only bound on total time. Keep it above LLM_MAX_RETRIES x LLM_MIN_RETRY_SECONDS (720s at the defaults) and above 900s: the deadline includes time spent paced and queued, and a provider 429/503 opens a 900-second pacing episode. The 1200 default clears both; a warning is logged at startup if the deadline does not fit the computed ladder. The deadline gates starting new work rather than cancelling in flight, so the true ceiling is LLM_REQUEST_DEADLINE_SECONDS + LLM_REQUEST_TIMEOUT_SECONDS; retries’ pacing and slot waits are bounded by the remaining budget, while the first attempt may wait out a whole episode. Set it to 0 to disable.
  • The cascade stops sending a mode that never produces anything. On an endpoint without a server-side tool parser (e.g. vLLM started without --enable-auto-tool-choice --tool-call-parser), tools and functions can never produce a payload. The OpenAI-compatible adapter tracks, per endpoint and per mode, three consecutive structured calls that exhausted their attempts with no native payload, then skips that mode, re-probing once every 64 calls so a redeploy recovers without a restart. Errored requests (4xx/5xx, resets, deadline aborts) and truncated responses do not count against a mode, and any answered call clears its count. JSON mode is never skipped. There is no setting; transitions are logged at warn.
  • Concurrency and rate are separate ceilings. LLM_MAX_PARALLEL_REQUESTS bounds how many LLM requests are in flight at once (enforced inside the LLM transport, so it applies on every surface); the LLM_RATE_LIMIT_* bucket bounds how often they start. A slot is held for the HTTP exchange itself and released while a request waits for pacing or a retry backoff. It defaults to 1000 (matching the connection-pool limit openai-python and litellm set), and 128 on Android/iOS.
  • Pacing is off until it is needed. With LLM_RATE_LIMIT_ENABLED=false (the default) requests dispatch unbounded up to the concurrency ceiling. When the provider reports overload — HTTP 429, 503, 529, or a timeout — AUTO_RATE_LIMIT (default on) opens an episode: for the next 15 minutes dispatch is paced at LLM_RATE_LIMIT_REQUESTS per LLM_RATE_LIMIT_INTERVAL seconds. Fresh overload extends it; it lapses on quiet. Set LLM_RATE_LIMIT_ENABLED=true to pace from the start, or AUTO_RATE_LIMIT=false to never pace automatically.
  • Some failures are never retried, because no wait can fix them: bad request, authentication, unknown model, payment required, and a 429 whose body reports exhausted quota or billing rather than a per-minute limit.
Embedding requests have their own EMBEDDING_RATE_LIMIT_* bucket, which is flag-gated only — provider overload does not switch it on automatically.

Logging

Canonical table. Binding READMEs and .env.example link here. cognee writes structured logs to stdout and (when writable) to a rotating file. File logging is owned by cognee-logging: the CLI installs it at startup, the language bindings when you call their setup_logging entry point. Exception — machine-readable CLI output. cognee-cli sends console logs to stderr for any invocation whose stdout is a contract with a program: search/recall with -f json or -f simple, and export, visualize and bench, which print a path or a JSON document. Human-facing pretty output, and every other subcommand, still log to stdout.
Logging configures a process-wide singleton (the tracing subscriber), so it is process config: one LoggingConfig, resolved once, at startup. Its only window onto the environment is LoggingConfig::from_env_source (the from_env() boundary). Every field is also settable from code: assign it, or use LoggingConfig::set(key, value) with the option keys below. Precedence: defaults < environment < options from code.
  • CLI. The CLI loads Settings before it installs logging, so the two Settings keys log_level and logs_root_directory (default empty: use the logging default) are lowered into the logging config. Each fills only what the logging environment left unset: RUST_LOG > LOG_LEVEL > the config file’s log_level > the default filter, and COGNEE_LOGS_DIR > the config file’s logs_root_directory > ~/.cognee/logs. So cognee-cli config set log_level debug persists a level.
  • Bindings. A binding installs logging from a module-level function that runs before any handle exists, so a handle’s Settings (and config.set("log_level", …)) do not affect logging there. Pass the options to the entry point instead; they win over the environment. The first call wins, later calls are no-ops.
Option keys are snake_case; the bindings also accept camelCase (logLevel). An unknown key or an invalid value is an error (Python ValueError, a JS exception, CogneeException with VALIDATION_ERROR, C return code 2 with cg_last_error_message()). The default bridge subscriber a binding installs on import (into Python logging, or stderr) reads its level filter through the same LoggingConfig boundary (RUST_LOG > LOG_LEVEL > default).
Multi-process warning — when several cognee processes share one log file via LOG_FILE_NAME, rotation is not coordinated; concurrent rotation can corrupt the log. For sharded workers, give each shard its own COGNEE_LOGS_DIR (or unset LOG_FILE_NAME per shard).

Observability & telemetry

cognee emits OpenTelemetry traces (behind the telemetry feature) and opt-out product analytics. The deep references are observability/opentelemetry.md and observability/send_telemetry.md.

OpenTelemetry traces

Precedence: defaults < environment < code. The exporter is process-wide too, but where it reads its settings depends on who initialises it:
  • CLI — from Settings (defaults < config file < env < --flags), so cognee-cli config set otel_exporter_otlp_endpoint … works.
  • Rust embedders — cognee::telemetry::init_telemetry(&settings) takes any SettingsView: a Settings, or an EnvSettingsView (env only, plus EnvSettingsView::set(key, value) from code).
  • Bindings — from a module-level function that runs before any handle exists, so a handle’s Settings do not apply. The binding’s default service name (cognee.python-binding, cognee.node-binding, cognee.java-binding, cognee.capi-binding) is a default: OTEL_SERVICE_NAME and an explicit otel_service_name option both win over it, and the process environment is never modified. Options use the Settings key names above (camelCase also accepted):

Product analytics

Product analytics are on by default (opt-out, matching the Python SDK). The switch is process-wide: one TelemetryConfig governs every event the process sends, whichever handle emits it. It is not a Settings key and not a cognee-cli config key. Precedence: defaults (on) < environment < code. The environment is read when an event is sent, so a .env loaded when the first handle is built, or a variable set later, is honoured. An explicit call wins over the environment in both directions and stays in force until the next one: set_enabled pins only the switch, configure pins every field, reset_to_env returns to the environment. Disabling also drops events that are queued but not yet sent. The existing setup_telemetry_analytics() / setupTelemetryAnalytics() / Cognee.initTelemetry() / cognee_init_telemetry() arm the binding (which importing it already does) and report the same state as the query functions. Building without the telemetry cargo feature compiles analytics out entirely; the switch still exists and is_enabled() is always false. The api_key_tracking_id in each event is a salted hash of the resolved LLM key (llm.api_key, from env, a config file or code). Every ComponentManager holds a cognee::telemetry::LlmKeyRegistration for its key, updated when its configuration changes and dropped with the manager; embedders without a manager call cognee::telemetry::set_llm_api_key. The LLM_API_KEY variable is not read by the analytics client itself. When every live registration uses the same key, its hash is reported; when live handles use different keys, process-wide events cannot be attributed to one of them and the id is reported empty rather than mislabelled.
Divergence from Python: Python hashes os.getenv("LLM_API_KEY") only. Rust hashes the key it actually uses, which can also come from its OPENAI_TOKEN alias, a config file or code. The derivation is identical (PBKDF2-HMAC-SHA256, same salt), so the same key gives the same id in both SDKs, which is what cross-SDK grouping needs; a Rust process configured only through OPENAI_TOKEN reports an id where a Python process without LLM_API_KEY (and so without a working LLM) reports none.

HTTP server

The server binary is not part of the cognee-rs repository. It reads its own env surface (host/port, auth, body limits, pipeline registry, notebooks, health probes), which is documented with the server rather than here; see HTTP Server Tools.

Cloud

Cloud/Auth0 configuration (COGNEE_CLOUD_URL, COGNEE_AUTH0_*) and the serve()/disconnect() flow are not part of OSS.