backends.radiatordb
- radiatordb
- Example
- Configuration Options
- setup_timeout
- lifecycle
- server_selection
- server
- RadiatorDB cluster addresses
- replication_host
- collection
- query
- Query with exact key
- Query with multipart key
- Query with key filters
- Where clause
- Where field types
- Where value conversion
- Where fields without a declared type
- Where typed value expressions
- Where filter validation
- Result order
- Result limit
- Order and limit validation
- Example: next user to expire
- Example: largest usage in a period
- put
- put key
- put collection
- put expire
- put bindings
- Multipart Keys
- Processing query results
- Processing a single result with @once
- Processing every result with @each
- Mapping
- Mapping without a result pipeline
- Rejecting
- Storing documents from AAA pipelines
- Revision Retention
- Garbage Collection
- Storage Statistics
- Performance Tuning
- synchronous_commit
- Internal timing environment variables
radiatordb
RadiatorDB is a document-oriented backend that stores and retrieves JSON documents in PostgreSQL. See the RadiatorDB article for an overview of features and concepts. See RadiatorDB REST API for management API document operations.
Example
The backend stores user documents in the users collection. Each document
uses the user name as its key. The FIND_USER query finds the document of the
requesting user:
backends {
radiatordb "MAIN_DB" {
server "primary" {
host "db.example.com";
port 5432;
database "radiator";
username "radiator";
password "secret";
}
collection "users" {
roles {
read "support";
write "ops";
}
name "Users";
description "User credentials for authentication";
key {
field {
name "Username";
type text;
}
}
field "password" {
name "Password";
type password;
required true;
}
field "sessionTimeout" {
name "Session Timeout";
type number;
}
}
query "FIND_USER" {
collection "users";
key aaa.identity;
}
}
}
The @once pipeline of the backend action copies the password and session
timeout from the document, and pap verifies the password:
aaa {
policy "RADIUS" {
conditions all {
aaa.protocol == protocol.RADIUS;
}
handler "AUTHENTICATION" {
@authentication {
backend {
name "MAIN_DB";
query "FIND_USER";
@once {
user.password = doc | jsonpath("$.password");
vars.session_timeout = doc | jsonpath("$.sessionTimeout");
}
}
pap;
}
}
}
}
If no document has the key, the backend action rejects and the @once
pipeline does not run. See Rejecting.
Configuration Options
setup_timeout
Limits each background node setup attempt, including connection, collection
initialization, and replication setup. The default is 30s. The value must be
greater than 0 and accepts duration units.
Connection failures retry automatically. A setup failure that requires repair
stops that node until you remove and reprovision it or restart Radiator Server.
setup_timeout 1m;
lifecycle
Configures node health checks and automatic retry delays. All values must be
greater than 0 and accept duration units.
lifecycle {
health_check_interval 5s;
health_check_timeout 2s;
retry_max_delay 30s;
}
health_check_intervalis the delay between successful idle health checks. The default is5s.health_check_timeoutlimits one health check. The default is2s, capped bysetup_timeout. A timed-out check removes the node from request routing. It also limits each lock wait when setup creates a newly configured collection, because the creation blocks requests on nodes it has already locked.retry_max_delaycaps the increasing delay between automatic retries. The default is30s.
server_selection
Selects a server when multiple RadiatorDB server blocks are configured. The
allowed algorithms are fallback, round_robin, hash_balance,
least_connections, and no_fallback. The default is fallback.
See Backend Load Balancing for syntax, algorithm behavior, field selection, and failover details.
server
Defines a PostgreSQL server for document storage. Multiple server blocks can be configured for high availability. Each server is identified by a unique name.
For failover, set host to an address that every Radiator instance can reach. Do not use localhost unless each Radiator instance connects only to its local RadiatorDB instance. If those local instances form a cluster, also set replication_host.
RadiatorDB uses the same PostgreSQL server configuration as the postgres backend. See that reference for the full list of server options including url, host, port, database, username, password, connections { ... }, priority, tls, and service_level_objective.
server "primary" {
host "db.example.com";
port 5432;
database "radiator";
username "radiator";
password "secret";
connections {
max 10;
}
}
RadiatorDB cluster addresses
RadiatorDB cluster members must be able to discover and reach each other for replication. In the typical setup, the host address is reachable by both Radiator and other RadiatorDB members. No separate replication address is needed.
Use replication_host only when Radiator should connect through one address, but other RadiatorDB members must use another address to reach the same member.
replication_host
replication_host is an optional RadiatorDB server setting. It gives RadiatorDB a separate address that the database engine uses to discover this member for replication.
Set replication_host when each Radiator instance connects to its local RadiatorDB instance with host "localhost", but those RadiatorDB instances also form a cluster. In that layout, set replication_host to the address that other RadiatorDB members can reach.
server "primary" {
host "localhost";
replication_host "db1.internal.example.com";
port 5432;
database "radiator";
username "radiator";
}
replication_host cannot be combined with url. Use separate host, port, database, username, and password settings when this override is needed.
collection
Defines a document collection and its field schema. Collections organize documents into logical groups and describe their structure for Radiator UI.
See collection for all available options.
Collection storage defaults to normal mutable storage. Configure time-series
storage in the collection storage block. Radiator initializes the configured
collections on each PostgreSQL node before it creates replication subscriptions.
A newly added collection is created on every node at once and is unavailable
until every configured node is set up. See
Add a Collection.
Use identical collection storage configuration in every Radiator process that connects to the same PostgreSQL databases. A conflicting configuration keeps the affected node unavailable and prevents subscription creation until corrected.
Removing a collection block keeps the stored data of the collection on every
node until the collection is destroyed. See
Remove a Collection
for removing a collection permanently and for changing its storage.
Every possible runtime value of a query or put collection expression must
match a declared collection block. When collection is a string literal that
does not match a declared collection, Radiator fails at startup. When
collection is an expression, Radiator rejects unknown names when the query
runs without trying another PostgreSQL node.
query
Defines a named read operation that retrieves documents from a collection. The
backend action selects the query with its query
parameter. A query selects documents in one of these ways:
- An exact
keylookup returns at most one document. - Key filters and a
whereblock scan the collection and can return many documents. - A query without
keyscans the whole collection.
A query that can return many documents can order them by a field and limit their number.
Each matched document is one result. The backend action that calls the query
processes the results in an @once or
@each pipeline. A query can also contain
a mapping block that preprocesses each result.
AAA backend pipeline query operations do not support time-series collections.
Use the Management API to read time-series documents.
Expressions in collection, key, key filters, and where filters can read request-local
arguments supplied by the calling backend action, for example
key args.identity;. Put query key, collection, and binding expressions can use the same scope.
Query with exact key
The simplest query form looks up a single document by its full key:
query "FIND_USER" {
collection "users";
key aaa.identity;
}
The key value is an expression that resolves to the document key at runtime.
Query with multipart key
Multiple key directives compose a multipart key. All parts are joined to form the full document key for an exact lookup:
query "FIND_DEVICE" {
collection "devices";
key vars.site;
key radius.request.attr.NAS-Identifier;
}
When the key is not found, the reject reason includes the composed key: "Key not found: devices/["site:helsinki","edge-1"]".
Query with key filters
Key filters match documents by inspecting individual parts of multipart keys without requiring a full exact match. Two filter types are available:
at filter - matches a specific key part by index (zero-based, supports negative indexes):
query "FIND_DEVICE" {
collection "devices";
key {
part vars.site;
at 0;
}
key {
part radius.request.attr.NAS-Identifier;
at 1;
}
where {
username == aaa.identity;
}
}
Negative indexes count from the end of the key (-1 is the last part, -2 is second to last). If the negative index is out of range for a document's key length, that document does not match.
contains filter - matches documents where any key part equals the specified value:
query "FIND_DEVICE" {
collection "devices";
key {
contains vars.site;
}
where {
username == aaa.identity;
}
}
Key filters cannot be mixed with plain key directives in the same query.
Where clause
The where block filters documents by comparing JSON field values. It is valid only in queries that use key filters or that have no key at all (collection scan). A query with an exact key cannot have a where block.
query "FIND_USER" {
collection "users";
where {
username == aaa.identity;
role != "guest";
last_seen >= "2026-01-01";
}
}
Each filter line specifies a top-level JSON field name, an operator, and a value expression.
Supported operators:
| Operator | Description |
|---|---|
== | Field value equals the value expression. |
!= | Field value is valid for the field type and does not equal the value expression. |
> | Field value is greater than the value expression. |
>= | Field value is greater than or equal to the value expression. |
< | Field value is less than the value expression. |
<= | Field value is less than or equal to the value expression. |
contains | Text of the field value contains the value expression. The match ignores case. |
Multiple filters - all filters in a where block must match (AND logic). Documents that fail any filter are excluded.
Where field types
The field type declared in the collection
decides how ==, !=, >, >=, <, and <= compare the stored value
with the value expression:
| Field type | == and != | >, >=, <, and <= |
|---|---|---|
text, enum, password, secret | Exact text. The comparison is case-sensitive. | Text by Unicode code point. The comparison is case-sensitive: "Z" < "a". |
number | Numeric. 1.0 equals 1. | Numeric. 9 < 10. |
datetime | Same instant. Offsets are taken into account. | Chronological. Offsets and fractional seconds are taken into account. |
hex | Same bytes. "AABB", "0xAABB", and "0xaabb" are equal. | Not supported. |
ip | Same address. | Not supported. |
network | The stored network contains the whole network of the value expression. | Not supported. |
boolean | Same boolean. | Not supported. |
list | Not supported. | Not supported. |
any | See Fields without a declared type. | See Fields without a declared type. |
!= is the negation of == among stored values that are valid for the field
type. Missing fields do not match any operator. Stored values that are not
valid for the declared type do not match any operator, including !=. Such
values can exist when the field type was changed after the documents were
stored, because existing documents are not converted.
contains and regular expressions match the text of the stored value for
every field type.
Where value conversion
Radiator converts the value expression to the field type in the same way as it converts a value when a document is written. The comparison then uses the converted value:
number: finite numbers and numeric strings, such as10or"10".datetime: timestamp values and every format thatdatetimefields accept when a document is written, such as"2026-01-01","2026-01-01T12:00:00+02:00", and Unix seconds. Dates and date-times without an offset are interpreted as UTC. The value must be between years 1 and 9999.hex: hexadecimal strings with or without a0xprefix in any letter case, and byte values.boolean:true,false,"true", and"false".text,enum,password,secret: strings. Numbers and booleans are compared as their text. Anenumvalue outside the configuredvalueoptions matches no document with==.ip: an IP address string. A CIDR network string with a full-length prefix, such as192.0.2.10/32or2001:db8::1/128, is the same as its address. Other CIDR network strings fail.network: an IP address string or a CIDR network string without host bits. An address is a network with a full-length prefix. For example,subnet == "10.0.0.0/16"matches the stored networks10.0.0.0/8and10.0.0.0/16, but not10.0.0.0/24or10.1.0.0/16.subnet != "10.0.0.0/16"matches10.0.0.0/24and10.1.0.0/16.
When the value expression cannot be converted, the query fails. For example,
count == "abc" fails when count is a number field.
The value expression must have one string, number, boolean, timestamp, byte,
IP address, or network value. Timestamps are compared as RFC3339 strings and
byte values as lowercase hexadecimal strings. The query fails when the value
is missing, such as a request attribute that the request does not contain, and
when it is a JSON null, array, or object.
Where fields without a declared type
A field has no declared type when its type is any, when the collection
declares no fields, or when the field name is a key field name that is not
declared as a payload field. For these fields:
==and!=compare the text of the stored value exactly. Numbers and booleans are compared as their JSON text, so the stored number9equals"9".>,>=,<, and<=with an RFC3339 value expression, such as"2026-01-01T00:00:00Z"or a timestamp value, compare stored RFC3339 strings chronologically. Other stored values do not match.- With a decimal number value expression, stored numbers are compared numerically and stored strings are compared as text by Unicode code point.
- With any other value expression, stored strings are compared as text by Unicode code point. Stored numbers do not match.
- Stored booleans, objects, and arrays never match
>,>=,<, or<=.
Where typed value expressions
Two kinds of value expressions change how == compares for every field type.
Regex literals also change !=:
Regex matching - use a regex literal (delimited by /) as the value. The
filter matches when the text of the stored value matches the regular
expression. With !=, the filter matches when the stored value exists and its
text does not match:
where {
username == /^ali.*$/;
role != /^guest/;
}
A regex literal with any other operator fails the query.
IP containment - when the value expression is an IP address or network
value, such as a RADIUS address attribute, the filter matches when the stored
address or CIDR network contains that address. For a network value, Radiator
uses its network address. A string value on a network field compares the
whole network instead, as described in
Where value conversion:
where {
network == radius.request.attr.NAS-Ip-Address;
}
Where filter validation
When collection is a string literal, Radiator checks each filter at startup.
If the collection declares fields, the filter field must be declared. The
field type must support the operator. A literal value, such as "abc" in
count == "abc", must convert to the field type, and a literal enum value
must be one of the configured value options.
When collection is an expression, Radiator does the same checks when the
query runs and fails the query if a check fails. Values computed when the
query runs, such as request attributes, are always converted when the query
runs.
A literal value that can never be compared, such as none or a regex literal
with >, fails at startup even when collection is an expression.
Result order
By default, a query that can return many documents returns the most recently
updated document first. Use order_by to order the documents by one top-level
field instead. The statement takes the field name and a direction:
query "BY_EXPIRY" {
collection "users";
order_by expires_at asc;
}
asc: the smallest value first.desc: the largest value first.
The field must be declared in the
collection
with one of these types. The order is the same as the order that >, >=,
<, and <= use in a where block:
| Field type | Order |
|---|---|
text, enum, password, secret | Unicode code point. The order is case-sensitive: "Z" sorts before "a". |
number | Numeric. 9 sorts before 10. |
datetime | Chronological. Offsets and fractional seconds are taken into account. |
order_by does not support other field types, fields of type any, or
collections that declare no fields.
Documents that do not have the field sort last in both directions. Documents
whose stored value is not valid for the field type also sort last. Such values
can exist when the field type was changed after the documents were stored.
Documents with equal values are ordered by their key in the same direction.
A query with an exact key cannot have order_by.
Result limit
Use limit to return at most the given number of documents. Together with
order_by, the query returns the documents with the smallest or largest
values:
query "TOP_USAGE" {
collection "usage";
order_by used_bytes desc;
limit 10;
}
The limit value is an expression that Radiator evaluates every time the query
runs. It can read the request or the
backend action arguments, for example
limit args.count;. The value must be an integer, or text that contains an
integer, from 1 to 10000. Otherwise, the query fails. When the value is a
literal, such as limit 10;, Radiator checks it when it loads the
configuration.
Without limit, a query returns every matching document. A query with an
exact key cannot have limit.
Order and limit validation
When collection is a string literal, Radiator checks the order_by field at
startup. The field must be declared, and its type must support ordering. When
collection is an expression, Radiator does the same checks when the query
runs and fails the query if a check fails.
Example: next user to expire
Each user document has an expires_at field. This query finds the user that
expires next among the users that have not expired yet:
collection "users" {
roles {
read "support";
write "ops";
}
key {
field {
name "Username";
type text;
}
}
field "expires_at" {
type datetime;
}
}
query "NEXT_TO_EXPIRE" {
collection "users";
where {
expires_at > datetime.timestamp;
}
order_by expires_at asc;
limit 1;
}
With limit 1, the query returns at most one document, so an
@once pipeline never rejects
because of multiple results.
Example: largest usage in a period
Each usage document holds the number of bytes that one user used in one period. This query finds the 10 users with the largest usage in October 2026:
collection "usage" {
roles {
read "support";
write "ops";
}
key {
field {
name "Username";
type text;
}
field {
name "Period";
type text;
}
}
field "period_start" {
type datetime;
}
field "used_bytes" {
type number;
}
}
query "TOP_USAGE" {
collection "usage";
where {
period_start >= "2026-10-01";
period_start < "2026-11-01";
}
order_by used_bytes desc;
limit 10;
}
Users without a used_bytes value in the period sort last.
put
Defines a write operation that stores a document in a collection. The document content is built from the bindings block.
A put replaces the stored document that has the same key. Fields that the
bindings block does not set are not kept.
Time-series puts are immutable events. Do not configure expire for a
time-series collection.
put "STORE_ACCOUNTING" {
collection "accounting";
key aaa.identity;
key radius.request.attr.acct-session-id;
expire 1h;
bindings {
username = aaa.identity;
attrs = radius.request.attrs;
metadata.source = "radius";
}
}
put key
At least one key directive is required. The key identifies the document within the collection.
A single key expression:
put "STORE_SESSION" {
collection "sessions";
key aaa.identity;
bindings { ... }
}
Multiple key directives create a multipart key:
put "STORE_ACCOUNTING" {
collection "accounting";
key aaa.identity;
key radius.request.attr.acct-session-id;
bindings { ... }
}
put collection
Specifies the target collection. Required.
put expire
Sets a time-to-live for the stored document. After this duration, the document is no longer returned by queries. Accepts duration units.
put "STORE_SHORT" {
collection "sessions";
key aaa.identity;
key radius.request.attr.acct-session-id;
expire 30m;
bindings { ... }
}
If expire is not set, the document persists indefinitely.
put bindings
The bindings block defines the document content as field-value assignments. Only the = operator is allowed.
bindings {
username = aaa.identity;
attrs = radius.request.attrs;
metadata.source = "radius";
metadata.timestamp = datetime.timestamp;
}
Dotted field names (e.g. metadata.source) create nested JSON objects in the stored document.
Multipart Keys
Multipart keys allow hierarchical document organization. See RadiatorDB REST API multipart keys for key formatting in CLI arguments and management API URLs.
Processing query results
The backend action runs a RadiatorDB query and processes its results in an
@once or
@each pipeline. The pipeline has access to
these variables:
docis the current matched document.keyholds the key of the current document. It is a multivalue variable with one value for each key part, in the same order as the stored key. Read a part with a zero-based index, for examplekey[0].argsholds the arguments of thebackendaction.
Read document fields with the
jsonpath filter, for example
doc | jsonpath("$.username"). The doc and key variables are available only
inside the query mappings and the result pipeline.
Radiator completes the database operation, including failover to other
servers, before it runs any pipeline. Radiator then processes the documents one
at a time, in the order the query returns them. A query with key filters, a
where block, or no key returns the most recently updated document first,
unless it has order_by.
The result pipeline starts without a previous result, and Radiator discards the
result of the pipeline. The backend action result depends only on the query.
Changes that the pipeline makes to variables and attributes remain after the
backend action. If an action in the pipeline fails with an error, Radiator
stops processing the results and the backend action fails.
The query parameter of the backend action can be a literal name or a runtime
expression. Radiator validates literal names when it loads the configuration and
names from expressions when the backend action runs. A put operation
does not support @once or @each.
Processing a single result with @once
Use @once when the query must match exactly one document, such as a user
entry. Radiator runs the pipeline once, with the matched document as doc. If
the query matches zero documents or more than one document, the backend action
rejects without running the pipeline. To process only the first document in
an order, use order_by with limit 1;.
This backend action calls the FIND_USER query of the
example. It copies the password and the user name from the document
and its key:
backend {
name "MAIN_DB";
query "FIND_USER";
@once {
user.username = key[0];
user.password = doc | jsonpath("$.password");
}
}
Processing every result with @each
Use @each to run a pipeline for every matched document. In this example, each
user document has a group field:
{ "group": "staff", "vlan": "100" }
The query uses a backend argument to select the users of one group:
query "FIND_GROUP" {
collection "users";
where {
group == args.group;
}
}
The @each pipeline collects the user names from the document keys:
backend {
name "MAIN_DB";
query "FIND_GROUP";
args {
group = "staff";
}
@each {
vars.staff_users += key[0];
}
}
Use continue; to skip the remaining actions for the current document, and
break; to stop processing the remaining documents. See
@each.
Mapping
The mapping block preprocesses each document before the @each or @once
pipeline runs. Radiator runs the mappings for each document with the same doc
and key variables, and the pipeline then uses the mapped values.
Map the fields of each document to vars.* variables in the query, and read
these variables in the pipeline. The pipeline then does not need to know the
document format or use the jsonpath filter. If the document format changes,
update only the query mappings.
This query maps the user name and the VLAN of each user document in the
@each example to vars.username and
vars.vlan:
query "FIND_GROUP" {
collection "users";
where {
group == args.group;
}
mapping {
vars.username = key[0];
vars.vlan = doc | jsonpath("$.vlan");
}
}
Each @each run sees the values mapped from the current document, so the
pipeline uses vars.username and vars.vlan instead of key and doc:
backend {
name "MAIN_DB";
query "FIND_GROUP";
args {
group = "staff";
}
@each {
vars.staff_users += vars.username;
vars.staff_vlans += vars.vlan;
}
}
The mappings also run when the backend action has no @each or @once
pipeline. Variables set by the mappings stay set after each document. With =,
each document overwrites the value, so after the backend action the variable
holds the value of the last document. With +=, the values of all documents
accumulate.
If a mapping fails, for example because a mapped value is not valid for the
target variable, Radiator stops processing the documents and the backend
action fails. Changes made for the earlier documents remain. With @once, the
mappings do not run when the query matches more than one document.
Mapping without a result pipeline
This query collects the names of all devices of a site. The device documents use multipart keys where the first part is the site and the second part is the device name:
query "FIND_SITE_DEVICES" {
collection "devices";
key {
part vars.site;
at 0;
}
mapping {
vars.devices += key[1];
}
}
After the backend action, vars.devices holds the names of all matched
devices:
backend {
name "MAIN_DB";
query "FIND_SITE_DEVICES";
}
Rejecting
The backend action rejects in these cases:
- Exact key lookup: No document has the key. Radiator sets
aaa.reasontoKey not found: <collection>/<key>. - Key filters,
whereblock, or no key: No document matches. Radiator setsaaa.reasontoNo documents found from collection: <collection>. @oncewith many documents: More than one document matches. Radiator setsaaa.reasonto:RadiatorDB query '<query>' @once expected exactly one result, but found <count> documents from collection: <collection>.
When the backend action rejects, Radiator does not run the mappings or the
@each or @once pipeline. Otherwise, the backend action accepts.
Storing documents from AAA pipelines
Use the backend action to run a put operation. This handler stores
an accounting document for each accounting request:
aaa {
policy "RADIUS" {
conditions all {
aaa.protocol == protocol.RADIUS;
}
handler "ACCOUNTING" {
conditions all {
radius.request.code == radius.ACCOUNTING_REQUEST;
}
@execute {
backend {
name "MAIN_DB";
query "STORE_ACCOUNTING";
}
accept;
}
}
}
}
Revision Retention
RadiatorDB stores revision history for each document insert, update, and delete. By default, RadiatorDB does not remove archived revisions. Add a collection revisions block to configure archived revision cleanup limits:
collection "users" {
roles {
read "support";
write "ops";
}
revisions {
age 30d;
max 100;
}
key {
field {
name "Username";
type text;
}
}
}
The revisions block supports these parameters:
ageremoves archived revisions older than the configured duration. If omitted, RadiatorDB does not remove revisions based on age.maxkeeps at most this many archived revisions for each document key. If omitted, RadiatorDB does not remove revisions based on count.
Both limits are enforced by RadiatorDB garbage collection. RadiatorDB can remove a revision when it is older than age or when the document key has more than max retained revisions.
If you omit the revisions block, garbage collection keeps all archived revisions for the collection. This does not change tombstone garbage collection.
Garbage Collection
RadiatorDB garbage collection removes old tombstones. It also removes old archived revisions from normal collections that configure a revisions block. A tombstone is the delete marker that RadiatorDB keeps after a document is deleted so that deletion can replicate to other RadiatorDB nodes.
Configure scheduled garbage collection in the backend gc block:
radiatordb "mydb" {
gc {
enable true;
max_tombstone_age 30d;
interval {
duration 1h;
random 5m;
}
}
server "primary" {
host "db.example.com";
database "radiator";
username "radiator";
}
collection "users" {
roles {
read "support";
write "ops";
}
key {
field {
name "Username";
type text;
}
}
}
}
The gc block supports these parameters:
enablecontrols whether scheduled garbage collection runs. The default istrue.max_tombstone_ageremoves tombstones older than the configured duration. The default is30d.intervaldefines one or more schedules. The default isduration 1hwithrandom 5mjitter. See interval for the shared interval syntax.
If the gc block is omitted, RadiatorDB uses these defaults and scheduled garbage collection is enabled.
Use only one scheduled GC runner for a shared RadiatorDB deployment. If multiple Radiator Server processes use the same RadiatorDB backend, enable scheduled GC on one process and disable it on the others. This avoids duplicate maintenance work against the same PostgreSQL databases.
You can keep the same configuration file on all servers by reading enable from an environment variable:
radiatordb "mydb" {
gc {
enable env.RADIATORDB_GC_ENABLE | default("false");
max_tombstone_age 30d;
}
# Other backend settings are identical on every Radiator Server.
}
Set RADIATORDB_GC_ENABLE=true for exactly one Radiator Server process. Leave it unset, or set it to false, on the other processes. See Environment Variables for more information about env.VAR_NAME configuration values.
Storage Statistics
RadiatorDB counts the stored rows on each PostgreSQL node every five minutes and publishes the counts as statistics. Use them to see how much data each node holds and which collection grows. Each node stores its own copy of the data, so do not add the counts of different nodes together.
Each node publishes its totals in the namespace backend::RadiatorDB::<backend>::node::<server>. Each collection on that node publishes its counts in backend::RadiatorDB::<backend>::node::<server>::collection::<collection>. <backend> is the name of the radiatordb block, <server> is the name of a server block, and <collection> is the name of a collection block.
| Statistic | Description |
|---|---|
DocumentCount | Current documents that are neither deleted nor expired. |
TombstoneCount | Deleted or expired documents that garbage collection has not removed yet. |
RevisionCount | Archived revisions. See Revision Retention. |
A collection without rows reports 0. The node namespace also contains the StoragePolls and StoragePollFailures counters, which count completed and failed storage counts.
For example, read the live document count of the users collection on the primary server of the mydb backend:
GET /api/v1/statistics/counter/backend/RadiatorDB/mydb/node/primary/collection/users/DocumentCount
Performance Tuning
synchronous_commit
Set synchronous_commit in the postgresql block to control when PostgreSQL
acknowledges commits for all servers in this backend:
postgresql {
synchronous_commit off;
}
onwaits for PostgreSQL to durably flush the WAL before acknowledging.offacknowledges before the local WAL flush. It improves throughput for loss-tolerant data such as accounting logs, but a host crash can lose recent acknowledged transactions.
RadiatorDB does not override this setting by default. If you omit the parameter,
PostgreSQL uses its configured value and defaults to on. This parameter does
not make RadiatorDB logical replication synchronous.
Internal timing environment variables
Radiator Server reads these variables when it starts a RadiatorDB backend.
They adjust internal timing and retry policies without adding RadConf parameters.
These variables are internal and may change without notice.
Duration values use the same duration syntax as
setup_timeout. Attempt counts are positive integers. Invalid values prevent
the backend from starting.
| Variable | Default | Purpose |
|---|---|---|
RADIATORDB_RECOVERY_PEER_PROBE_TIMEOUT | 500ms | Maximum time to inspect one recovery peer, capped by the remaining setup deadline. |
RADIATORDB_WRITE_GATE_ACQUISITION_TIMEOUT | 30s | Maximum wait for each cluster write gate, capped by the setup deadline when one applies. |
RADIATORDB_MAINTENANCE_LOCK_TIMEOUT | 30s | PostgreSQL wait for each collection topology or table lock during cluster maintenance. |
RADIATORDB_NODE_REMOVAL_TIMEOUT | 30s | Shared deadline for bounded connection, lock, subscription and replication slot drop, and replication catch-up waits during Remove Node. Independent of setup_timeout; not a deadline for the entire request or every cleanup query. Forced removal does not wait for replication catch-up. |
RADIATORDB_COLLECTION_DESTROY_TIMEOUT | 30s | Shared deadline for the node connections, topology and write gates, replication catch-up, and lock waits of one collection destroy. Each node connection also waits at most health_check_timeout, and each lock wait at most RADIATORDB_MAINTENANCE_LOCK_TIMEOUT. The destroy fails and destroys nothing when replication between the reachable nodes does not catch up within this deadline, with or without force. |
RADIATORDB_BACKUP_IMPORT_LOCK_TIMEOUT | 30s | PostgreSQL lock wait during backup import. |
RADIATORDB_HEARTBEAT_QUERY_TIMEOUT | 2s | Maximum time for one replication heartbeat query, and for each diagnostic query that inspects replication state after a failed node setup or removal catch-up. |
RADIATORDB_HEARTBEAT_INTERVAL | 5s | Delay between completed steady replication monitoring passes. Each publisher emits at most one heartbeat per interval. |
RADIATORDB_HEARTBEAT_FOLLOW_UP_INTERVAL | 250ms | Delay before an extra monitoring pass while a replication edge waits for its subscriber to apply the latest heartbeat. Extra passes run only within one heartbeat interval after the monitor starts, a node enters routing, or a replication edge becomes ready. |
RADIATORDB_HEARTBEAT_LAG_THRESHOLD | 15s | Raw heartbeat distance above which replication is lagging. Must be greater than RADIATORDB_HEARTBEAT_INTERVAL. |
RADIATORDB_REPLICATION_OBSERVATION_STALE_AFTER | 30s | Age above which a replication observation no longer supports readiness. Must be greater than RADIATORDB_HEARTBEAT_INTERVAL. |
RADIATORDB_DEFAULT_NODE_OBSERVATION_STALE_AFTER | 30s | Node observation age before managed startup takes over. Managed startup derives it from lifecycle. |
RADIATORDB_SLOT_DROP_TIMEOUT | 2s | Deadline for retrying an active replication slot drop. |
RADIATORDB_BACKEND_TERMINATION_TIMEOUT | 1s | PostgreSQL wait for an active replication sender to terminate. |
RADIATORDB_TOPOLOGY_GATE_RELEASE_TIMEOUT | 5s | Maximum wait to release each held cluster-wide lock on one database, such as a replication topology gate or a cluster write gate. |
RADIATORDB_SUBSCRIPTION_POLL_INTERVAL | 100ms | Delay between subscription catch-up and synchronization checks. |
RADIATORDB_SUBSCRIPTION_READY_MAX_ATTEMPTS | 100 | Maximum subscription-readiness polls during explicit synchronization. |
RADIATORDB_REPLICATION_FLUSH_MAX_ATTEMPTS | 100 | Maximum WAL-flush polls during explicit synchronization. |
RADIATORDB_POOL_PRUNER_MAX_INTERVAL | 5s | Maximum interval between idle connection pruning checks. |
RADIATORDB_PARTITION_RECONCILIATION_INTERVAL | 20m | Base interval between partition reconciliation passes. |
RADIATORDB_PARTITION_RECONCILIATION_MAX_JITTER | 20m | Maximum additional random delay between reconciliation passes; 0s disables jitter. |
RADIATORDB_RETRY_INITIAL_DELAY | 1s | Initial nominal lifecycle retry delay, capped by lifecycle.retry-max-delay. |
RADIATORDB_RETRY_JITTER_MIN_PERCENT | 50 | Minimum percentage of nominal delay used for randomized retries; valid range is 0 to 100. |
RADIATORDB_NODE_INSPECTION_TIMEOUT | 5s | Budget for inspecting one physical node through the maintenance API. |
RADIATORDB_NODE_METRICS_TIMEOUT | 5s | Budget for sampling one physical node through the maintenance API. |
RADIATORDB_METRICS_FAST_INTERVAL | 30s | Delay after each completed backend statistics poll before the next poll. |
RADIATORDB_METRICS_STORAGE_INTERVAL | 5m | Delay after each completed physical storage-count poll before the next poll. |
RADIATORDB_METRICS_POLL_TIMEOUT | 5s | Maximum time for one node's backend statistics or storage-count poll. |
RADIATORDB_MAINTENANCE_LOCK_TIMEOUT and
RADIATORDB_BACKUP_IMPORT_LOCK_TIMEOUT accept whole milliseconds from 1ms
through 2147483647ms. RADIATORDB_BACKEND_TERMINATION_TIMEOUT also
requires whole milliseconds.
Polling attempt counts include database query time, so they are not wall-clock
deadlines. To set setup_timeout, the lifecycle values, or the server
connection idle timeout from environment variables, use RadConf env.VAR
expressions in those existing settings.