Overview
Experio stores the knowledge graph (ontology instances, document relationships, embeddings on graph nodes) against either Neo4j or FalkorDB. Exactly one backend is active per deployment. The application routes reads and writes through a shared abstraction layer so ingestion, chat retrieval, and admin tools stay consistent.Choosing the active backend
SetGRAPH_PROVIDER to neo4j or falkordb in Admin > Settings > System Settings (DB category). Connection
fields for the inactive provider remain stored but are ignored until you switch.
See the DB settings reference for all keys. After changing provider or
connection details, validate connectivity using the dashboard flow (including the reachability probe surfaced when
switching providers).
Graph migration (staff)
Staff can copy data between backends from Admin > Settings > Graph migration:- Neo4j → FalkorDB and FalkorDB → Neo4j jobs stream progress over a WebSocket.
- Use prerequisites on that page (backup, connectivity) before starting a long run.
Behavioral differences
Node ids on FalkorDB
On FalkorDB a node’s id is the whole number thatid(n) returns. Experio passes it around as text (for example the element_id of the lineage API) and sends it back to FalkorDB as a number when it looks a node up by id. If you read or write node ids, three things matter:
- Compare
id(n)with a number. On FalkorDB 6.0.3,WHERE id(n) = '12345'fails withNode ID must be an integer. FalkorDB 4.22.1 returned no rows for the same query and said nothing, so a query with a text id that looked harmless there fails now. Writeid(n) = 12345, or compare text withtoString(id(n)) = '12345'. Neo4j element ids are unchanged: they stay text. - 0 is a real node. A graph starts at node 0, and FalkorDB 6.x hands out the lowest free id first, so after a delete the next node can take 0 again. Only a missing or blank id means “no node”; never read 0 as missing.
- An id names a node only while the node exists. FalkorDB reuses the ids of deleted nodes (6.x takes the lowest free id, 4.22.1 took the one freed most recently), so an id you noted earlier can later name a different node. Do not keep a node id as a long-term key outside Experio; use a property the node carries, such as its name.
FalkorDB resilience & query limits
- Per-query memory cap — FalkorDB is started with
QUERY_MEM_CAPACITYset to 512 MiB by default (earlier releases used 1 GiB). A single query that exceeds the cap is terminated by FalkorDB withQuery's mem consumption exceeded capacityinstead of pushing the container toward its memory limit (and an OOM restart). If a legitimate analytical query hits the cap, narrow the query; the cap is the FalkorDB Query Memory Cap deployment parameter, which reaches FalkorDB throughFALKORDB_ARGS(see FalkorDB deployment parameters). - Per-query runtime cap —
TIMEOUT_DEFAULTis set to 60 seconds. FalkorDB’s default is0(unlimited). Queries that omit an explicitTIMEOUT(including an unbounded FalkorDB BrowserMATCH (n) RETURN n) are killed at 60s instead of holding the graph lock indefinitely while RedisPINGstill succeeds. 60s matches the Experio clientsocket_timeout. Do not set the deprecatedTIMEOUTalongsideTIMEOUT_DEFAULT— the combination aborts module load.TIMEOUT_MAXis left at0so a client may pass a longer per-queryTIMEOUTwhen needed. - Interactive read limit — a read that a person is waiting for (a chat answer, an
agent flow tool, a dashboard, the graph evidence shown with an answer) carries a shorter
limit of its own,
GRAPH_READ_TIMEOUT_MS(15 seconds by default), and FalkorDB answersQuery timed outwhen it is passed. Ingestion and other background work do not carry it and keep the 60s cap above. A read stopped this way is reported as the knowledge graph being unavailable, not as “no data”. See Graph Read Time Limit. - Fail-fast on outages — graph calls use a 60s read timeout. While FalkorDB is
restarting (it reloads its dataset into memory and answers
LOADINGduring that window), chat and ingestion graph queries fail quickly with a provider-unavailable error instead of hanging. Expect/health/to reportstatus: degradeduntil the reload completes, then queries recover automatically — no server restart is needed. A chat answer that meets such a failure says the knowledge graph could not be queried, instead of reporting no data.
FalkorDB read indexes
On FalkorDB, optional indexes onDocument(file_id) and on name (one for every node type
that has a name) speed up ingestion’s document lookups and the matching of entity names. The
gain grows with the size of the graph. They are never built automatically: building one
waits for every read in flight on the graph, and the whole graph server waits with it.
Build them on purpose, in a maintenance window with ingestion paused and chat quiet, with
Force ensure on the Read performance indexes check or with
python manage.py ensure_read_indexes --force. On a new instance, build them before the first
ingestion, while the graph is empty and the build takes moments. The procedure, the output to
expect and the rollback note are in
Startup Health. To see
which of them are built, building or missing, use View indexes on that row
(how). Neo4j does not use these indexes.
Omnistrate / cluster deployment
Both Neo4j and FalkorDB run as internal cluster services with manual scale-to-zero. They are independent of platform boot — server, workers, andinitdb no longer wait for a graph DB to be healthy.
Default state
Seeded connection defaults (via
initdb → seed_config):
FALKOR_URI:redis://falkordb:6379(cluster internal DNS)NEO4J_URI:neo4j://neo4j:7687
neo4jPassword and falkordbPassword.
See Omnistrate deployment.
Controlling graph DB replicas
Use Django admin ServiceConfiguration (same UI as JupyterLab scale-to-zero):keep_alive— maintain 1 replica (use for the active provider)keep_down— maintain 0 replicas (use for the inactive provider to save cost)auto— manual strategy defaults to 1 replica; prefer explicit modes above
FalkorDB deployment parameters
These are parameters of the Omnistrate deployment. Set them when you create or update the instance.
The FalkorDB image is pinned to release 6.0.3 by its tag,
falkordb/falkordb:6.0.3, and that tag is the only image the deployment accepts. The deployment names the tag and not a digest because Omnistrate does not keep a digest-only image reference. The image is never the floating latest tag: FalkorDB publishes two engines, a 4.x line and a 6.x line, and latest follows the newest release of either, so on 28 October 2026 it moved from 6.0.3 to 4.22.3. Local development pins the same release by its tag and its digest. Moving to another 6.x release is a deliberate change to the deployment. There is no way back to 4.x once 6.0.3 has saved a graph: read “Upgrading an existing instance to FalkorDB 6.0.3” and “There is no rollback to FalkorDB 4.x” below first.
The last five parameters in the table (FalkorDB Query Threads, FalkorDB Query Memory Cap, FalkorDB Max Queued Queries, FalkorDB Output Buffer Limit and FalkorDB Max Memory) are the FalkorDB limit parameters. The container passes them to FalkorDB through FALKORDB_ARGS, so an instance applies them at its next upgrade. Their defaults are sized for the default FalkorDB memory limit of 3 GiB. Earlier releases ran with a 1 GiB query memory cap, no queue limit of their own (FalkorDB’s default applied) and no output buffer limit, so the upgrade lowers those three limits on an instance that does not set its own values.
Upgrading an existing instance to FalkorDB 6.0.3
Earlier releases ran the floatinglatest image, so an instance ran whichever FalkorDB release that tag pointed to when its FalkorDB pod last started: a 4.22 release or a 6.0 release. This release pins FalkorDB 6.0.3, a rewrite of the engine in Rust, by its release tag. An instance moves to it at its next upgrade, and no parameter has to be set.
- Take the backup first, and prefer a snapshot of the FalkorDB volume. A snapshot is taken from outside the FalkorDB process, so it does not fork FalkorDB and needs no extra FalkorDB memory. Pause ingestion and every other writer first. A snapshot holds the data file as FalkorDB last saved it, so before you take it check that
INFO persistence(run withredis-cliand the FalkorDB password) showsrdb_changes_since_last_save:0, which means that nothing is waiting to be saved. If it shows more than 0, keep the writers paused until FalkorDB’s next scheduled save has run and it shows 0. - If you copy the data with
redis-cli --rdb <file>and the FalkorDB password instead, pause ingestion and every other writer first, and make sure the FalkorDB node has free memory. The copy forks the FalkorDB process, and the extra memory the fork needs grows with the writes made while it runs, so a node that is already close to its memory limit (3 GiB on the default node) can run out of memory. Keep the copy off the FalkorDB volume, because FalkorDB 6.0.3 overwritesdump.rdbon its save schedule and when it shuts down. - Your graphs come back. On its first start FalkorDB 6.0.3 loads the data file that 4.22.1 wrote. In our tests a graph of about 354,000 nodes and 436,000 edges came back with the same node and edge content and all 17 indexes operational. It loaded in 4 to 5 seconds, against 15 to 18 seconds on 4.22.1. Do not delete the FalkorDB volume.
- Expect about a third more FalkorDB memory. On that graph FalkorDB used 1,067 MiB after loading, against 803 MiB on 4.22.1 (33 percent more), and the container used 1,076 MiB against 738 MiB. The peak while loading, which includes the page cache of the data file, was 1,293 MiB against 928 MiB (39 percent more). The worst case we measured, two queries at the 512 MiB query memory cap plus a save, used 2,013 MiB of the 3,072 MiB limit (66 percent), with about 1 GiB free. Multiply the memory your FalkorDB uses today by about 1.4 and compare the result with the memory limit of the FalkorDB node, which is 3 GiB on the default node. By a linear estimate, not a measurement, the largest graph that fits the default node shrinks from about 2.8 times this one to about 1.75 times. If the graph no longer fits, raise the instance type of the medium tier in the same upgrade: the FalkorDB node follows it, and so do PostgreSQL, Neo4j and RabbitMQ, which share that tier.
- Expect about 37 percent more CPU per query. With 80 simulated users at the same request rate, FalkorDB used about 1.06 of the 2 cores of the default node, against 0.84. The median query latency was 1.8 to 1.9 times that of 4.22.1 and the 95th percentile 33 to 37 percent higher, and throughput stayed at 97 to 98 percent of it.
- Similarity scores move in the last digits. Vector search returned the same nearest neighbours in our tests. The scores differ from 4.22.1 in the sixth decimal place.
- Keep the container user. The FalkorDB container runs as user
1001:1001on the volume the platform provides. Do not start it as root: that start hands the data directory to another user, and a container that runs as1001:1001cannot open the directory afterwards.
There is no rollback to FalkorDB 4.x
In that state the node data, the node counts, the indexes and vector search still look right, and so does the plain countMATCH ()-[r]->() RETURN count(r), because FalkorDB answers it from its statistics. The same happens when a 6.0.3 graph is copied into a 4.x engine with DUMP and RESTORE. Treat every graph that 6.0.3 has saved as affected. A 4.x engine in this state cannot take a live backup either: redis-cli --rdb fails.
If a 4.x engine was started on a graph that 6.0.3 had saved, stop everything that writes to the graph and put the image back to the pinned release 6.0.3. In our tests every edge came back, on 4.22.3 as well as on 4.22.1, from every data file the 4.x engine left behind. Do not start a 4.x engine on that volume again: it stays blind.
The only way back to 4.x is a restore, not an image change:
- Stop everything that writes to the graph, for example by scaling the job workers to zero.
- Restore the backup that 4.22.1 wrote before 6.0.3 first started into the FalkorDB volume while FalkorDB is stopped.
- Put the FalkorDB image back to release 4.22.1 and add
CACHE_SIZE 1to the FalkorDB arguments, because 4.22.1 can crash when it evicts a cached query plan at the default size. This is a change to the deployment itself, not a parameter. - Ingest again the files that were ingested after the backup was taken.
MATCH (a)-[r]->(b) RETURN max(id(r))must return a number, not null, when the graph has edges.- A join count that you recorded before the change must return the same value afterwards, for example
MATCH (a:Label)-[:TYPE]->(b:Label) RETURN count(b)with a label and relationship type of your graph.
count(r) on its own for this check. FalkorDB answers it from its statistics, so it stays right while traversals are broken. Do not use count(*) for it either: over parallel edges it can count fewer rows on 6.x.
Migration window (both providers up)
Graph migration requires both backends reachable:- Set
neo4jandfalkordbtokeep_alivein ServiceConfiguration - Wait until both pass connectivity checks (Admin → System Settings → provider probe,
or inspect
/health/— graph may showchecks.neo4junhealthy until warm) - Run migration from Admin → Settings → Graph migration
- Switch
GRAPH_PROVIDERin System Settings - Set the inactive backend to
keep_down
Health monitoring
GET /health/ reports the active graph provider under checks.neo4j (legacy key
name; includes provider: neo4j | falkordb). Graph downtime does not fail the
whole endpoint:
status: degraded— platform OK, graph unavailable (expected during scale-to-zero)status: unhealthy— Postgres, Redis, or RabbitMQ failed
degraded.
GET /health/ no longer calls the LLM: checks.llm is always skipped, so a model-provider
outage cannot make the endpoint answer 503. The probe moved to GET /health/llm/, which only
staff users may call. See Startup Health
for the three endpoints.
Schema cache
On server startup a background thread pre-warms the graph schema cache in Redis (24h TTL). If the graph is down at boot, warming fails softly and the first chat query may be slower until cache is populated. Withfalkordb on keep_alive, this
is rarely an issue in steady state.
The cache includes relationship fan-out statistics (fan_out_risk, max distinct
targets per source, max distinct sources per target) computed from live graph data.
After upgrading or switching graph providers, refresh the schema cache so fan-out
stats and aggregated property types reflect the active backend.
For how relationship attributes interact with graph introspection, see
Relationship attributes.