KDB knowledge

Health, info, check and backup over HTTP

Check that the server is alive, read the tables, disk use and health of a database, look for damage, make and list backups, watch deferred trigger queues, and read the server's memory, processor and settings.

These routes tell you how the server and a database are doing. The web console uses them for its table list and health box.

GET /health

Syntax:

GET /health

Answers when the server is running. Use it to check that KDB is up (a monitoring script, a load balancer, a start script that waits for the server).

ParameterWhereMeaningDefault
(none)-No database, no login-
Answer fieldMeaning
statusalways "ok": the server is running
readyfalse while the server still opens the default database after a start (a long log replay after a crash, a slow disk); true after that. Requests for that database wait until it is open; other databases answer at once
startingonly while ready is false: what it is doing, like opening the default database 'default': replaying the log, 12 s so far
versionthe KDB version
server_time_msthe server clock: milliseconds since 1970-01-01 UTC
auth_requiredtrue when other requests need a token or a login
logintrue when the data folder has users (the console then shows a login form instead of a token box)

HTTP: Is the server alive?

GET /health

Response:

HTTP 200 (application/json)

{
  "status": "ok",
  "version": "0.2.0-dev",
  "server_time_ms": 1792647852082,
  "auth_required": false,
  "login": false,
  "tls": false,
  "ready": true,
  "standby": false
}
curl -s http://127.0.0.1:9900/health
# Wait until a server that is starting answers (at most 10 seconds).
foreach ($i in 1..50) {
  try { $h = Invoke-RestMethod http://127.0.0.1:9900/health; "KDB $($h.version) is up"; break }
  catch { Start-Sleep -Milliseconds 200 }
}
import requests

try:
    h = requests.get("http://127.0.0.1:9900/health", timeout=3).json()
    print("up, version", h["version"])
except requests.RequestException:
    print("KDB is down")

GET /api/info

Syntax:

GET /api/info[?db=<name>]

Everything about one database: its tables and columns, sizes, damage, what happened when it was opened, and the last backup.

ParameterWhereMeaningDefault
dbURLThe database (or the header X-KDB-Database)the default database
Answer fieldMeaning
databaseThe database name
dirIts folder
versionThe KDB version
opened_msWhen the server opened the database (ms since 1970, UTC)
server_time_msThe server clock
last_lsnThe number of the last change in the log (the LSN, log sequence number). It grows with every change.
data_files, data_bytesCount and size of the data files
tables[]One object per table: name, kind (timeseries or relational), partition (NONE, DAY, MONTH, YEAR), timestamp_column, rows, bytes, columns[], create_sql
tables[].columns[]name, type, primary_key, not_null, computed (a column made from others), references (the table and column it points to, or empty)
damaged[]Damaged files KDB knows about; empty when all is well
recovery_summaryWhat KDB did when it opened the database (for example after a crash)
warningA warning when the data is far bigger than --memory; "" when all is well
backup_dirThe backup folder of this database (empty when backups are off)
last_backupThe newest backup (same fields as POST /api/backup), or null
auth_requiredtrue when requests need a token or login
read_onlytrue when this database only reads
protectionThe server's protection settings: statement_timeout_ms, max_result_bytes, max_groups, max_queries_per_user, safe_updates, running (statements running now), server_read_only, audit
backup_scheduleThe backup schedule and its last run, or null without a schedule
backup_commandThe copy command run after each backup and its last run, or null without one
timersHow many timers of this database are on

HTTP: A small database: one table

Setup:

CREATE TABLE brokers (id INT PRIMARY KEY, name STRING NOT NULL);
INSERT INTO brokers VALUES (1, 'Keystone Securities'), (34, 'Vision Securities');
GET /api/info

Response:

HTTP 200 (application/json)

{
  "database": "demo",
  "dir": "kdb-data/demo",
  "version": "0.2.0-dev",
  "opened_ms": 1792647852097,
  "server_time_ms": 1792647852145,
  "last_lsn": 4,
  "data_files": 1,
  "data_bytes": 231,
  "tables": [
    {
      "name": "brokers",
      "kind": "relational",
      "partition": "NONE",
      "timestamp_column": "",
      "rows": 2,
      "bytes": 0,
      "columns": [
        {
          "name": "id",
          "type": "INT",
          "primary_key": true,
          "not_null": false,
          "computed": false,
          "references": ""
        },
        {
          "name": "name",
          "type": "STRING",
          "primary_key": false,
          "not_null": true,
          "computed": false,
          "references": ""
        }
      ],
      "create_sql": "CREATE TABLE brokers (\n  id INT PRIMARY KEY,\n  name STRING NOT NULL\n)"
    }
  ],
  "damaged": [],
  "recovery_summary": "manifest copy: -, last shutdown: clean\nreplayed 0 transactions after LSN 0, last LSN 0\nopened in 1 ms: manifest 1 ms, 0 files read 0 ms, log check 0 ms, replay 0 ms (0 transactions), clean-up 0 ms",
  "warning": "",
  "backup_dir": "./backups/demo",
  "last_backup": null,
  "auth_required": false,
  "read_only": false,
  "protection": {
    "statement_timeout_ms": 60000,
    "max_result_bytes": 1073741824,
    "max_groups": 5000000,
    "max_queries_per_user": 32,
    "safe_updates": false,
    "running": 1,
    "server_read_only": false,
    "audit": true
  },
  "backup_schedule": null,
  "backup_command": null,
  "timers": 0
}
$info = Invoke-RestMethod "http://127.0.0.1:9900/api/info?db=market"
"log LSN: $($info.last_lsn)   damaged files: $($info.damaged.Count)"
$info.tables | Select-Object name, kind, partition, timestamp_column, rows | Format-Table -AutoSize
const info = await (await fetch("http://127.0.0.1:9900/api/info?db=market")).json();
for (const t of info.tables) console.log(t.name, t.kind, t.rows, t.columns.map((c) => `${c.name} ${c.type}`).join(", "));

GET /api/disk

Syntax:

GET /api/disk[?db=<name>]

What one database takes on disk, part by part. The console's Keep data page shows it. It walks the trash folder, so it is a separate route and not part of /api/info.

ParameterWhereMeaningDefault
dbURLThe database (or the header X-KDB-Database)the default database
Answer fieldMeaning
databaseThe database name
data_files, data_bytesThe current version of every file
tail_files, tail_bytesOf those: tail files, rows appended after a partition's file and not yet folded back into it
delta_files, delta_bytesOf those: delta files, rows of a partition replaced or deleted and not yet compacted into it
previous_files, previous_bytesOlder versions kept for repairs (PURGE VERSIONS removes them)
trash_folders, trash_bytesReplaced files waiting in _trash to be removed (PURGE TRASH)
wal_bytesThe log (the WAL)
wal_archive_bytesOld log kept for repairs and restores (WAL_ARCHIVE)
total_bytesAll of the above together
trash_purged_bytesWhat the database removed from its trash by itself since it opened

HTTP: Disk use of a small database

Setup:

CREATE TABLE brokers (id INT PRIMARY KEY, name STRING NOT NULL);
INSERT INTO brokers VALUES (1, 'Keystone Securities'), (34, 'Vision Securities');
GET /api/disk

Response:

HTTP 200 (application/json)

{
  "database": "demo",
  "data_files": 1,
  "data_bytes": 231,
  "tail_files": 0,
  "tail_bytes": 0,
  "delta_files": 0,
  "delta_bytes": 0,
  "previous_files": 0,
  "previous_bytes": 0,
  "trash_folders": 0,
  "trash_bytes": 0,
  "wal_bytes": 1048640,
  "wal_archive_bytes": 0,
  "total_bytes": 1048871,
  "trash_purged_bytes": 0
}

GET /api/trigger-queue

Syntax:

GET /api/trigger-queue[?db=<name>][&trigger=<name>][&wait=<time>]

The queues of the database's deferred triggers, and every job that has not finished yet. Use it to see the lag after a load, or to wait until the work of a load is done.

ParameterWhereMeaningDefault
dbURLThe database (or the header X-KDB-Database)the default database
triggerURLOnly this triggerevery deferred trigger
waitURLHold the answer until the jobs finished, or stand still behind a failed job, for at most this long (500ms, 30s, 10min)no wait
Answer fieldMeaning
queues[]One object per deferred trigger (below)
jobs[]Every job not finished yet: id, trigger, table, status, rows, commit_lsn, made_ms, took_ms, error

Each queues object:

FieldMeaning
trigger, table, enabledThe trigger, its table, and whether it is enabled
timeout_msIts DEFERRED TIMEOUT; 0 = the statement timeout
pendingJobs not finished: waiting, running and failed
oldest_pending_age_msHow long the oldest of those has waited: the lag. 0 = none.
running_job, running_since_msThe job running now (same fields as jobs[]), or null
failed_job, failed_msThe job that failed and stopped the queue, or null (recovery)
last_done_jobThe newest job that finished (done or skipped)

A job's status is waiting, running, done, skipped, failed, or blocked (waiting behind a failed job).

HTTP: One deferred trigger, after a load

Setup:

CREATE TABLE trades (id INT, qty INT);
CREATE TABLE seen (id INT);
CREATE TRIGGER copy_ids AFTER INSERT ON trades DEFERRED EXECUTE INSERT INTO seen SELECT id FROM NEW;
INSERT INTO trades VALUES (1, 10), (2, 20);
GET /api/trigger-queue?wait=10s

Response:

HTTP 200 (application/json)

{
  "queues": [
    {
      "trigger": "copy_ids",
      "table": "trades",
      "enabled": true,
      "timeout_ms": 0,
      "pending": 0,
      "oldest_pending_age_ms": 0,
      "running_job": null,
      "running_since_ms": 0,
      "failed_job": null,
      "failed_ms": 0,
      "last_done_job": 1,
      "failures": 0,
      "retries": 0,
      "refused_writes": 0
    }
  ],
  "jobs": []
}

HTTP: An unknown trigger

GET /api/trigger-queue?trigger=nope

Response:

HTTP 404 (application/json)

{"error": "deferred trigger 'nope' does not exist"}

POST /api/check

Syntax:

POST /api/check[?db=<name>]

Reads every block of every data file and of the log, and checks each against its checksum (a small number that changes when the data is damaged). Use it to find damage early.

ParameterWhereMeaningDefault
dbURLThe database (or the header X-KDB-Database)the default database
Answer fieldMeaning
oktrue when nothing is damaged
files_checkedData files read
bytes_checkedBytes read
wal_segments_checkedLog files read (the WAL, write-ahead log, where every change is written first)
problems[]One text for each problem found; empty when ok
elapsed_usTime it took, in microseconds

HTTP: Check the sample database (sample data: sample)

POST /api/check

Response:

HTTP 200 (application/json)

{
  "ok": true,
  "files_checked": 14,
  "bytes_checked": 77929,
  "wal_segments_checked": 1,
  "problems": [],
  "elapsed_us": 650
}

HTTP: GET is not allowed: checking is work for the server

GET /api/check

Response:

HTTP 405 (application/json)

{"error": "method GET is not allowed here"}

POST /api/backup

Syntax:

POST /api/backup[?db=<name>]

Makes a verified backup of the database while the server keeps running: it copies the files into a new folder and reads the copy back to check it.

ParameterWhereMeaningDefault
dbURLThe database (or the header X-KDB-Database)the default database
Answer fieldMeaning
nameThe backup's folder name: the time, like 20260911-081748-398
dirThe backup folder: <backup folder>/<database>/<name>
created_ms, createdWhen it was made: ms since 1970, and as UTC text
end_lsnThe last change in the backup
checkpoint_lsnThe last change already in the data files when it was made
files, bytesCount and size of the copied files

HTTP: Make a backup, then list the backups (sample data: sample)

POST /api/backup
###
GET /api/backups

Response:

POST /api/backup

HTTP 200 (application/json)

{
  "name": "20261022-054412-215",
  "dir": "./backups/demo/20261022-054412-215",
  "created_ms": 1792647852215,
  "created": "2026-10-22T05:44:12.215Z",
  "end_lsn": 30,
  "checkpoint_lsn": 30,
  "files": 9,
  "bytes": 194519
}

GET /api/backups

HTTP 200 (application/json)

{
  "backups": [
    {
      "name": "20261022-054412-215",
      "dir": "./backups/demo/20261022-054412-215",
      "created_ms": 1792647852215,
      "created": "2026-10-22T05:44:12.215Z",
      "end_lsn": 30,
      "checkpoint_lsn": 30,
      "files": 9,
      "bytes": 194519
    }
  ],
  "backup_dir": "./backups/demo",
  "point_in_time": {
    "possible": true,
    "from_ms": 1792647852215,
    "from": "2026-10-22T05:44:12.215Z",
    "from_backup": "20261022-054412-215",
    "log_first_lsn": 1,
    "why": "",
    "wal_archive_max_bytes": 1073741824
  }
}
# daily-backup.ps1: a backup and a check of a running server (run it with Task Scheduler).
$r = Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:9900/api/backup?db=market"
"backup $($r.name): $($r.files) files, $($r.bytes) bytes"
$c = Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:9900/api/check?db=market"
if (-not $c.ok) { Write-Output "CHECK FOUND A PROBLEM: $($c.problems -join '; ')"; exit 2 }
"check ok: $($c.files_checked) files"
# Linux cron, on a server with users: an admin user for that database.
curl -s -u admin:"$(cat /root/kdb-admin-password)" -X POST "http://127.0.0.1:9900/api/backup?db=shop"

GET /api/backups

Syntax:

GET /api/backups[?db=<name>]

Lists the backups of the database, newest first.

ParameterWhereMeaningDefault
dbURLThe database (or the header X-KDB-Database)the default database
Answer fieldMeaning
backups[]One object per backup, with the fields of POST /api/backup: name, dir, created_ms, created, end_lsn, checkpoint_lsn, files, bytes
backup_dirThe folder they are in ("" when backups are off)
point_in_time.possibletrue when a restore up to a time works now
point_in_time.from_ms, fromThe earliest time it can go back to (the time of from_backup)
point_in_time.from_backupThe oldest backup the kept log still reaches
point_in_time.log_first_lsnThe oldest log record the database still keeps
point_in_time.whyWhen possible is false: what to change
point_in_time.wal_archive_max_bytesThe old log this database may keep (wal_archive)

HTTP: No backups yet

GET /api/backups

Response:

HTTP 200 (application/json)

{
  "backups": [],
  "backup_dir": "./backups/demo",
  "point_in_time": {
    "possible": false,
    "from_ms": 0,
    "from": "",
    "from_backup": "",
    "log_first_lsn": 0,
    "why": "there is no backup yet: a point-in-time restore starts from one",
    "wal_archive_max_bytes": 1073741824
  }
}
import requests

for b in requests.get("http://127.0.0.1:9900/api/backups", params={"db": "market"}).json()["backups"]:
    print(b["created"], b["files"], "files", b["bytes"], "bytes")

POST /api/restore

Syntax:

POST /api/restore[?db=<name>]
body: {"as": "<new database>", "database": "<name>", "backup": "<backup name>", "until": "<time>" | "latest", "until_ms": <ms>}

Restores a backup of a database into a new database while the server runs. The database itself is not touched. See restore while the server runs.

FieldMeaningDefault
asThe name of the new database. It must not exist.required
databaseWhose backup (or db= / X-KDB-Database)the default database
backupOne backup's name, from GET /api/backupsthe newest one made before until, else the newest
untilReplay the database's own log up to this time: 2026-09-24 14:05:09 (UTC unless it ends in Z or an offset like +05:45). latest = all of the log.the backup as it was
until_msThe same time in ms since 1970
Answer fieldMeaning
database, sourceThe new database and the one whose backup it is
backupThe backup used (fields of POST /api/backup)
until_ms, used_logThe time asked for (null = none), and whether the log was replayed
opened, open_errorThe new database was opened and its log replayed; false with the reason if not
notesWhat the restore did, like stopped at the requested time: changes after LSN 36 are not restored
took_msHow long it took
nextWhat to do next

HTTP: A backup, then a restore of it into a new database (sample data: sample)

POST /api/backup
###
POST /api/restore
Content-Type: application/json

{"as": "demo_restored"}
###
DELETE /api/databases/demo_restored

Response:

POST /api/backup

HTTP 200 (application/json)

{
  "name": "20261022-054412-388",
  "dir": "./backups/demo/20261022-054412-388",
  "created_ms": 1792647852388,
  "created": "2026-10-22T05:44:12.388Z",
  "end_lsn": 30,
  "checkpoint_lsn": 30,
  "files": 9,
  "bytes": 194519
}

POST /api/restore

HTTP 200 (application/json)

{
  "database": "demo_restored",
  "source": "demo",
  "backup": {
    "name": "20261022-054412-388",
    "dir": "./backups/demo/20261022-054412-388",
    "created_ms": 1792647852388,
    "created": "2026-10-22T05:44:12.388Z",
    "end_lsn": 30,
    "checkpoint_lsn": 30,
    "files": 9,
    "bytes": 194519
  },
  "until_ms": null,
  "used_log": false,
  "opened": true,
  "open_error": "",
  "notes": [
    "restored 9 data files from ./backups/demo/20261022-054412-388; the log (up to LSN 30) is replayed on first open"
  ],
  "took_ms": 98,
  "next": "check 'demo_restored', then POST /api/databases/swap {\"database\":\"demo\",\"with\":\"demo_restored\"} to put it in place, or DROP DATABASE demo_restored"
}

DELETE /api/databases/demo_restored

HTTP 200 (application/json)

{
  "name": "demo_restored",
  "dropped": true,
  "moved_to": "kdb-data/_trash/1792647852627-demo_restored"
}

HTTP: The name is taken

POST /api/backup
###
POST /api/restore
Content-Type: application/json

{"as": "demo_twice"}
###
POST /api/restore
Content-Type: application/json

{"as": "demo_twice"}

Response:

POST /api/backup

HTTP 200 (application/json)

{
  "name": "20261022-054412-423",
  "dir": "./backups/demo/20261022-054412-423",
  "created_ms": 1792647852423,
  "created": "2026-10-22T05:44:12.423Z",
  "end_lsn": 0,
  "checkpoint_lsn": 0,
  "files": 0,
  "bytes": 64
}

POST /api/restore

HTTP 200 (application/json)

{
  "database": "demo_twice",
  "source": "demo",
  "backup": {
    "name": "20261022-054412-423",
    "dir": "./backups/demo/20261022-054412-423",
    "created_ms": 1792647852423,
    "created": "2026-10-22T05:44:12.423Z",
    "end_lsn": 0,
    "checkpoint_lsn": 0,
    "files": 0,
    "bytes": 64
  },
  "until_ms": null,
  "used_log": false,
  "opened": true,
  "open_error": "",
  "notes": [
    "restored 0 data files from ./backups/demo/20261022-054412-423; the log (up to LSN 0) is replayed on first open"
  ],
  "took_ms": 44,
  "next": "check 'demo_twice', then POST /api/databases/swap {\"database\":\"demo\",\"with\":\"demo_twice\"} to put it in place, or DROP DATABASE demo_twice"
}

POST /api/restore

HTTP 409 (application/json)

{"error": "database 'demo_twice' already exists; restore into a new name, then swap or drop"}

POST /api/databases/swap

Syntax:

POST /api/databases/swap[?db=<name>]
body: {"database": "<name>", "with": "<other name>"}

The two databases trade their data, by renaming their folders. Everything KDB keeps by name stays with the name: users' rights, READ ONLY, the dialect, WAL_ARCHIVE, timers and pins. So after a restore into market_check, a swap of market with market_check puts the restored data under market for everyone who uses it, and the old data is in market_check.

HTTP: Swap a database with a restored copy, then swap back

POST /exec
Content-Type: text/plain

CREATE TABLE t (a INT); INSERT INTO t VALUES (1)
###
POST /api/backup
###
POST /exec
Content-Type: text/plain

INSERT INTO t VALUES (2)
###
POST /api/restore
Content-Type: application/json

{"as": "demo_old"}
###
POST /api/databases/swap
Content-Type: application/json

{"with": "demo_old"}
###
POST /exec
Content-Type: text/plain

SELECT count(*) FROM t
###
POST /api/databases/swap
Content-Type: application/json

{"with": "demo_old"}
###
DELETE /api/databases/demo_old

Response:

POST /exec

HTTP 200 (application/json)

{
  "query": "CREATE TABLE t (a INT); INSERT INTO t VALUES (1)",
  "results": [
    {
      "statement": "CREATE TABLE t (\n  a INT\n)",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "table 't' created",
      "timings": {"parse_us": 20, "execute_us": 20219}
    },
    {
      "statement": "INSERT INTO t VALUES (1)",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 1,
      "message": "1 row(s) inserted",
      "timings": {"parse_us": 20, "execute_us": 3141}
    }
  ],
  "columns": [],
  "dataset": [],
  "count": 0,
  "timings": {"parse_us": 40, "execute_us": 23360}
}

POST /api/backup

HTTP 200 (application/json)

{
  "name": "20261022-054412-514",
  "dir": "./backups/demo/20261022-054412-514",
  "created_ms": 1792647852514,
  "created": "2026-10-22T05:44:12.514Z",
  "end_lsn": 4,
  "checkpoint_lsn": 4,
  "files": 2,
  "bytes": 707
}

POST /exec

HTTP 200 (application/json)

{
  "query": "INSERT INTO t VALUES (2)",
  "results": [
    {
      "statement": "INSERT INTO t VALUES (2)",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 1,
      "message": "1 row(s) inserted",
      "timings": {"parse_us": 19, "execute_us": 26}
    }
  ],
  "columns": [],
  "dataset": [],
  "count": 0,
  "timings": {"parse_us": 19, "execute_us": 26}
}

POST /api/restore

HTTP 200 (application/json)

{
  "database": "demo_old",
  "source": "demo",
  "backup": {
    "name": "20261022-054412-514",
    "dir": "./backups/demo/20261022-054412-514",
    "created_ms": 1792647852514,
    "created": "2026-10-22T05:44:12.514Z",
    "end_lsn": 4,
    "checkpoint_lsn": 4,
    "files": 2,
    "bytes": 707
  },
  "until_ms": null,
  "used_log": false,
  "opened": true,
  "open_error": "",
  "notes": [
    "restored 2 data files from ./backups/demo/20261022-054412-514; the log (up to LSN 4) is replayed on first open"
  ],
  "took_ms": 64,
  "next": "check 'demo_old', then POST /api/databases/swap {\"database\":\"demo\",\"with\":\"demo_old\"} to put it in place, or DROP DATABASE demo_old"
}

POST /api/databases/swap

HTTP 200 (application/json)

{"swapped": true, "database": "demo", "with": "demo_old"}

POST /exec

HTTP 200 (application/json)

{
  "query": "SELECT count(*) FROM t",
  "results": [
    {
      "statement": "SELECT count(*) FROM t",
      "columns": [
        {"name": "count(*)", "type": "LONG"}
      ],
      "dataset": [
        [1]
      ],
      "count": 1,
      "truncated": false,
      "rows_affected": 0,
      "message": "",
      "timings": {"parse_us": 27, "execute_us": 144}
    }
  ],
  "columns": [
    {"name": "count(*)", "type": "LONG"}
  ],
  "dataset": [
    [1]
  ],
  "count": 1,
  "timings": {"parse_us": 27, "execute_us": 144}
}

POST /api/databases/swap

HTTP 200 (application/json)

{"swapped": true, "database": "demo", "with": "demo_old"}

DELETE /api/databases/demo_old

HTTP 200 (application/json)

{
  "name": "demo_old",
  "dropped": true,
  "moved_to": "kdb-data/_trash/1792647852777-demo_old"
}

GET /api/stats

Syntax:

GET /api/stats

What the server is doing right now: the memory and processor of the process, the statements running this second, and the memory and cache counters of every open database. Everything here is a counter already in memory, so it is cheap enough to ask for every couple of seconds - no file is read and no database is opened to answer it. The Monitor page of the console shows it.

Answer fieldMeaning
versionthe KDB version
server_time_msthe server's clock (ms since 1970, UTC)
processsee the table below
runningone object per statement running now (HTTP and the PostgreSQL port together)
can_canceltrue when the server was started with query protection, so DELETE /exec/<id> works
lanespriority lanes: enabled, heavy_slots, max_waiting (HTTP places in line), http_waiting, occupied (heavy slots in use), long_normal, heavy_ms, fast_ms, long_ms, shapes, heavy_shapes, cpu_priority, read_slots (read slots: slots, slot_ms, used, waiting, waited, wait_ms_total, wait_ms_max), and per lane (fast, normal, heavy) running, waiting, entered, waited, wait_ms_total, wait_ms_max, last_wait_ms, refused, overflowed (heavy by history, HTTP line full: ran at once), stopped_waiting. Each object of running has lane and waiting too.
zone_partition_skips, zone_block_skipssince the process started: partitions not read and blocks not scanned because their saved column statistics (min and max) ruled the WHERE out
slice_readssince the process started: partitions not in memory of which a one-symbol query read only that symbol's rows from the file (see slice reads)
slice_cache_hitsof those, slices found in the slice cache instead of the file
slice_cache_byteswhat the slice cache holds now (at most KDB_SLICE_CACHE_MB)
searchsince the process started, all databases: bm25_evaluations (full-text ranks worked out), top_k_searches (ORDER BY search_rank(...) DESC LIMIT n answered from the index), skipped_blocks and skipped_docs (rows those searches skipped because they could not make the top n; see search_rank); segments_written, segment_extensions, segments_merged, segments_loaded, segment_rows_loaded, segments_damaged, segments_stale and rows_indexed (segments on disk)
opening_databasesone object per database a request is opening right now
databasesone object per open database; a database opens the first time it is used
warningssentences an operator should read now, one per problem. Today: a database far bigger than --memory (see memory). [] when all is well

process:

FieldMeaning
resident_bytesmemory the process holds in RAM now (Windows: working set; Linux: VmRSS)
peak_resident_bytesthe highest that has been since the process started
virtual_bytesmemory it asked for that is its own (Windows: private bytes, which can be smaller than resident; Linux: VmSize, which is larger)
cpu_msprocessor time used since the process started, user and kernel together
uptime_mshow long the process has been up
threads, open_filesthreads and open handles now
cpu_countprocessors the process may use: inside docker --cpus=2 this is 2, not the host's count (self-tuning)
heap_bytes, heap_objectswhat KDB's own objects hold now
allocatorthe C library's allocator: glibc (the Docker image), windows or other
allocator_retained_bytesresident_bytes - heap_bytes: memory that was freed but is still kept by the allocator, plus the program and its thread stacks (tens of MB). On glibc KDB gives freed memory back in the background, so after work this falls back near that.
allocator_arena_maxhow many glibc malloc arenas KDB allows (0 = the C library decides; GLIBC_TUNABLES=glibc.malloc.arena_max=N overrides)
allocator_mmap_thresholdblocks of this many bytes or more get memory of their own from the system. 0 = the C library moves the line itself. A GLIBC_TUNABLES with trim_threshold (as in the Docker example) stops glibc from moving it, so KDB sets it to 32 MB; glibc.malloc.mmap_threshold=N overrides
allocator_trims, allocator_trim_released_bytes, allocator_last_trim_ms_agohow often freed memory was given back, how much in total, and how long ago the last time was (0 = never)
allocator_purge_delay_msthe program built with mimalloc (the Docker image): how long memory a thread freed stays with it before it goes back to the system, 1000 unless MIMALLOC_PURGE_DELAY says otherwise. A query that comes within that time on the same thread reuses the pages instead of faulting them in again. 0 with another allocator
allocator_longest_trim_msthe longest one give-back took, in milliseconds. Threads that need memory wait for it, so a long one is a pause of the whole server (HTTP and PostgreSQL port at once). One of 1 s or more is also written to the server log
system_memory_bytes, system_memory_free_bytesRAM in the computer, and what is still free. Inside a container these are the container's limit and use, not the host's.

limits - the CPU and memory the process may really use, and what KDB sized from them (self-tuning):

FieldMeaning
cpusCPUs KDB sizes its threads by: the smaller of the CPUs it may run on and the CPU quota, rounded up
host_cpus, affinity_cpusthe machine's processors, and those the process may run on (0 = not known)
cpu_quotathe container's or job's CPU quota in CPUs (1.5 for --cpus=1.5); 0 = none
cpu_sourcewhat decided cpus: machine, affinity, cgroup v2 cpu.max, cgroup v1 cpu.cfs_quota_us or job object CPU rate
pinned_cpusthe CPUs KDB's threads are pinned to, like "0-3" (--pin-threads: on, or auto inside a CPU quota); "" when not pinned
pinned_threadson: threads that pinned themselves since the start; auto: the threads pinned now (0 when not pinned)
memory_limit_bytes, memory_sourcethe container's or job's memory limit (0 = none) and where it was read
machine_memory_bytesRAM in the machine
query_threads, query_threads_followthe query pool's size now, and whether it follows the CPU limit (false with --threads)
process_memory_limit_bytes, process_memory_limit_followsthe memory limit for the whole process now, and whether it follows the container's (false with --memory-limit)
check_interval_ms, checks, changes, last_check_ms, last_changehow often the limits are read (0 = only at start), readings and changes since start, when the last reading was, and the log line of the last change

memory_limit - the memory limit for the whole process (--memory-limit):

FieldMeaning
limit_bytesthe limit; 0 = none
levelgreen (under 80%), yellow (80-95%: memory is being given back), red (95% and over: quick statements run in the reserve, others wait, then get memory full)
in_use_bytesthe number the limit is checked against: heap_bytes, or the resident size when that is more. Over by the resident size alone (heap under 80%) the level stops at yellow
level_fromin_use, or heap with KDB_LEVEL_FROM_HEAP=1 (the level from the heap alone)
heap_byteswhat KDB's objects hold now
table_data_bytes, key_map_bytes, view_bytesloaded table data of all databases, split as in memory below
pending_byteschanges not yet written to partition files, all databases
index_bytesCREATE INDEX indexes, all databases
result_cache_bytesthe result caches
other_bytesthe rest of the heap: rows of running queries, results being sent, sessions
allocator_bytesresident_bytes - heap_bytes
refusedstatements told memory full since the start
reserve_in_use, reserve_admittedquick statements (reads, no writes or loads) running now in the reserve kept for them at red (at most 8), and how many were let in that way since the start
reserve_liftedof those, how many needed more than their place, waited for memory to come back and then ran on without the cap
stoppedrunning statements stopped with memory full since the start (the process went over the limit, and each held the most memory)
relieved, trimslooks that gave memory back, and times freed memory went back to the system (Linux)
resident_holdslooks at yellow by the resident size alone that did not unload partitions or drop kept results, because the last time that did not bring the level down

Work out the processor share yourself from two readings: (cpu_ms2 - cpu_ms1) * 100 / (uptime_ms2 - uptime_ms1) is the percentage of one processor.

Each running object: id (the X-KDB-Request-Id the client sent, or ""), user ("" for the token), client, database, sql (the first 200 characters), started_ms, elapsed_ms, stopping. Cancel one with DELETE /exec/<id>; a statement with no id can only stop on its own timeout.

timers: every timer of the databases the caller may read, each as in GET /api/timers, with failures_24h (failed runs in about the last 24 hours).

Each opening_databases object says how far the open got. Opening a big database replays the log written after its last checkpoint, which over a slow disk takes minutes; the first request for it waits that long, and this list shows why and whether it is moving. Meanwhile other databases, /api/stats and GET /api/databases answer as usual.

FieldMeaning
name, elapsed_msthe database, and how long it has been opening
phasewhat it does now: reading manifest, opening table files, checking log, replaying log, cleaning up, loading tables, finishing
partitions_done, partitions_totalpartition files gone through, of those the manifest names. Most are only read when a query first needs them, so this part is quick
log_bytes_replayed, log_bytes_totalhow much of the log has been applied again. The log is read once to check it (checking log), then applied (replaying log)
transactions_replayedtransactions applied so far

Each databases object: name, busy and memory_warning (only when set, see below), checkpoint (below), opened_ms, last_lsn, loaded_bytes and memory_budget_bytes (table data in memory, against --memory), pending_bytes (changes not yet written to partition files), cache_bytes, cache_budget_bytes, cache_entries, cache_hits, cache_misses (--cache-mb), cache_verify_checks and cache_verify_mismatches (hits computed again and compared, with KDB_CACHE_VERIFY=1; see result cache), rev_values (values of rev columns that keep their own revision, all tables), cache_pinned (pinned statements whose result is in the cache now, see pinned queries), memory (below), open_cursors, read_only, tables - one object per table with name, rows, partitions, loaded_partitions, loaded_bytes and pending_rows - and materialized_views.

/api/stats never waits for a database. When a write or a checkpoint holds one, its object says "busy": true: the memory and cache numbers are live, and tables, the trigger queues, last_lsn and pending_bytes are from the last reading that got in. With snapshot reads on (since 2026-10-02), a long CALL or an open transaction no longer makes a database busy here: the rows of the tables it changes are counted on a snapshot, so they are the committed rows, fresh, and the open transaction is left alone. Only a checkpoint or a schema change still gives "busy": true. memory_warning is the memory warning of a database far bigger than --memory.

checkpoint - a checkpoint writing changes to partition files right now, or null. Over a slow disk one can take minutes; readers go on meanwhile.

FieldMeaning
stagewriting files (one new file per changed partition), then publishing (switching to them all at once)
tablethe table of the partition written last
partitions_done, partitions_totalchanged partitions written, of those this checkpoint writes
elapsed_mshow long it has been running

Each materialized_views object says how far a materialized view is behind its table:

FieldMeaning
name, basethe view and the table it reads
lag_mshow long ago the oldest change not yet in the view was committed; 0 = up to date
pending_partitionsview partitions waiting to be rebuilt
full_refresh_pendingtrue when the whole view waits (just created, or its table was truncated or altered)
refreshes, last_refresh_ms, last_refresh_took_msrefreshes since the database opened, when the last one ended, and how long it took
last_errorwhy the last refresh failed, or "". It is tried again after a short wait.

disk - what the database takes on disk, by kind, as it last measured it. Each open database measures once a minute, when it empties its old trash, so this costs the reply nothing: measured_ms (when; 0 = not yet), data_bytes, tail_files, tail_bytes, delta_files, delta_bytes (both part of data_bytes), previous_bytes, trash_bytes, trash_folders, wal_bytes, wal_archive_bytes, total_bytes, trash_purged_bytes (the fields of GET /api/disk).

memory - what this database holds, by kind. The sizes are estimates of the bytes KDB asked for.

FieldMeaning
data_bytessaved rows read from partition files
key_map_bytesthe lookup maps of DEDUP KEY and PRIMARY KEY for loaded partitions
view_bytescolumn copies of changed rows that fast scans read; freed at the checkpoint
pending_byteschanges not yet written to partition files (at most a quarter of --memory, and of --memory-limit, before a checkpoint writes them)
cache_bytesresults in the result cache
index_bytesCREATE INDEX indexes; not limited by --memory (--memory-limit counts them)
budget_used_bytesdata + key maps + views of all open databases: this is what --memory limits
cache_used_bytesthe result caches of all open databases: what --cache-mb limits
partition_loads, key_map_buildssince the database opened: partition files read into memory, and key maps built. When they go up with every request, the budget is too small for the work.
partial_loadsof partition_loads: reads of only the columns a query names (per-column loads)
trigger_queuesone object per deferred trigger, as in GET /api/trigger-queue
trigger_job_file_bytesthe size of the file that holds the deferred trigger jobs not finished yet
share_bytesthis database's part of budget_used_bytes
floor_byteswhile the database is writing, the part of the budget other databases cannot take from it; 0 when it is not writing or is the only open database
writingtrue when it changed rows in the last 30 seconds
evicted_by_other_databases, evicted_by_other_databases_bytessince it opened: partitions it unloaded to make room for another database, and their size
curl -s -H "Authorization: Bearer $KDB_TOKEN" http://127.0.0.1:9900/api/stats

GET /metrics

Syntax:

GET /metrics

The server's counters in the Prometheus text format, for Prometheus, Grafana Agent or any scraper. Every number is a counter the server keeps as it works, so a scrape never waits for a database lock and never slows a query.

MetricKindMeaning
kdb_http_requests_total{code}counterHTTP requests answered, by status class (2xx, 4xx, 5xx ...)
kdb_http_unsent_totalcounteranswers that could not be sent: the client stopped reading for the whole send timeout (30 s), or left
kdb_http_read_timeouts_totalcounterrequests that started to arrive but did not finish within 30 s (answered 408)
kdb_http_request_duration_secondshistogramtime of each HTTP request, 1 ms to 10 s buckets
kdb_statement_duration_seconds{protocol}histogramtime of each statement, http or pg
kdb_statement_errors_total{protocol}counterstatements that failed or were stopped
kdb_statements_runninggaugestatements running now
kdb_slow_statements_total, kdb_slow_query_threshold_secondscounter, gaugestatements over the slow-query threshold, and the threshold
kdb_gate_waits_total{mode}, kdb_gate_wait_seconds_total{mode}counterrequests that had to wait for a database lock, and for how long, by read, write, alone, checkpoint
kdb_result_cache_hits_total, kdb_result_cache_misses_totalcounterthe result cache
kdb_column_engine_queries_totalcounterqueries the column engine ran
kdb_partition_loads_totalcounterpartitions read from disk into memory
kdb_checkpoints_total, kdb_checkpoint_written_bytes_totalcountercheckpoints that wrote data files, and the bytes they wrote
kdb_wal_written_bytes_totalcounterbytes written to the log
kdb_io_retries_totalcounterfile reads, writes and syncs tried again because the system was short of memory for a moment (out of memory)
kdb_memory_bytes{kind}gaugememory by kind: table_data, key_maps, views, pending, indexes, result_cache, other, allocator
kdb_memory_limit_bytes, kdb_memory_levelgauge--memory-limit, and its level (0 green, 1 yellow, 2 red)
kdb_process_resident_bytes, kdb_process_cpu_seconds_total, kdb_process_threads, kdb_process_open_files, kdb_uptime_secondsthe process
kdb_database_disk_bytes{database,kind}gaugedisk use by kind: data, previous_versions, trash, wal, wal_archive (measured once a minute)
kdb_database_tail_bytes{database}gaugetail files, part of the data kind above
kdb_database_delta_bytes{database}gaugedelta files, part of the data kind above
kdb_database_trash_purged_bytes_total{database}countertrash the database removed by itself
kdb_trigger_jobs_waiting{database}, kdb_trigger_job_failures_total{database}gauge, counterdeferred trigger jobs waiting, and failed runs
kdb_timer_runs_total, kdb_timer_failures_total, kdb_timer_late_seconds_total, kdb_timer_late_seconds_maxcounter, gaugetimer runs, failed runs, and how late they started
curl -s -H "Authorization: Bearer $KDB_TOKEN" http://127.0.0.1:9900/metrics

A Prometheus scrape job:

scrape_configs:
  - job_name: kdb
    authorization: { credentials: "<the token>" }
    static_configs: [{ targets: ["127.0.0.1:9900"] }]

GET /api/slow-queries

Syntax:

GET /api/slow-queries[?limit=100][&db=<name>]

Statements that took at least the slow-query threshold, newest first. They are kept on disk in <data folder>/_logs/slow_queries.jsonl (one JSON object per line), so they survive a restart. At 16 MB the file becomes slow_queries.1.jsonl and a new one starts; the one before that is removed. The Monitor shows the newest 20.

SettingWhereDefault
the threshold in milliseconds, 0 = offkdb serve --slow-query-ms N, then slow_query_ms in kdb.conf, then KDB_SLOW_QUERY_MS1000
Answer fieldMeaning
threshold_msthe threshold in use
recordedslow statements written since the server started
queriesone object each: ms (when it ended), took_ms, user, client, database, rows, ok, error, sql
curl -s -H "Authorization: Bearer $KDB_TOKEN" "http://127.0.0.1:9900/api/slow-queries?limit=10"

GET /api/settings

Syntax:

GET /api/settings

Where the server keeps its data, and whether that can be changed from here. The Settings page of the console shows it.

Answer fieldMeaning
data_dirthe data folder in use
data_dir_sourcewhere that came from: --data-dir, kdb.conf, KDB_DATA_DIR or program folder
saved_data_dirwhat kdb.conf holds, which may differ from data_dir
config_paththe settings file (--config, else kdb.conf next to the program); "" = settings cannot be saved
config_errorwhy the settings file could not be read, if it could not
command_linethe command line the server was started with
wal_archive_mb, wal_archive_mb_sourceold log each database keeps for repairs, in MB, and where that came from: kdb.conf, KDB_WAL_ARCHIVE_MB or default
backupsthe backup settings in use since the start: dir, schedule, keep_daily, keep_weekly, command, each with a ..._source (--backups, kdb.conf, KDB_BACKUP_DIR, default, ...), and what kdb.conf holds now: saved_dir, saved_schedule, saved_keep_daily, saved_keep_weekly ("" / -1 = not set)
web_dir, help_dir, skill_dirthe folders the console, the help and the Claude skill are read from ("" = not found)
web_dir_source, help_dir_source, skill_dir_sourcewhat chose each folder: kdb.conf, KDB_WEB_DIR (and so on), program folder, share folder, source checkout or not found (folders)
can_savetrue when the settings file can be written
cannot_save_reasonwhy not, when can_save is false
can_restarttrue when POST /api/restart works on this server
flag_overrides_configtrue when --data-dir was given, so a saved folder will not be used
default_database, has_root, read_only_serverthe default database's name, whether there is a data folder, whether the server is read-only
free_bytesroom left on that disk
databases, data_byteshow many databases there are and how much they use

POST /api/settings

Syntax:

POST /api/settings
body: {"data_dir": "E:/kdb-data"}   (or data_dir=<folder> as a query or form field)
body: {"wal_archive_mb": 100}       (or both in one body)
body: {"backup_schedule": "daily 02:00", "backup_keep_daily": 14, "backup_keep_weekly": 8, "backup_dir": "E:/kdb_backups"}

Checks a folder and writes it to the settings file. A new folder takes effect at the next start, not at once, and nothing is moved: the databases in the old folder stay where they are. wal_archive_mb (0 = keep no old log) takes effect at once, for every database without its own ALTER DATABASE ... SET WAL_ARCHIVE. The backup settings take effect at the next start (backup_dir: a folder, off, or default for _backups in the data folder). backup_command is refused with 403: it runs a program on the server, so it is set in kdb.conf by hand, with --backup-command or KDB_BACKUP_COMMAND.

Before writing anything the server makes the folder if it is missing, writes and removes a probe file in it, and refuses a folder that is itself one database (it holds a _manifest.a) - the data folder is the one above those. A folder that fails any of these is a 400 with the reason, and the settings file is not touched.

Answer fieldMeaning
savedalways true
data_dirthe folder as KDB spells it (/ separators, no trailing separator)
wal_archive_mbthe old log kept per database now, in MB
config_paththe file it was written to
restart_requiredtrue when this is not the folder in use now, or a backup setting changed
flag_overrides_configtrue when --data-dir will keep winning, so the restart will change nothing

POST /api/restart

Syntax:

POST /api/restart

Closes every database cleanly, then starts the server again in the same process, on the same port. No service manager, Docker restart policy or supervisor is needed.

The reply is sent before the server stops listening, so the caller always gets an answer. Statements that are running are stopped and open transactions rolled back, as at a normal shutdown. The server prints restarting..., then databases closed cleanly, then its usual banner; the restart is written to the audit log.

A folder saved in kdb.conf that cannot be used when the server comes back does not stop it: the server prints a warning and starts on the usual folder instead.

Answer fieldMeaning
restartingalways true
messagewhat it is about to do