Expand description
The official Rust client for FalkorDB — a fast, low-latency
graph database. One ergonomic API across a blocking client and an async (tokio) client, with
typed results, parameter binding, batching, an embedded server, TLS, automatic retries, and
OpenTelemetry-aligned tracing and metrics.
§Highlights
- Sync and async — a blocking client and a
tokioasync client share the same ergonomic API. - Header-aware, typed results — read columns by name or index, or map rows straight into your own
serdetypes. - Safe parameters — bind Rust values as Cypher literals; no hand-quoting, no injection.
- Batching and pipelining — send many queries in a single round-trip.
- Async streaming — result sets are
Streams that compose with the fullfuturestoolbox. - Resilient — opt-in
RetryPolicywith bounded backoff for transient failures; writes are never retried. - Observable — OpenTelemetry-aligned
tracingspans andmetricscounters/histograms, privacy-safe by default. - Replica-aware — opt in to routing read-only queries to replicas behind Redis Sentinel.
- Embedded server — spin up a self-contained FalkorDB for tests and prototyping, with an optional build-time bundle mode that runs fully offline.
§Table of contents
- Highlights
- Quickstart
- Cargo feature flags
- Guide
- Examples
- API documentation
- Migration guides
- Contributing
- Community and license
§Quickstart
§Install
Install it with cargo add:
cargo add falkordb§Run a FalkorDB server
Docker:
docker run --rm -p 6379:6379 falkordb/falkordb§Your first query
use falkordb::{FalkorClientBuilder, FalkorConnectionInfo};
// Connect to FalkorDB
let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
.expect("Invalid connection info");
let client = FalkorClientBuilder::new()
.with_connection_info(connection_info)
.build()
.expect("Failed to build client");
// Select the social graph
let mut graph = client.select_graph("social");
// Create 100 nodes and return a handful
let mut nodes = graph.query("UNWIND range(1, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10")
.with_timeout(5000)
.execute()
.expect("Failed executing query");
// Each item is a `FalkorResult<Row>`; read columns by index or name.
while let Some(row) = nodes.data.next() {
let row = row.expect("row failed to parse");
println!("{:?}", row.get_at(0));
}§Cargo feature flags
All features are off by default — enable only what you need:
| Feature | Enables |
|---|---|
tokio | The async client and API on the tokio runtime (multi-threaded scheduler). |
serde | Map query results into your own serde::Deserialize types. |
tracing | OpenTelemetry-aligned tracing spans with a privacy-safe query fingerprint. |
metrics | Counters and histograms via the metrics facade (install any exporter). |
embedded | Run a self-contained embedded FalkorDB server (module downloaded at runtime). |
embedded-bundle | Embed the module at build time so the embedded server runs fully offline. |
rustls / native-tls | TLS for the sync client, via rustls or native-tls. |
tokio-rustls / tokio-native-tls | TLS for the async client. |
cargo add falkordb --features tokio,serde§Guide
Each capability below has a short explanation, a minimal snippet, and a link to a complete,
runnable example. The async client mirrors the sync API — await the terminals.
§Queries and results
§Header-aware result rows
QueryResult::data iterates the result set as FalkorResult<Row>. Each Row pairs the query
header (the column aliases) with that row’s values, so you read columns by name or index and a
row that fails to parse surfaces as an Err instead of being silently swallowed:
use falkordb::{FalkorClientBuilder, FalkorConnectionInfo};
let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
.expect("Invalid connection info");
let client = FalkorClientBuilder::new()
.with_connection_info(connection_info)
.build()
.expect("Failed to build client");
let mut graph = client.select_graph("imdb");
let mut result = graph
.query("MATCH (m:Movie) RETURN m.title AS title, m.year AS year")
.execute()
.expect("Failed executing query");
for row in result.data.by_ref() {
let row = row.expect("row failed to parse");
// Read a column by alias and convert it in one step (strictly, via `FromFalkorValue`).
let title: String = row.try_get("title").expect("title column");
let year: i64 = row.try_get("year").expect("year column");
println!("{title} ({year})");
}Row offers borrowing accessors (get, get_at, get_all), typed accessors
(try_get::<T>, try_get_at::<T>), and consuming conversions (into_values, into_map). Typed
access is strict — no silent lossy casts — via the FromFalkorValue conversion trait. Because
collect short-circuits on the first Err, a whole result set can be gathered with
result.data.collect::<falkordb::FalkorResult<Vec<_>>>().
FalkorDB rejects a query whose result columns are not uniquely named, so rows from a query always
have distinct columns; if a Row ever does hold duplicates, the access paths are still defined
(get/try_get return the first match, get_all returns every match, into_map keeps the last).
To opt back into the pre-0.7 behavior (bare Vec<FalkorValue> rows, parse errors collapsed to
FalkorValue::Unparseable), call result.data.into_values_lossy(). A runnable version lives in
examples/rows.rs. Upgrading from 0.6? See the
0.7 migration guide.
§Type-safe query parameters
Pass Rust values straight into a query — the client encodes them as Cypher literals and escapes them for you, so you never hand-quote strings or risk Cypher injection:
let res = graph
.query("MATCH (m:Movie {title: $title}) WHERE m.year IN $years RETURN m")
.with_param("title", "The Matrix")
.with_param("years", [1999, 2003])
.execute()?;Add several at once from an array, Vec, or map with with_params (the values share a single
type; use chained with_param calls, as above, for a mix of types):
.with_params([("min_year", 1990), ("max_year", 2000)])Supported value types include integers, floats, boolean values, strings, Option (encoded as
null), arrays/Vec, and string-keyed HashMap/BTreeMap (nested freely). Points and vectors
cannot be bound directly (a FalkorDB limitation) — pass the components and construct them in the
query:
use std::collections::BTreeMap;
let coords = BTreeMap::from([("latitude", 32.07), ("longitude", 34.79)]);
graph.query("RETURN point($p)").with_param("p", coords).execute()?;If you really need a raw Cypher expression, with_raw_param("key", "…") is the explicit escape
hatch — no escaping is applied to the value (the parameter name is still validated).
Temporal values returned by queries — datetime, date, time/localtime and duration —
decode into the typed DateTime, Date, Time and Duration values. Each exposes its scalar as a
typed Seconds (value.seconds()), and DateTime/Duration support a small type-safe algebra
(DateTime - DateTime → Duration, DateTime ± Duration → DateTime, plus Duration
add/subtract/negate) with overflow-checked checked_* variants. They are read from results but
cannot be bound back as parameters — build them in the query with the matching Cypher function (e.g.
date($s)). A runnable version lives in examples/temporal.rs.
§Typed result mapping with serde
Enable the optional serde feature to map query results straight into your own types instead of hand-matching every
FalkorValue variant:
cargo add falkordb --features serdeDerive serde::Deserialize on your type and call FalkorValue::deserialize_into (or the free function
falkordb::from_falkor_value) on a returned value. A node is deserialized from its properties, and scalars, Option,
sequences and maps map onto the matching Rust types:
use falkordb::{FalkorClientBuilder, FalkorConnectionInfo};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Movie {
title: String,
year: i64,
rating: Option<f64>,
}
let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
.expect("Invalid connection info");
let client = FalkorClientBuilder::new()
.with_connection_info(connection_info)
.build()
.expect("Failed to build client");
let mut graph = client.select_graph("imdb");
let mut result = graph.query("MATCH (m:Movie) RETURN m").execute()
.expect("Failed executing query");
for row in result.data.by_ref() {
let row = row.expect("row failed to parse");
if let Some(node) = row.into_iter().next() {
let movie: Movie = node.deserialize_into().expect("Failed to map node");
println!("{} ({})", movie.title, movie.year);
}
}A runnable version lives in examples/typed_mapping.rs.
To map a whole result set in one shot, call query_as::<T>() before execute(). Each row is
deserialized into a T, and the result’s data becomes an iterator of FalkorResult<T>, so it
collects directly into a Vec:
let movies: Vec<Movie> = graph
.query("MATCH (m:Movie) RETURN m")
.query_as::<Movie>()
.execute()
.expect("Failed executing query")
.data
.collect::<Result<_, _>>()
.expect("Failed mapping rows");A single-column row (such as RETURN m) is deserialized from that one column’s value, so a node
maps from its properties and RETURN count(m) maps a scalar. A multi-column row (such as
RETURN m.title AS title, m.year AS year) maps each column alias onto the matching struct field,
or yields the values in order for a tuple. The query header and stats remain available on the
returned result.
§Async
§tokio support
This client supports nonblocking API using the tokio runtime.
It can be enabled like so:
cargo add falkordb --features tokioCurrently, this API requires running within a
multi_threaded tokio scheduler, and
does not support the current_thread one, but this will probably be supported in the future.
The API uses an almost identical API, but the various functions need to be awaited:
use falkordb::{FalkorClientBuilder, FalkorConnectionInfo};
use futures::StreamExt; // brings `.next().await` onto the result stream
// Connect to FalkorDB
let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
.expect("Invalid connection info");
let client = FalkorClientBuilder::new_async()
.with_connection_info(connection_info)
.build()
.await
.expect("Failed to build client");
// Select the social graph
let mut graph = client.select_graph("social");
// Create 100 nodes and return a handful
let mut nodes = graph.query("UNWIND range(1, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10")
.with_timeout(5000)
.execute()
.await
.expect("Failed executing query");
// `nodes.data` is a `Stream<Item = FalkorResult<Row>>`; pull rows with `.next().await`:
while let Some(row) = nodes.data.next().await {
let row = row.expect("row failed to parse");
println!("{:?}", row.get_at(0));
}The result set (nodes.data) is an owned, Send + 'static Stream, so it can be moved into a
spawned task and driven with the full StreamExt / TryStreamExt toolbox. The graph
handle itself is Send + Clone: cloning is cheap and shares one schema cache, so to use a graph
from several concurrent tasks you just clone it — no Arc<Mutex<_>> wrapping required.
§Async streaming
Because results are a Stream, the standard combinators just work. Import the extension traits
(use futures::{StreamExt, TryStreamExt};) and:
use futures::{StreamExt, TryStreamExt};
// Collect a typed stream in one line (errors short-circuit):
let years: Vec<i64> = graph
.query("MATCH (m:Movie) RETURN m.year AS year ORDER BY year")
.execute()
.await?
.data
.map(|row| row?.try_get::<i64>("year"))
.try_collect()
.await?;
// Move a result stream into its own task (it is `Send + 'static`):
let mut stream = graph.query("MATCH (n) RETURN n").execute().await?.data;
let count = tokio::spawn(async move {
let mut n = 0usize;
while let Some(row) = stream.next().await {
row?;
n += 1;
}
Ok::<_, falkordb::FalkorDBError>(n)
})
.await
.unwrap()?;
// Fan out a follow-up query per row with bounded concurrency, over cloned handles:
let enriched: Vec<i64> = graph
.query("MATCH (m:Movie) RETURN m.year AS year")
.execute()
.await?
.data
.map(|row| {
let mut g = graph.clone(); // cheap; shares the schema cache
async move {
let year: i64 = row?.try_get("year")?;
let mut r = g.query(format!("RETURN {year} + 1 AS next")).execute().await?;
r.data.try_next().await?.expect("a row").try_get::<i64>("next")
}
})
.buffer_unordered(8)
.try_collect()
.await?;A runnable version lives in examples/async_stream.rs.
§Connection strategy and multiplexing
The asynchronous client chooses how it manages its underlying Redis connections via a
ConnectionStrategy:
Multiplexed(the async default): a small number of shared, cloneable, auto-reconnecting connections. Many concurrent commands are pipelined over each socket, so a single connection can carry many in-flight requests at once. This avoids the borrow/return bottleneck and is the most efficient option for highly concurrent workloads.Pooled: a fixed pool of independent connections, each used by exactly one command at a time (borrow/return). This gives strict per-command isolation and a natural cap on in-flight commands. It is the only strategy for the synchronous client.
Select or tune the strategy on the builder:
use falkordb::{ConnectionStrategy, FalkorClientBuilder};
use std::num::NonZeroU8;
// Spread commands across 4 shared multiplexed sockets (the default uses 8).
let client = FalkorClientBuilder::new_async()
.with_connection_strategy(ConnectionStrategy::Multiplexed {
connections: NonZeroU8::new(4).unwrap(),
})
// Optional backpressure: cap concurrently in-flight commands per socket.
.with_max_inflight(std::num::NonZeroUsize::new(256).unwrap())
.build()
.await
.expect("Failed to build client");
assert_eq!(client.connection_pool_size(), 4);Notes and caveats:
- Behavior change: the async default is now multiplexed (previously an exclusive
borrow-pool). The API is source-compatible;
with_num_connectionsnow sets the number of underlying connections/sockets for the active strategy, andconnection_pool_size()reports that count. - Backpressure: multiplexed mode does not bound the number of outstanding requests
unless you set
with_max_inflight(n)(wherenis aNonZeroUsize; ignored by the pooled strategy, whose pool size already caps in-flight commands). - Sentinel: a multiplexed connection built from a Sentinel-resolved node would not
re-resolve the master/replica on failover, so for Sentinel deployments the client
transparently falls back to the pooled strategy (which re-resolves on reconnect).
connection_strategy()returns this effective strategy.
A runnable example is provided in examples/multiplexed_async.rs.
§Execution patterns
§Batch and pipelined execution
Normally each query is one network round-trip. graph.batch() queues several queries and sends them
over a single Redis pipeline in one round-trip, returning one result per query in submission
order. Queue queries with query (a GRAPH.QUERY) / ro_query (a GRAPH.RO_QUERY) and set
per-query parameters on the returned handle:
let mut batch = graph.batch();
for movie in &movies {
batch.query("CREATE (:Movie {title: $t})").with_param("t", movie);
}
batch.ro_query("MATCH (m:Movie) RETURN count(m) AS n");
let results = batch.execute()?; // Vec<BatchItemResult>, one per query, in order
for (i, item) in results.into_iter().enumerate() {
match item {
Ok(result) => { /* result.data: Vec<Row>, result.header, result.stats */ }
Err(err) => eprintln!("query {i} failed: {err}"),
}
}On the async client it is identical but for the await:
let mut batch = graph.batch();
// … queue queries …
let results = batch.execute().await?;Key points:
- Per-item errors. A failing query (bad Cypher, or a parameter that can’t be encoded) becomes
that slot’s
Err; the other queries are unaffected. The outerResultonly fails if the whole batch could not be completed — and if that happens after the pipeline was sent, the server may have run some or all queries (the state is unknown), which matters for writes. - Not a transaction. A pipeline is not
MULTI/EXEC: every queued query is executed, so a failure in one does not roll back or stop the others. - Results are eager. Each query’s rows are parsed up front into a
Vec<Row>(the sameRowas elsewhere), since many result sets coexist in one batch. - Owned queries. To build queries ahead of time, construct
BatchQuery::write(..)/BatchQuery::read(..), attach params/timeout, andbatch.push(query).
A runnable version lives in examples/batch.rs.
§Waiting for background operations
Some FalkorDB operations finish after the command that starts them returns: when you create or
drop an index or constraint, the request returns immediately while the index is populated (or the
constraint is enforced) on a background worker thread, and GRAPH.COPY can fail transiently while
the server is unable to fork. The eager methods
(create_index, create_unique_constraint, copy_graph, …) stay fire-and-forget, but every
one of them now has an additive *_op builder that adds explicit, opt-in waiting while keeping
full backward compatibility.
Each builder offers .execute() (non-blocking, identical to the eager method) and .wait() /
.wait_with(WaitOptions) terminals. For index and constraint builders, .wait() blocks until the
operation has actually taken effect (the index/constraint becomes operational or is dropped),
returning FalkorDBError::Timeout if it does not happen in time. For the copy builder, GRAPH.COPY
is already blocking on the server, so .wait() simply retries transient could not fork failures
with backoff; it does not verify the copied contents (that remains the caller’s responsibility).
use falkordb::{EntityType, FalkorClientBuilder, FalkorConnectionInfo, IndexType, WaitOptions};
use std::time::Duration;
let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
.expect("Invalid connection info");
let client = FalkorClientBuilder::new()
.with_connection_info(connection_info)
.build()
.expect("Failed to build client");
let mut graph = client.select_graph("social");
// Fire-and-forget, exactly like `create_index` (returns as soon as the server accepts it):
graph.create_index_op(IndexType::Range, EntityType::Node, "Person", &["age"], None)
.execute()
.expect("Failed to request index creation");
// Block until the index is actually operational (default 30s readiness timeout):
graph.create_index_op(IndexType::Range, EntityType::Node, "Person", &["name"], None)
.wait()
.expect("Index did not become operational");
// A unique constraint reports a *distinct* error if existing data violates it:
match graph.create_unique_constraint_op(EntityType::Node, "Person", &["email"])
.wait_with(WaitOptions::with_timeout(Duration::from_secs(10)))
{
Ok(()) => println!("constraint is enforced"),
Err(falkordb::FalkorDBError::ConstraintFailed { .. }) => println!("data violates the constraint"),
Err(other) => panic!("unexpected error: {other}"),
}
// Copy a graph, retrying transient `could not fork` failures:
let _copy = client.copy_graph_op("social", "social_backup")
.wait()
.expect("Failed to copy graph");The same builders exist on the async client — just await the terminals. See
examples/waiting_ops.rs for a complete, runnable example.
For vector indexes, the typed helpers create_node_vector_index / create_edge_vector_index take a
dimension and a VectorSimilarity (Euclidean or Cosine) and generate the correct
OPTIONS { dimension: N, similarityFunction: '…' } clause for you. Like the other index operations
they are fire-and-forget, and they have matching create_node_vector_index_op /
create_edge_vector_index_op builders that integrate with the waiting ergonomics above —
.wait() blocks until the vector index is operational (and .execute() is the non-blocking
equivalent). A runnable version lives in
examples/vector_index.rs.
§Connections and networking
§TLS support
This client is currently built upon the redis crate, and therefore supports TLS
using
its implementation, which uses either rustls or
native_tls.
This is not enabled by default, and the user just opt-in by enabling the respective features: "rustls"/"native-tls" (
when using tokio: "tokio-rustls"/"tokio-native-tls").
For Rustls:
cargo add falkordb --features rustlscargo add falkordb --features tokio-rustlsFor Native TLS:
cargo add falkordb --features native-tlscargo add falkordb --features tokio-native-tlsA runnable example is provided in examples/tls.rs.
§TCP keepalive
Long-lived clients behind NATs, stateful firewalls, or idle-timeout-enforcing proxies can silently lose their TCP sessions. The builder exposes TCP-level socket settings to prevent this:
use falkordb::FalkorClientBuilder;
use std::time::Duration;
// Convenience: just enable keepalive with a 30-second idle timeout
let client = FalkorClientBuilder::new()
.with_tcp_keepalive(Duration::from_secs(30))
.build()
.expect("Failed to build client");
// Or full control via redis::io::tcp::TcpSettings
let settings = redis::io::tcp::TcpSettings::default()
.set_nodelay(true)
.set_keepalive(
redis::io::tcp::socket2::TcpKeepalive::new()
.with_time(Duration::from_secs(60)),
);
let client = FalkorClientBuilder::new()
.with_tcp_settings(settings)
.build()
.expect("Failed to build client");Note: TCP settings apply to direct Redis TCP connections only. Unix-domain socket / embedded connections and the Sentinel connection path are not affected.
§Read-only queries and replica routing
Read-only queries (ro_query and call_procedure_ro) send GRAPH.RO_QUERY, which the server
refuses to let write. Where such a query runs is a separate, opt-in choice expressed with
ReadPreference. Because a FalkorDB replica applies writes only after the primary, a read
served from a replica can be slightly stale, so the default (ReadPreference::Primary)
keeps every read on the primary — you never observe replication lag unless you ask for it.
Opt into replicas either per client or per query:
- Per client —
with_read_preferencesets the default for every read-only query. - Per query —
prefer_replicaopts a single query in, andprimary_onlyforces one back onto the primary (read-your-writes). The per-query choice overrides the client default.
Replica routing requires a Redis Sentinel deployment that exposes readable replicas; when none is
available (for example a single node), ReadPreference::PreferReplica transparently falls back
to the primary, so the same code runs everywhere. Writes always go to the primary — asking for a
replica on a writable query/call_procedure/batch fails with
FalkorDBError::ReadPreferenceNotReadOnly.
Connection pool sizing: When readable replicas are present the client opens a second pool of up to
num_connectionsadditional connections (one per slot) alongside the primary pool, regardless of the read preference. Size your pool limits and file-descriptor limits accordingly.
use falkordb::{FalkorClientBuilder, ReadPreference};
let client = FalkorClientBuilder::new()
// A Sentinel endpoint, e.g. falkor://127.0.0.1:26379
.with_connection_info("falkor://127.0.0.1:26379".try_into().expect("Invalid connection info"))
// Prefer replicas for this client's read-only queries (accepts slightly stale reads).
.with_read_preference(ReadPreference::PreferReplica)
.build()
.expect("Failed to build client");
// Capability (a replica pool exists) vs policy (the default routing).
if client.replica_reads_available() {
println!("Replica connections are available");
}
println!("Default read preference: {:?}", client.read_preference());
let mut graph = client.select_graph("imdb");
// Writes go to the primary.
graph.query("CREATE (:Actor {name: 'Tom Hanks'})").execute().expect("Failed to write");
// Follows the client default (a replica when available, else the primary).
let mut nodes = graph.ro_query("MATCH (a:Actor) RETURN a.name").execute().expect("Failed to read");
// Force the freshest data from the primary for a single read, overriding the default.
let mut fresh = graph.ro_query("MATCH (a:Actor) RETURN a.name").primary_only().execute().expect("Failed to read");Against a single node (or any deployment without readable replicas),
replica_reads_available returns false and reads
use the primary. See examples/readonly_replica.rs
for a complete working example.
§Resilience and observability
§Automatic retries
A client can opt in to a RetryPolicy that automatically re-issues eligible operations on
transient connection failures, with bounded backoff. It is disabled by default, so a client
built without one behaves exactly as before (every operation is attempted once):
use falkordb::{Backoff, FalkorClientBuilder, RetryPolicy};
use std::time::Duration;
let client = FalkorClientBuilder::new()
.with_retry_policy(
RetryPolicy::read_only() // retry read-only ops only
.max_attempts(4) // 1 initial try + up to 3 retries
.backoff(Backoff::exponential(Duration::from_millis(50))
.max_delay(Duration::from_secs(1))), // 50ms, 100ms, 200ms, … capped at 1s
)
.build()?;The same with_retry_policy(..) is available on the async builder
(FalkorClientBuilder::new_async()).
Write safety. The only scope available today, RetryScope::ReadOnly, retries read-only /
idempotent operations only (ro_query, explain, list_indices, list_constraints, read-only
procedure calls). Writes are never retried, so enabling a policy can never duplicate a write.
Classification is by the API you call, never by inspecting Cypher — query() is treated as a write
even when it only reads, so use ro_query() for retryable reads.
Only transient connection errors are retried (a dropped/unavailable connection, or a Sentinel resolution failure); deterministic errors (syntax, constraint violations, parse/type errors, wait-operation timeouts) are returned immediately. Retries compose with the client’s existing connection healing: each attempt re-borrows a connection, so a recovered connection is picked up on the next try.
Scope. Retry currently wraps query and procedure execution — ro_query, query, explain,
profile, call_procedure/call_procedure_ro, and list_indices/list_constraints (only the
read-only ones are eligible). Direct client/admin calls (list_graphs, configuration getters/setters,
slowlog, server INFO) and the internal schema-cache refresh that can run while a result is parsed
are not wrapped yet, so a transient failure there still surfaces even with a policy enabled.
Broadening the coverage is a planned follow-up.
See examples/retry.rs for a complete, runnable example.
§Tracing
This crate fully supports instrumentation using the tracing crate, to use
it, simply, enable the tracing feature:
cargo add falkordb --features tracingNote that different functions use different filtration levels, to avoid spamming your tests, be sure to enable the correct level as you desire it.
When the tracing feature is enabled, the query- and procedure-execution spans are enriched with
structured, low-cardinality fields you can slice and filter on (named after the
OpenTelemetry database conventions so they
map cleanly when exported via tracing-opentelemetry):
| Field | Example | Meaning |
|---|---|---|
db.system.name | falkordb | constant |
db.namespace | social | the graph name |
db.operation.name | GRAPH.RO_QUERY / db.idx.fulltext.queryNodes | the command or procedure |
db.falkordb.read_only | true | whether the operation is read-only |
db.falkordb.strategy | multiplexed | the active connection strategy |
db.query.fingerprint | a1b2c3d4e5f60718 | a privacy-safe hash of the query shape |
error.type | connection_down | a bounded error kind, recorded on failure |
db.response.returned_rows | 42 | rows the server returned (on the outer execute span) |
db.falkordb.server_time_ms | 1.18 | the server’s internal execution time, when reported |
Privacy by default. The raw query text and parameter values are never recorded by default —
only the db.query.fingerprint, which is a hash of the query with all literals (strings, numbers,
true/false/null) redacted, so two queries that differ only in their values share a fingerprint
and no value ever enters a span. If you need the raw Cypher for debugging in a trusted environment,
opt in explicitly:
use falkordb::FalkorClientBuilder;
let client = FalkorClientBuilder::new()
.with_query_logging(true) // records `db.query.text`; off by default
.build()?;Parameter values supplied via with_param are never recorded even when query logging is enabled
(they live in the query preamble, not the query text).
Note: the async query/procedure futures are deeply nested (retry + instrumentation). If you
tokio::spawnthem with thetracingfeature enabled and hit arecursion limit/overflow evaluating ... Senderror, add#![recursion_limit = "256"]to your crate root — the standard fix for deepasync+tracingstacks.
See examples/observability.rs for a complete, runnable example.
§Metrics
Enable the metrics feature to emit counters and histograms through the
metrics facade, so your application can install any
exporter (Prometheus, OpenTelemetry, …):
cargo add falkordb --features metricsEach query and procedure execution records:
| Metric | Type | Labels |
|---|---|---|
falkordb_queries_total | counter | command, operation (read/write), strategy |
falkordb_query_duration_seconds | histogram | command, operation |
falkordb_query_errors_total | counter | command, error_kind |
falkordb_retries_total | counter | operation, error_kind |
falkordb_connections_in_flight | gauge | route (primary/replica) |
falkordb_connection_pool_wait_seconds | histogram | route (pooled strategy only) |
All labels are bounded, low-cardinality values: command is an allowlist of known commands
(unknown ⇒ other), operation/strategy/error_kind are small fixed sets. The graph name, query
text, and query fingerprint are never used as labels (they are unbounded and would explode metric
cardinality) — those belong on tracing spans, not metrics. Like tracing, recording is a no-op
until you install a recorder; for example, with metrics-exporter-prometheus:
let builder = metrics_exporter_prometheus::PrometheusBuilder::new();
builder.install().expect("failed to install Prometheus recorder");
// ... use the client; metrics are now exported on the configured endpoint.§Actionable error hints
FalkorDBError::mitigation_hint() turns common, recognizable failures into a short, actionable
remediation tip — handy for logs and AI tooling. It is purely additive: the raw error and its
Display/Debug output are unchanged, hints are fixed &'static strs (so they never echo text from
the underlying message), and unrecognized errors return None.
use falkordb::FalkorDBError;
let err = FalkorDBError::ConnectionDown;
if let Some(hint) = err.mitigation_hint() {
println!("hint: {hint}");
}§Embedded server
This client supports running an embedded FalkorDB server, which is useful for:
- Testing without external dependencies
- Embedded applications
- Quick prototyping and development
To use the embedded feature, enable it:
cargo add falkordb --features embedded§Choosing a module-provisioning mode
The redis-server binary is never downloaded — it must be installed on the host (see
Requirements). Only the FalkorDB falkordb.so module is provisioned, and there are two
features for that:
-
embedded— runtime download. The module is downloaded on first start (and cached), so the running process needs network access the first time. Best for development. -
embedded-bundle— build-time embed, offline at runtime. Abuild.rsfetches the module for the build target at compile time and embeds it in your binary, so the running process needs no network at all. Best for network-isolated deployments. Enable it instead ofembedded:cargo add falkordb --features embedded-bundleControl the bundled module at build time with environment variables:
FALKORDB_EMBEDDED_MODULE_VERSION(release tag; defaults to the pinned version),FALKORDB_EMBEDDED_MODULE_PLATFORM(override the asset for distro-specific Linux targets such asrhel9-x64), andFALKORDB_EMBEDDED_MODULE_PATHto embed a local.soinstead of downloading (fully offline builds, or unsupported platforms). A non-default version must be accompanied byFALKORDB_EMBEDDED_MODULE_SHA256— unchecked downloaded native code is never embedded. The downloading build uses the hostcurl(setFALKORDB_EMBEDDED_MODULE_PATHon build hosts withoutcurlor network access); theembedded-bundleruntime itself carries no HTTP/hashing dependencies.License:
embedded-bundleembeds the SSPL-licensed FalkorDB module into your binary, so you are responsible for complying with its license when you distribute that binary.
§Requirements
redis-server(version 8.0 or newer) must be installed and available in PATH (or you can specify a custom path). It is not downloaded automatically — install it from your package manager (e.g.brew install redis,apt-get install redis-server).- The
falkordb.somodule is provisioned automatically: downloaded at runtime withembedded(whenauto_downloadis enabled, the default) or embedded at build time withembedded-bundle. You can also pointfalkordb_module_pathat an existing module, or disableauto_downloadto use only explicit/system-installed binaries. - On macOS the module requires OpenMP:
brew install libomp.
Supported platforms: Linux x86_64/aarch64 (glibc and musl/Alpine, plus RHEL 8/9 and Amazon Linux 2023 on x86_64) and macOS aarch64 (Apple Silicon).
§Self-contained vs. already-installed
use falkordb::EmbeddedConfig;
use std::path::PathBuf;
// Self-contained (default): download + cache the module if it is missing.
let _auto = EmbeddedConfig::default();
// Offline: use only binaries already on the machine (no network access).
let _offline = EmbeddedConfig {
auto_download: false,
falkordb_module_path: Some(PathBuf::from("/usr/lib/redis/modules/falkordb.so")),
..Default::default()
};The cache directory defaults to ~/.cache/falkordb-rs (Linux) or
~/Library/Caches/falkordb-rs (macOS) and can be overridden with the
cache_dir field or the FALKORDB_RS_CACHE_DIR environment variable.
§Usage Example
use falkordb::{EmbeddedConfig, FalkorClientBuilder, FalkorConnectionInfo};
// Create an embedded configuration with defaults
let embedded_config = EmbeddedConfig::default();
// Or customize the configuration:
// let embedded_config = EmbeddedConfig {
// redis_server_path: Some(PathBuf::from("/path/to/redis-server")),
// falkordb_module_path: Some(PathBuf::from("/path/to/falkordb.so")),
// db_dir: Some(PathBuf::from("/tmp/my_falkordb")),
// falkordb_version: None, // pin a different release, e.g. Some("v4.18.10".into())
// cache_dir: None, // override the download cache location
// ..Default::default()
// };
// Build a client with embedded FalkorDB
let client = FalkorClientBuilder::new()
.with_connection_info(FalkorConnectionInfo::Embedded(embedded_config))
.build()
.expect("Failed to build client");
// Use the client normally
let mut graph = client.select_graph("social");
graph.query("CREATE (:Person {name: 'Alice', age: 30})").execute().expect("Failed to execute query");
// The embedded server will be automatically shut down when the client is droppedThe embedded server:
- Spawns a
redis-serverprocess with the FalkorDB module loaded - Uses Unix socket for communication (no network port)
- Automatically cleans up when the client is dropped
- Can be configured with custom paths, database directory, and socket location
§Examples
Every example is a runnable file under examples/ and is compiled in CI.
Run one with cargo run plus the flags shown:
| Example | Shows | Run with |
|---|---|---|
basic_usage | A minimal connect, query, and iterate flow | --example basic_usage |
rows | Header-aware rows: read columns by name or index with strict typed access | --example rows |
typed_params | Type-safe, injection-proof query parameters | --example typed_params |
typed_mapping | Map query results into your own serde types | --features serde --example typed_mapping |
temporal | Decode temporal values and use the type-safe DateTime/Duration algebra | --example temporal |
vector_index | Create vector indexes with the typed helpers and VectorSimilarity | --example vector_index |
batch | Batch / pipelined execution: many queries in one round-trip | --example batch |
waiting_ops | Wait for background index / constraint / copy operations to take effect | --example waiting_ops |
udf_usage | Load a user-defined-function (UDF) library | --example udf_usage |
async_api | The async (tokio) client end to end | --features tokio --example async_api |
async_stream | Async streaming with futures combinators | --features tokio --example async_stream |
multiplexed_async | The multiplexed async connection strategy | --features tokio --example multiplexed_async |
readonly_replica | Route read-only queries to replica nodes | --example readonly_replica |
retry | The opt-in retry policy for transient failures | --example retry |
tls | Connect to FalkorDB over TLS | --features rustls --example tls |
observability | tracing span enrichment and the query fingerprint | --features tracing --example observability |
embedded_usage | Run an embedded FalkorDB server | --features embedded --example embedded_usage |
§API documentation
The complete API reference is published on docs.rs.
§Migration guides
§Contributing
Development setup, the full just recipe reference, and how to run the tests and benchmarks live in
CONTRIBUTING.md.
§Community and license
Licensed under the MIT License.
Structs§
- Async
Constraint OpBuilder - Builder for creating or dropping a constraint on an
AsyncGraph. - Async
Copy Graph Builder - Builder for copying a graph via a
FalkorAsyncClient. - Async
Graph - The main graph API, this allows the user to perform graph operations while exposing as little details as possible.
- Async
Index OpBuilder - Builder for creating or dropping an index on an
AsyncGraph. - Backoff
- The backoff schedule applied between retry attempts.
- Batch
Builder - Accumulates queries to run as a single pipelined batch. Create one with
graph.batch(). - Batch
Query - A single, graph-less query to run as part of a
BatchBuilder. Owns its query text, parameters and timeout, so queries can be built up front (e.g. in a loop) andpushed into a batch. - Constraint
- A constraint applied on all ‘properties’ of the graph entity ‘label’ in this graph
- Constraint
OpBuilder - Builder for creating or dropping a constraint on a
SyncGraph. - Copy
Graph Builder - Builder for copying a graph via a
FalkorSyncClient. - Date
- A FalkorDB
datevalue (compact type marker 14). - Date
Time - A FalkorDB
datetimevalue (compact type marker 13): seconds since the Unix epoch (UTC). - Duration
- A FalkorDB
durationvalue (compact type marker 16): a span expressed in seconds. - Edge
- An edge in the graph, representing a relationship between two
Nodes. - Embedded
Config - Configuration for an embedded FalkorDB server instance.
- Embedded
Server - Manages an embedded FalkorDB server instance.
- Execution
Plan - An execution plan, allowing access both to the human-readable text representation, access to a per-operation map, or traversable operation tree
- Falkor
Async Client - This is the publicly exposed API of the asynchronous Falkor Client It makes no assumptions in regard to which database the Falkor module is running on, and will select it based on enabled features and url connection
- Falkor
Client Builder - A Builder-pattern implementation struct for creating a new Falkor client.
- Falkor
Index - Contains all the info regarding an index on the database
- Falkor
Params - An ordered, name-deduplicated set of query parameters.
- Falkor
Sync Client - This is the publicly exposed API of the sync Falkor Client It makes no assumptions in regard to which database the Falkor module is running on, and will select it based on enabled features and url connection
- Falkor
Value Deserializer - A
serde::Deserializerthat consumes an ownedFalkorValue. - Graph
Schema - A struct containing the various schema maps, allowing conversions between ids and their string representations.
- Index
OpBuilder - Builder for creating or dropping an index on a
SyncGraph. - Lazy
Result Set - A wrapper around the returned raw data, parsing each row on demand into a header-aware
Row. - Node
- A node in the graph, containing a unique id, various labels describing it, and its own property.
- Path
- Represents a path between two nodes, contains all the nodes, and the relationships between them along the path
- Point
- A point in the world.
- Procedure
Query Builder - A Builder-pattern struct that allows creating and executing procedure call on a graph
- Query
Builder - A Builder-pattern struct that allows creating and executing queries on a graph
- Query
Result - A response struct which also contains the returned header and stats data
- RawParam
- A raw, already-valid Cypher expression to use as a parameter value, without any escaping.
- Retry
Policy - An opt-in, client-wide policy describing how many times, how fast, and which operations to re-issue on transient connection failures.
- Row
- A single result row: the query’s column names (the header) paired with this row’s values.
- RowStream
- The owned result set produced by an
AsyncGraphquery. - Seconds
- A signed number of seconds — the scalar carried by every FalkorDB temporal value.
- Slowlog
Entry - A slowlog entry, representing one of the N slowest queries in the current log
- Sync
Graph - The main graph API, this allows the user to perform graph operations while exposing as little details as possible.
- Time
- A FalkorDB
time/localtimevalue (compact type marker 15), expressed in seconds. - Typed
Lazy Result Set - A typed view over a
LazyResultSetthat deserializes each row intoTon demand. - Typed
RowStream - A typed view over a
RowStreamthat deserializes each row intoTon demand. - Wait
Options - Tunables for how a
waitterminal polls/retries a background operation.
Enums§
- Config
Value - An enum representing the two viable types for a config value
- Connection
Strategy - Selects how a client manages its underlying Redis connection(s).
- Constraint
Status - The status of this constraint
- Constraint
Type - The type of restriction to apply for the property
- Entity
Type - Whether this element is a node or edge in the graph
- Falkor
Connection Info - An agnostic container which allows maintaining of various connection details. The different enum variants are enabled based on compilation features
- FalkorDB
Error - A verbose error enum used throughout the client, messages are static string slices.
this allows easy error integration using
thiserror - Falkor
Value - An enum of all the supported Falkor types
- Index
Status - The status of this index
- Index
Type - The type of this indexed field
- Read
Preference - Controls which node a read-only query may be served from.
- Retry
Scope - Which operations an enabled
RetryPolicyis allowed to re-issue. - Schema
Type - An enum specifying which schema type we are addressing When querying using the compact parser, ids are returned for the various schema entities instead of strings Using this enum we know which of the schema maps to access in order to convert these ids to strings
- Vector
Similarity - The similarity function a vector index uses when comparing vectors.
- Wait
Operation - Identifies which background operation a
crate::FalkorDBError::Timeoutrefers to.
Traits§
- From
Falkor Value - A type that can be extracted from a
FalkorValue. - Into
Falkor Param - A Rust value that can be encoded as a single FalkorDB/Cypher query-parameter literal.
- Into
Falkor Params - A collection that can be turned into a set of query parameters for
QueryBuilder::with_params.
Functions§
- from_
falkor_ row - Deserialize a single result row into any type that implements
serde::Deserialize. - from_
falkor_ value - Deserialize a
FalkorValueinto any type that implementsserde::Deserialize. - to_
cypher_ param - Encode a single value as a Cypher parameter literal.
Type Aliases§
- Batch
Item Result - The result of one query in a batch:
Okwith itsQueryResult(rows eagerly parsed into aVec<Row>), orErrwith that query’s server / encoding / parse error. A failure here does not affect the other queries in the batch. - Batch
Result - The result of
BatchBuilder::execute: oneBatchItemResultper query, in submission order. - Falkor
Result - A
Resultwhich only returnsFalkorDBErroras its E type