Configuration
This page lists every setting that changes how HornDB behaves at runtime or at build time. HornDB’s configuration surface has four parts: a config.toml file (with config.d/*.toml drop-in fragments), environment variables, the serve binary’s own command-line flags, and two Cargo feature flags.
Layers merge from lowest precedence to highest:
- built-in defaults,
- the base
config.toml, config.d/*.tomldrop-in fragments, in filename order,- environment variables,
- command-line flags.
Environment variables carry the HORNDB_ prefix and use two underscores to separate a config section from the key inside it: [server].bind is HORNDB_SERVER__BIND, [simd].max_isa is HORNDB_SIMD__MAX_ISA. A HORNDB_ variable with no __ in its name is not a config key and is ignored by the config layer — HORNDB_CONFIG is the one such variable HornDB reads, and it is read separately, to locate the config file itself.
An unknown key or an unparsable value is fatal: serve names the offending setting and exits rather than starting with a value it could not read.
SPARQL HTTP server
The serve binary loads one or more RDF files into a store and exposes SPARQL 1.1 over HTTP on a single bind address, at six fixed routes:
| Route | Methods | Purpose |
|---|---|---|
/query |
GET, POST |
SPARQL 1.1 query. |
/update |
POST |
SPARQL 1.1 Update. |
/graphs |
GET, PUT, POST, DELETE |
Graph Store Protocol — read or write one whole named graph, selected with ?graph=<iri> or ?default. |
/metrics |
GET |
Prometheus scrape (see Metrics endpoint). |
/healthz |
GET |
Liveness: the process is up. |
/readyz |
GET |
Readiness: the startup data load has finished. Returns 503 until it has, and /query, /update, and /graphs shed requests with 503 over the same window rather than answer from a partly-loaded store. |
The routes themselves are not configurable. serve resolves the rest of its settings through the layers listed above before it loads any data or binds the socket.
--data(list of paths)-
Default: none. Required unless
[server].data_dirnames a durable store; with neither,serveexits with an error because it has nothing to serve.Scope: CLI flag to the
servebinary; read once at process start.Allowed values: one or more paths to
.nt(N-Triples),.ttl(Turtle),.nq(N-Quads), or.trig(TriG) files, or to directories containing them. Repeat the flag to pass more than one path.Tuning notes: point at a directory to load every file of those four types under it, recursively.
.ntand.ttlcarry triples and load into the default graph..nqand.trigcarry quads: each quad loads into the named graph it names, so these are how you populate named graphs at startup. Blank-node labels are scoped per file, so two files can reuse a label without colliding.Tuning notes (with
data_dir): a durable store already holds what was loaded into it, so a restart against one needs no--dataat all. Passing both re-loads the files on every start. That is safe — a quad already present inserts nothing — but it costs a parse of the whole corpus and a log record per batch each time. [server].data_dir(path)-
Default: unset, which means the store lives in memory only and every write is lost when the process exits.
Scope: server. Restart-only — the directory is opened, and locked, once at startup.
Allowed values: a path to a directory. It is created if it does not exist. Environment form:
HORNDB_SERVER__DATA_DIR.Tuning notes: this is what makes writes survive a restart. The directory holds a write-ahead log, the dictionary base, and checkpoints. Every write is appended to the log and flushed to disk before the request that made it is answered, so anything
/updatereturned a success status for is recoverable — including after akill -9or a power loss.Only one process may hold the directory at a time. A second
serveon the same one fails at startup with a clear error instead of interleaving records into one log and corrupting it. The lock is the operating system’s and is tied to an open file, so it is released whenever the holder dies, however abruptly — a restart after a crash is never blocked by a leftover lock.Put the directory on local disk. Write latency is a flush to this device on every write batch, and a network filesystem’s flush semantics are not strong enough for the guarantee above.
[server].checkpoint_interval(duration) and[server].checkpoint_changes(integer)-
Defaults:
60sand100000.Scope: server. Restart-only; both are ignored without
data_dir.Allowed values: a duration string (
30s,5m) and a count. Acheckpoint_changesof0turns the change trigger off and leaves the time trigger running.Tuning notes: a checkpoint folds the log into a fresh base so the next start replays less. One runs when either trigger fires: the interval has passed with at least one write, or that many quads have been inserted or retracted since the last one. An idle store never checkpoints.
Both knobs trade startup time against steady-state cost. Lower values checkpoint more often — a shorter replay on the next start, but each checkpoint writes the dictionary and every visible row, and blocks writers (not readers) while it runs. Raise them on a write-heavy store where that pause shows up, and accept the longer restart.
--config(path)-
Default: none. With the flag unset,
servereads the path inHORNDB_CONFIG; with that unset too, it reads/etc/horndb/config.toml.Scope: CLI flag to the
servebinary; chooses which base config file is read, and outranksHORNDB_CONFIGin doing so.Allowed values: a path to a TOML file.
Tuning notes: a file named explicitly — by this flag or by
HORNDB_CONFIG— must exist. A missing/etc/horndb/config.toml, which nothing asked for by name, is not an error:servestarts on built-in defaults. HORNDB_CONFIG(path, environment variable)-
Default: unset, so
servereads/etc/horndb/config.toml.Scope: environment variable, read once at process start. Ranks between the default path and
--config, which wins over it. It names the config file and is not itself a config key, so it carries no__section separator.Allowed values: a path to a TOML file.
Tuning notes: set it to point a whole environment at one config file without changing each unit’s command line.
--bind(address string),HORNDB_SERVER__BIND-
Default:
127.0.0.1:3840(3840 is HornDB’s standard port). The default lives in the config layer as[server].bind, not on the flag.Scope: CLI flag to the
servebinary, or the matching environment variable and[server].bindconfig key; fixed for the life of the process. The flag wins over the environment variable, which wins over the file.Allowed values: any
host:portaddress accepted by a Tokio TCP listener, for example0.0.0.0:3840to listen on every interface.Tuning notes: change the host to expose the server beyond localhost; change the port to run more than one instance on the same host. Leave the flag off to keep whatever the file or environment resolved to — an unset flag never overrides a lower layer.
--simd-max-isa(string),--simd-autotune(boolean)-
Default: none; both leave
[simd].max_isaand[simd].autotuneas the config layer resolved them.Scope: CLI flags to the
servebinary, the top layer for the two[simd]settings; fixed for the life of the process.Allowed values: as for
HORNDB_SIMD__MAX_ISAandHORNDB_SIMD__AUTOTUNEbelow.Tuning notes: use these to override the SIMD kernel choice for a single run — a one-off diagnostic — without editing the config file or exporting a variable.
--materialize(boolean flag)-
Default:
false(off —serveanswers queries over the asserted triples only).Scope: CLI flag to the
servebinary; fixed for the life of the process. Requires the binary to be built with thereasonerCargo feature (on by default — see below).Allowed values: present or absent; the flag takes no argument.
Tuning notes: pass it to run OWL 2 RL forward-chaining over the loaded data before serving, so queries see the closure — the asserted triples plus every triple the reasoner derives — rather than only what was asserted. If the binary was built without the
reasonerfeature, passing this flag makesserveexit with an error instead of starting. server(Cargo feature,horndb-sparqlcrate)-
Default: on.
Scope: compile-time — set with
--features/--no-default-featureswhen building the crate, not at runtime.Allowed values: on or off.
Tuning notes: the
servebinary requires this feature and does not build without it. Turn it off only when usinghorndb-sparqlas a library with no HTTP server, for example an embedded query engine with no network listener. reasoner(Cargo feature,horndb-sparqlcrate)-
Default: on.
Scope: compile-time — set with
--features/--no-default-featureswhen building the crate, not at runtime.Allowed values: on or off.
Tuning notes: gates
--materialize. Turn it off to build a smaller binary that serves SPARQL over asserted data without pulling in the OWL 2 RL reasoner.
Query settings
The [server.limits] config section holds the defaults for settings a single query may override for itself. Five settings take a per-query override, listed below; three more in the same section are server-scoped only and are covered under Admission control and request size.
An override travels on all three protocol channels: as a URL query parameter on GET /query, as a URL query parameter on a POST with an application/sparql-query body, and as a form field in an application/x-www-form-urlencoded POST body. When a form-encoded POST carries the same key in both the URL and the body, the body wins. An unrecognised key, or a value the setting cannot parse, returns HTTP 400 naming the parameter; the query does not run. An override changes nothing on the server and does not affect any other query.
| Setting | Default | Enforced |
|---|---|---|
query_timeout |
30s |
Yes — the query is cancelled and the request ends 504 Gateway Timeout. |
max_result_rows |
1000000 |
Yes — see the entry below; the result is never silently truncated. |
rdf12 |
false |
Yes — with it off, a query using an RDF 1.2 triple term is refused when the algebra is built. |
default_graph |
union |
Yes — see the entry below. |
max_query_memory |
8GiB |
Yes — see the entry below; the query fails with 507 Insufficient Storage and the result is never truncated. |
[server.limits].query_timeout(duration),HORNDB_SERVER__LIMITS__QUERY_TIMEOUT,query_timeout(per query)-
Default:
30s.Scope: config key, environment variable, or a per-query override. It is one of the reloadable settings: an edit to the file takes effect on the next request, with no restart.
Allowed values: a duration with an explicit unit —
250ms,5s,2m. A bare number is an error, so a value cannot be read under the wrong unit.Tuning notes: when the timeout elapses the query is cancelled mid-flight through its cancellation token and the request ends
504 Gateway Timeout, which is a distinct status from the400a malformed or unanswerable query gets — a client can tell “too slow” from “wrong”. Raise it for one known-heavy analytical query rather than for the whole server. [server.limits].max_result_rows(integer),HORNDB_SERVER__LIMITS__MAX_RESULT_ROWS,max_result_rows(per query)-
Default:
1000000.Scope: config key, environment variable, or a per-query override. Reloadable, like
query_timeout.Allowed values: a non-negative integer. A negative value is an error.
Tuning notes: the cap never truncates. A query that produces more solutions than the cap ends with an error, not a short answer:
400 Bad Requestif the limit is reached before the response headers are sent, and an aborted response body if it is reached after. That is deliberate — a truncated result set that looked complete would be worse than a failure. [server.limits].rdf12(boolean),HORNDB_SERVER__LIMITS__RDF12,rdf12(per query)-
Default:
false.Scope: config key, environment variable, or a per-query override. Reloadable.
Allowed values: exactly
trueorfalse, lower case.Tuning notes: set it to
trueto let a query use RDF 1.2 triple terms. With it off, a query containing one is refused when the algebra is built, rather than evaluated under RDF 1.1 rules. Turn it on per query to try RDF 1.2 syntax without changing what every other client sees. [server.limits].max_query_memory(byte size),HORNDB_SERVER__LIMITS__MAX_QUERY_MEMORY,max_query_memory(per query)-
Default: unset — no limit.
Scope: config key, environment variable, or a per-query override.
Allowed values: a byte size in IEC units —
512MiB,2GiB. Decimal units such as2GBare an error.Tuning notes: not enforced yet. The value is parsed, validated, and carried with the query, but nothing acts on it: setting it does not stop a query from using more memory than it names. It is documented because it is a live, validated key — a bad value stops
serveat startup — not because it changes behaviour. Per-query memory accounting is specified separately. [server.limits].default_graph(string),HORNDB_SERVER__LIMITS__DEFAULT_GRAPH,default_graph(per query)-
Default:
union.Scope: config key, environment variable, or a per-query override. Reloadable.
Allowed values:
unionorstrict, lower case. Any other value in the config file makesserveexit at startup, naming the file and the key; any other value on a request returns HTTP 400 naming the parameter.Tuning notes: the setting decides what the default graph is for a query that names no dataset — one with no
FROMand noFROM NAMEDclause. Underunion, the default graph is the union of every named graph in the store plus the unnamed default graph, with a triple held by several graphs counted once. Understrict, it is the unnamed default graph alone, so a store whose data lives entirely in named graphs answers every unqualified query with zero rows. Graphs under the reservedhttps://horndb.io/graph/namespace are excluded from the union in both modes. Neither mode affects a query that names its own dataset, or whatGRAPH ?granges over. Choosestrictwhen comparing HornDB against another SPARQL store — most read the no-dataset case that way, sounionwill show a difference on every unqualified query. Override it for one query to check a difference without changing what other clients see:GET /query?query=…&default_graph=strict.
Admission control, request size, and store-side memory
Four [server.limits] keys bound what the server accepts at once, how large a request may be, and how much memory the store may keep on a query’s behalf. Unlike the query settings above they are not per-query and not reloadable: they are read once at startup and fixed for the life of the process, because the semaphore, the body-limit layer, and the store they configure are all built when the server starts.
[server.limits].max_concurrent_queries(integer),HORNDB_SERVER__LIMITS__MAX_CONCURRENT_QUERIES-
Default: the host’s available parallelism — the CPU count the process can actually use.
Allowed values: an integer of 1 or more.
0stopsserveat startup rather than being clamped, so a config typo is loud.Tuning notes: this many read requests execute at once; the rest wait. Slots gate
/queryandGET /graphs, the two routes that run a read over the store./updatedoes not take a slot. A request holds its slot for the whole response, not just until the first chunk, because it owns a thread and a pinned read view for as long as the client is draining. Lower it to protect a host from too many concurrent heavy queries; raise it when requests are small and clients are numerous. [server.limits].queue_timeout(duration),HORNDB_SERVER__LIMITS__QUEUE_TIMEOUT-
Default:
5s.Allowed values: a duration with an explicit unit, as for
query_timeout.Tuning notes: how long a request waits for a slot before the server gives up on it and returns
503 Service Unavailablewith aRetry-Afterheader. Applies to the same two routes asmax_concurrent_queries. Shedding a request that would have queued for a long time is better than holding a connection open behind an unbounded queue. [server.limits].max_request_body(byte size),HORNDB_SERVER__LIMITS__MAX_REQUEST_BODY-
Default:
4MiB.Allowed values: a byte size in IEC units —
1MiB,64MiB.Tuning notes: caps the request body on
/query,/update, and/graphs; a larger body is rejected with413 Payload Too Largebefore the handler sees it./metrics,/healthz, and/readyzare outside the cap. Bulk ingest is unaffected:LOADandserve --dataread files from disk, not request bodies. Raise it if you push large graphs overPUT /graphs. [server.limits].max_snapshot_memory(byte size),HORNDB_SERVER__LIMITS__MAX_SNAPSHOT_MEMORY-
Default:
64GiB.Allowed values: a byte size in IEC units —
8GiB,64GiB. Write0for no ceiling at all.Tuning notes: bounds the snapshot memo — the sorted copy of the graph the first query on a version builds, that every later query reuses, and that the store keeps until the next write. That memory is triggered by a query but owned by the store, so
max_query_memorydeliberately does not charge it: the first query to need the index would be refused and an identical second query served from the index the first was refused for building.A query that would push the memo past this ceiling fails with
507 Insufficient Storageand a message namingmax_snapshot_memory. Nothing is built, so the memo is unchanged — and a query the store already has the snapshot for is always served, however low the ceiling is.The memo is sized by the corpus, so size this by the corpus too. The ceiling is checked against the worst case, 144 bytes per triple (six sorted orderings). Watch
horndb_sparql_snapshot_memo_byteson/metricsto see how close a live server is running. Lower it on a host smaller than a benchmark server; the default is a backstop against runaway growth, not a tuning for a small machine.
Reasoning
serve --materialize runs OWL 2 RL forward chaining before it starts serving. The transitive- and equivalence-shaped rules (rdfs:subClassOf and rdfs:subPropertyOf chains, the owl:sameAs spine, declared transitive properties) can be closed two ways; every other rule is compiled rule firing either way. Both produce the same triples — only the cost differs.
[reasoning].backend(string)-
Default:
rule-firing. Environment variableHORNDB_REASONING__BACKEND. No command-line flag.Scope: read once at startup and applied to the
--materializepass; fixed for the life of the process. Ignored without--materialize.Allowed values:
rule-firing— in-engine nested-loop closure, always available.graphblas— SuiteSparse:GraphBLAS sparse-matrix closure (SPEC-05), which needs aservebuilt with thegraphblascargo feature (the released container image is). Anything else stopsserveat startup naming the bad value and the file it came from;graphblason a build without the feature stops it naming the feature, rather than quietly falling back to the slow path.Tuning notes:
graphblasis the fast path when closure dominates the materialize cost — longrdfs:subClassOf/skos:broaderchains, a largeowl:sameAsspine. On rule-firing-dominated data (LUBM-shaped, where the cost is compiled rules andrdf:typescans) it changes little. Thehorndb_reasoning_backendmetric reports which backend a running server used.
SIMD kernel selection
HornDB’s shared SIMD layer picks a kernel implementation — scalar, AVX2, AVX-512, or NEON — for each vectorized primitive (sorted-set intersection, lower-bound seek, and so on) once, at first use, and caches the choice for the life of the process. Two settings adjust that choice without a rebuild. Both live in the [simd] config section, and both are shown here under their environment-variable names.
HORNDB_SIMD__MAX_ISA(string, environment variable)-
Default: unset — no cap; the dispatcher may pick any instruction set the host supports. Same setting as
[simd].max_isainconfig.tomland the--simd-max-isaflag, which outranks it.Scope: environment variable, read by
serveat startup and applied to the SIMD dispatcher before the first kernel runs; fixed for the life of the process.[simd]is restart-only — a live config reload never changes it.Allowed values:
scalar(also accepted:none,off),avx2,avx512(also accepted:avx512f,avx-512), orneon; matching is case-insensitive and surrounding whitespace is ignored. The value is a width ceiling, not an exact pick — scalar is narrower than avx2/neon, which are both narrower than avx512 — soavx2also permits NEON kernels on an aarch64 host while it blocks AVX-512 on an x86-64 host. Any other value makesserveexit at startup, naming the bad value.Tuning notes: set to
scalarto turn SIMD off across the process, for example to isolate a suspected kernel regression. Set toavx2to disable AVX-512 across a fleet without a rebuild, since wide AVX-512 execution can trigger CPU frequency downclocking on some hosts. HORNDB_SIMD__AUTOTUNE(boolean, environment variable)-
Default: unset, which means on — each primitive times its available kernels at startup and caches the fastest. Same setting as
[simd].autotuneinconfig.tomland the--simd-autotuneflag, which outranks it.Scope: environment variable, read by
serveat startup and applied to the SIMD dispatcher before the first kernel runs; fixed for the life of the process.[simd]is restart-only — a live config reload never changes it.Allowed values: exactly
trueorfalse, lower case. The value is parsed as a boolean, so0,1,off,no, andFalseare all errors that stopserveat startup rather than falling back to a default.Tuning notes: set to
falseto use a static, widest-instruction-set preference instead of startup timing — for example on a host where per-process timing is unreliable, such as a machine with a mix of performance and efficiency CPU cores.HORNDB_SIMD__MAX_ISAstill bounds the candidate set in either mode.
Named-graph reasoning views
OWL 2 RL reasoning is off by default. When it is on, HornDB does not reason over “everything in the store”: it reasons per view — a shared vocabulary spine (your ontologies) plus exactly one data graph. Each view’s derived triples land in their own inferred graph under the reserved https://horndb.io/graph/ namespace, never in the source graph, so reading a data graph back returns exactly the quads you wrote to it.
All of these live in the [reasoning] section of config.toml. They are server-scoped and restart-only: there is no per-query override, because two queries disagreeing about what was entailed is not a setting, it is a bug.
[reasoning].enabled(boolean)-
Default:
false— no reasoning runs, no reserved graph appears, and the store behaves exactly as it did before views existed.Allowed values:
trueorfalse. Setting it totruerequires a binary built with thereasonerfeature (on by default); otherwiseserveexits at startup saying so. [reasoning].spine(list of strings)-
Default:
[]— an empty spine. Each entry is a graph IRI or an IRI prefix:"https://ex.org/vocab/"selects every graph whose IRI starts with it.Tuning notes: put your ontologies here. The spine is closed once and that closure is shared by every view, so a graph in the spine costs one reasoning run no matter how many views there are. An enabled but empty spine is legal and warned about at startup: with no shared axioms, no
rdfs:subClassOf(or similar) can fire. [reasoning].views.select(string or list of strings)-
Default:
"all-except-spine"— every graph that is not a spine graph and not one of HornDB’s own reserved graphs gets a view. The default graph gets one too, when it holds anything.Allowed values: the keyword
"all-except-spine", or a list of graph IRIs / IRI prefixes to narrow the set. A pattern that overlaps[reasoning].spine, or that reaches into the reservedhttps://horndb.io/graph/namespace, stopsserveat startup naming both keys — a graph is either spine or a view source, never both. [reasoning].views.include_spine(boolean)-
Default:
true. Set tofalseto make each view reason over its own graph alone, with no shared axioms. [reasoning].default_dataset_includes_inferred(boolean)-
Default:
false— inferred graphs stay out of a query that names no dataset, and out ofGRAPH ?genumeration. A query still reads any inferred graph by naming it explicitly.Tuning notes: set to
trueto have queries see entailed triples without naming the inferred graphs. It admits exactly the per-view inferred graphs plus the shared spine-closure graph; HornDB’s view catalog graph stays hidden either way.
Reading what the reasoner did
Two reserved graphs are queryable by name at any time:
https://horndb.io/graph/views— the view catalog: one node per view, carrying its source graph, its inferred graph, whether it is currently stale, and whether it is consistent.https://horndb.io/graph/spine-closure— what the spine alone entails, stored once rather than copied into every view.
A view that derives a contradiction is flagged consistent false in the catalog. It does not stop the server, and it does not affect any other view.
Metrics endpoint
GET /metrics on the server’s bind address returns an OpenMetrics text exposition for Prometheus to scrape. No setting controls it: the route is always mounted alongside the data routes, at a fixed path, with no separate address or port, and there is no flag or environment variable to disable it or move it. Unlike /query, /update, and /graphs, it is not subject to [server.limits].max_request_body.