Configuration

Every environment variable, CLI flag, and build-time feature flag that controls HornDB’s server and runtime behaviour.

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:

  1. built-in defaults,
  2. the base config.toml,
  3. config.d/*.toml drop-in fragments, in filename order,
  4. environment variables,
  5. 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_dir names a durable store; with neither, serve exits with an error because it has nothing to serve.

Scope: CLI flag to the serve binary; 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. .nt and .ttl carry triples and load into the default graph. .nq and .trig carry 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 --data at 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 /update returned a success status for is recoverable — including after a kill -9 or a power loss.

Only one process may hold the directory at a time. A second serve on 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: 60s and 100000.

Scope: server. Restart-only; both are ignored without data_dir.

Allowed values: a duration string (30s, 5m) and a count. A checkpoint_changes of 0 turns 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, serve reads the path in HORNDB_CONFIG; with that unset too, it reads /etc/horndb/config.toml.

Scope: CLI flag to the serve binary; chooses which base config file is read, and outranks HORNDB_CONFIG in 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: serve starts on built-in defaults.

HORNDB_CONFIG (path, environment variable)

Default: unset, so serve reads /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 serve binary, or the matching environment variable and [server].bind config key; fixed for the life of the process. The flag wins over the environment variable, which wins over the file.

Allowed values: any host:port address accepted by a Tokio TCP listener, for example 0.0.0.0:3840 to 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_isa and [simd].autotune as the config layer resolved them.

Scope: CLI flags to the serve binary, the top layer for the two [simd] settings; fixed for the life of the process.

Allowed values: as for HORNDB_SIMD__MAX_ISA and HORNDB_SIMD__AUTOTUNE below.

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 — serve answers queries over the asserted triples only).

Scope: CLI flag to the serve binary; fixed for the life of the process. Requires the binary to be built with the reasoner Cargo 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 reasoner feature, passing this flag makes serve exit with an error instead of starting.

server (Cargo feature, horndb-sparql crate)

Default: on.

Scope: compile-time — set with --features/--no-default-features when building the crate, not at runtime.

Allowed values: on or off.

Tuning notes: the serve binary requires this feature and does not build without it. Turn it off only when using horndb-sparql as a library with no HTTP server, for example an embedded query engine with no network listener.

reasoner (Cargo feature, horndb-sparql crate)

Default: on.

Scope: compile-time — set with --features/--no-default-features when 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 the 400 a 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 Request if 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 true or false, lower case.

Tuning notes: set it to true to 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 as 2GB are 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 serve at 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: union or strict, lower case. Any other value in the config file makes serve exit 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 FROM and no FROM NAMED clause. Under union, 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. Under strict, 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 reserved https://horndb.io/graph/ namespace are excluded from the union in both modes. Neither mode affects a query that names its own dataset, or what GRAPH ?g ranges over. Choose strict when comparing HornDB against another SPARQL store — most read the no-dataset case that way, so union will 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. 0 stops serve at 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 /query and GET /graphs, the two routes that run a read over the store. /update does 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 Unavailable with a Retry-After header. Applies to the same two routes as max_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 with 413 Payload Too Large before the handler sees it. /metrics, /healthz, and /readyz are outside the cap. Bulk ingest is unaffected: LOAD and serve --data read files from disk, not request bodies. Raise it if you push large graphs over PUT /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. Write 0 for 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_memory deliberately 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 Storage and a message naming max_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_bytes on /metrics to 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 variable HORNDB_REASONING__BACKEND. No command-line flag.

Scope: read once at startup and applied to the --materialize pass; 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 a serve built with the graphblas cargo feature (the released container image is). Anything else stops serve at startup naming the bad value and the file it came from; graphblas on a build without the feature stops it naming the feature, rather than quietly falling back to the slow path.

Tuning notes: graphblas is the fast path when closure dominates the materialize cost — long rdfs:subClassOf / skos:broader chains, a large owl:sameAs spine. On rule-firing-dominated data (LUBM-shaped, where the cost is compiled rules and rdf:type scans) it changes little. The horndb_reasoning_backend metric 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_isa in config.toml and the --simd-max-isa flag, which outranks it.

Scope: environment variable, read by serve at 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), or neon; 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 — so avx2 also permits NEON kernels on an aarch64 host while it blocks AVX-512 on an x86-64 host. Any other value makes serve exit at startup, naming the bad value.

Tuning notes: set to scalar to turn SIMD off across the process, for example to isolate a suspected kernel regression. Set to avx2 to 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].autotune in config.toml and the --simd-autotune flag, which outranks it.

Scope: environment variable, read by serve at 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 true or false, lower case. The value is parsed as a boolean, so 0, 1, off, no, and False are all errors that stop serve at startup rather than falling back to a default.

Tuning notes: set to false to 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_ISA still 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: true or false. Setting it to true requires a binary built with the reasoner feature (on by default); otherwise serve exits 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 reserved https://horndb.io/graph/ namespace, stops serve at startup naming both keys — a graph is either spine or a view source, never both.

[reasoning].views.include_spine (boolean)

Default: true. Set to false to 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 of GRAPH ?g enumeration. A query still reads any inferred graph by naming it explicitly.

Tuning notes: set to true to 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.