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 thesession 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 asseaorm,sqliteandpostgres: the session tables in the relational database (anyRelationalDb). This is the default when neithersession.storenorCACHE_BACKENDselects another store.fs(crate featuresession-fs): JSON files undersession.fs.directory.redis(crate featuresession-redis): Python’s Redis layout, atsession.redis.*.
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.
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 default3is at most two re-asks. Every adapter floors it at1.LLM_NETWORK_RETRIES(Python’sstop_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_RETRIESattempts and at leastLLM_MIN_RETRY_SECONDSelapsed. 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 aRetry-Afterhint of up to 60s is honoured; a longer hint is ignored and the normal backoff applies. SetLLM_MIN_RETRY_SECONDS=0to fail fast on attempts alone. Time spent queued for anLLM_MAX_PARALLEL_REQUESTSslot does not count against the floor. - Bedrock makes one attempt more. Its attempt floor is
LLM_NETWORK_RETRIES + 1(so2buys three attempts there), and the time floor applies to it like everywhere else. Bedrock ignoresRetry-After, so its gaps are always the 8s-to-128s ladder. - Three timeouts, three scopes.
LLM_CONNECT_TIMEOUT_SECONDSbounds the TCP handshake;LLM_REQUEST_TIMEOUT_SECONDSbounds one HTTP request (0means “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_SECONDSbounds one logical structured-extraction call and is the only bound on total time. Keep it aboveLLM_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. The1200default 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 isLLM_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 to0to 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),toolsandfunctionscan 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 atwarn. - Concurrency and rate are separate ceilings.
LLM_MAX_PARALLEL_REQUESTSbounds how many LLM requests are in flight at once (enforced inside the LLM transport, so it applies on every surface); theLLM_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 limitopenai-pythonandlitellmset), 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 atLLM_RATE_LIMIT_REQUESTSperLLM_RATE_LIMIT_INTERVALseconds. Fresh overload extends it; it lapses on quiet. SetLLM_RATE_LIMIT_ENABLED=trueto pace from the start, orAUTO_RATE_LIMIT=falseto 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_RATE_LIMIT_* bucket, which is
flag-gated only — provider overload does not switch it on automatically.
Logging
Canonical table. Binding READMEs andLogging configures a process-wide singleton (the.env.examplelink here. cognee writes structured logs to stdout and (when writable) to a rotating file. File logging is owned bycognee-logging: the CLI installs it at startup, the language bindings when you call theirsetup_loggingentry point. Exception — machine-readable CLI output.cognee-clisends console logs to stderr for any invocation whose stdout is a contract with a program:search/recallwith-f jsonor-f simple, andexport,visualizeandbench, which print a path or a JSON document. Human-facingprettyoutput, and every other subcommand, still log to stdout.
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
Settingsbefore it installs logging, so the twoSettingskeyslog_levelandlogs_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’slog_level> the default filter, andCOGNEE_LOGS_DIR> the config file’slogs_root_directory>~/.cognee/logs. Socognee-cli config set log_level debugpersists a level. - Bindings. A binding installs logging from a module-level function that runs
before any handle exists, so a handle’s
Settings(andconfig.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 viaLOG_FILE_NAME, rotation is not coordinated; concurrent rotation can corrupt the log. For sharded workers, give each shard its ownCOGNEE_LOGS_DIR(or unsetLOG_FILE_NAMEper shard).
Observability & telemetry
cognee emits OpenTelemetry traces (behind thetelemetry 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), socognee-cli config set otel_exporter_otlp_endpoint …works. - Rust embedders —
cognee::telemetry::init_telemetry(&settings)takes anySettingsView: aSettings, or anEnvSettingsView(env only, plusEnvSettingsView::set(key, value)from code). - Bindings — from a module-level function that runs before any handle exists,
so a handle’s
Settingsdo 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_NAMEand an explicitotel_service_nameoption both win over it, and the process environment is never modified. Options use theSettingskey names above (camelCase also accepted):
Product analytics
Product analytics are on by default (opt-out, matching the Python SDK). The switch is process-wide: oneTelemetryConfig 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 thecognee-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.