# Backups on a schedule
> kdb serve makes a verified backup of every database at set times, checks a second copy on another disk, runs your command to copy each backup off the server, and warns when the last good backup is old.
- Category: Backup, check, repair, restore
- Keywords: backup schedule, scheduled backup, nightly backup, daily backup, automatic backup, second copy, copy to another disk, offsite, off-site, S3, rclone, scp, remote storage, backup command, backup status, backup warning, default backup folder, \_backups, KDB\_BACKUP\_DIR, KDB\_BACKUP\_SCHEDULE, KDB\_BACKUP\_COMMAND, KDB\_BACKUP\_KEEP\_DAILY, KDB\_BACKUP\_KEEP\_WEEKLY, backup\_dir, backup\_schedule, backup\_command, --backups, --backup-schedule, --backup-copy, --backup-command, --backup-warn-hours, --keep-daily, --keep-weekly, daily, hourly, every
- See also: [Backup, check, repair and restore](backup-and-safety.md), [kdb serve](cli-serve.md), [kdb info, check, repair, dump-file](cli-maintenance.md), [Health, info, check and backup over HTTP](http-server-info.md), [Protection - timeouts, limits, safe updates, read-only](protection.md), [Audit log](audit-log.md)
You do not need a scheduled task outside KDB: `kdb serve` makes the backups itself.
A backup does not stop the database. KDB waits for an open transaction to end, writes a checkpoint, and then copies the files while reads and writes go on.
## Where the settings come from
Each backup setting is read when the server starts. The first of these wins: a flag, `kdb.conf` (the [Settings page](console-settings.md#backups) saves it there), the environment, the default.
| Setting | Flag | `kdb.conf` | Environment | Default |
|---|---|---|---|---|
| backup folder | `--backups
` | `backup_dir` | `KDB_BACKUP_DIR` | `_backups` in the data folder (one folder served by its path: `_backups/` next to it) |
| schedule | `--backup-schedule` | `backup_schedule` | `KDB_BACKUP_SCHEDULE` | `off` |
| daily backups kept | `--keep-daily` | `backup_keep_daily` | `KDB_BACKUP_KEEP_DAILY` | 7 |
| weekly backups kept | `--keep-weekly` | `backup_keep_weekly` | `KDB_BACKUP_KEEP_WEEKLY` | 4 |
| copy command | `--backup-command` | `backup_command` | `KDB_BACKUP_COMMAND` | none |
- A server with a data folder always has a backup folder: `/_backups`, one sub-folder per database. In Docker that is `/data/_backups`, inside the volume, so a rebuild keeps it. `BACKUP DATABASE` and **Backup now** work without any setting.
- `off` as the folder turns backups off. `_backups` cannot be a database name.
- The default folder is on the **same disk** as the data. Copy the backups off the server too: [--backup-command](#backup-command) or [--backup-copy](#backup-copy).
- A folder served by its path (`kdb serve D:\kdb\market`) backs up into `D:\kdb\_backups\market`: the folder a data-folder server in `D:\kdb` would use for the database `market`, so both keep one set of backups. It is next to the database folder, never inside it.
- The Settings page can save the schedule and what is kept, not the copy command: a command runs programs on the server, so only whoever can edit `kdb.conf` (or the server's start command) sets it.
**Command line: The default backup folder in the start-up lines**
```bash
kdb serve
```
Output:
```text
KDB 0.2.0-dev
console http://127.0.0.1:9900
data ./kdb-data (from KDB_DATA_DIR; one folder per database)
default default (used when a request names no database)
backups ./kdb-data/_backups/ (from default)
access no token
capacity 32 HTTP threads (default), 500 PostgreSQL connections (default), lock wait 5 s (default)
lanes reads using 100 ms of CPU (three times in a row) wait for one of 16 heavy slots, at most 16 HTTP in line (--lanes on)
reads 32 reads run at once (the CPUs, at least 2; follows the CPU limit), the rest wait in line (--read-slots)
commits synced to disk before they are confirmed (--sync-commits always)
time zone Asia/Katmandu (the computer's); also timers and backup schedules
readers read while a long trigger or CALL writes (--reader-yield on); idle open transaction rolled back after 30 s (default)
memory 26.95 GB for the whole process (85% of the machine's memory); table data 512.0 MB of it (--memory)
cpu 32 CPUs (the machine's); 16 query threads (half of the CPUs, follows the limit), limits checked every 10 s
limits statement timeout 1 min, result memory 1.00 GB, 5000000 groups, 32 statements per user
safety writes allowed; DROP TABLE kept 7 days (UNDROP TABLE); audit log on (SHOW AUDIT)
logs slow queries >= 1000 ms to ./kdb-data/_logs/slow_queries.jsonl; pg_stat_statements on; metrics at /metrics
recovery the default database is created when it is first used
Press Ctrl+C to stop.
```
## Set a daily backup
Backups are on by default, but nothing makes one by itself until there is a schedule. Three steps:
1. Open **Settings > Backups** in the console, type `daily 16:30` as the schedule and press **Save**. Or send it with the token:
Syntax:
```sql
POST /api/settings {"backup_schedule": "daily 16:30"} -> {"saved": true, "restart_required": true, ...}
```
2. The schedule is saved in `kdb.conf` and used **from the next start**. Restart the server (the **Restart** button on the Settings page, or restart the service or container).
3. Check it: `GET /api/info` shows `"backup_schedule": {"schedule": "daily 16:30", "next_run_ms": ...}`, and the Settings page shows the schedule from `kdb.conf`.
- In a container the time is UTC unless `kdb serve --timezone` is set: `daily 16:30` is 22:15 in Nepal.
- Pick a time after the day's data is in. A backup does not stop reads or writes.
- Then set a [copy command](#backup-command), so a lost disk does not take the backups with it.
## --backup-schedule
Syntax:
```sql
kdb serve [--backups ] --backup-schedule "daily 16:00" [--keep-daily 7] [--keep-weekly 4]
```
| Schedule | Meaning |
|---|---|
| `daily 16:00` | every day at 16:00 |
| `daily 02:00,14:00` | several times a day |
| `hourly`, `hourly :30` | every hour, at minute 0 (or 30) |
| `every 6h`, `every 30m` | counted from midnight, 5 minutes to 24 hours |
| `off` | no scheduled backups (the default) |
Times are the **local time of the computer**, or the zone of `kdb serve --timezone` when it is set (in a zone without daylight saving, such as Nepal `+05:45`, `daily 16:00` is always the same hour; pick a time after your data for the day is in).
- **Returns:** the start-up lines show `schedule backups daily 16:00, next 2026-09-12 16:00:00`. Each backup writes a line: `backup of 'market' OK: 12 files, 3.4 MB, 850 ms, ...` or `backup of 'market' FAILED: ...`.
- **Notes:**
- Every database of the data folder and `_system` (users, audit log) are backed up into `/`. Each backup is read back and checked before it counts.
- Old backups are removed by `--keep-daily` and `--keep-weekly` (7 and 4).
- Each backup is in the [audit log](audit-log.md) (`event = 'backup'`).
| Start error | Why |
|---|---|
| `a backup schedule needs a backup folder: add --backups (or KDB_BACKUP_DIR)` | a schedule with the folder `off` |
| `--backup-copy needs a backup schedule (the copy is made after each scheduled backup)` | a copy without a schedule |
| `the backup schedule (--backup-schedule): the backup interval must be from 5 minutes to 24 hours` | `every 1m` and the like |
These stop the start only when the flags themselves clash. When the folder
`off` comes from `kdb.conf` or `KDB_BACKUP_DIR`, the server starts anyway with
`warning: backups are off ... starting without scheduled backups`, so the
console stays reachable to fix it. The Settings page refuses to save such a
clash: send `{"backup_dir": "off", "backup_schedule": "off"}` to turn both off.
**Command line: Backups every day at 16:00 local time**
```bash
kdb serve --backups backups --backup-schedule "daily 16:00"
```
Output:
```text
KDB 0.2.0-dev
console http://127.0.0.1:9900
data ./kdb-data (from KDB_DATA_DIR; one folder per database)
default default (used when a request names no database)
backups backups/ (from --backups)
access no token
capacity 32 HTTP threads (default), 500 PostgreSQL connections (default), lock wait 5 s (default)
lanes reads using 100 ms of CPU (three times in a row) wait for one of 16 heavy slots, at most 16 HTTP in line (--lanes on)
reads 32 reads run at once (the CPUs, at least 2; follows the CPU limit), the rest wait in line (--read-slots)
commits synced to disk before they are confirmed (--sync-commits always)
time zone Asia/Katmandu (the computer's); also timers and backup schedules
readers read while a long trigger or CALL writes (--reader-yield on); idle open transaction rolled back after 30 s (default)
memory 26.95 GB for the whole process (85% of the machine's memory); table data 512.0 MB of it (--memory)
cpu 32 CPUs (the machine's); 16 query threads (half of the CPUs, follows the limit), limits checked every 10 s
limits statement timeout 1 min, result memory 1.00 GB, 5000000 groups, 32 statements per user
safety writes allowed; DROP TABLE kept 7 days (UNDROP TABLE); audit log on (SHOW AUDIT)
logs slow queries >= 1000 ms to ./kdb-data/_logs/slow_queries.jsonl; pg_stat_statements on; metrics at /metrics
schedule backups daily 16:00, next 2026-10-22 16:00:00
recovery the default database is created when it is first used
Press Ctrl+C to stop.
```
**Command line: A schedule with backups turned off is refused**
```bash
kdb serve --backups off --backup-schedule "daily 16:00"
```
Output:
```text
error: a backup schedule needs a backup folder: add --backups (or KDB_BACKUP_DIR)
```
exit code 1
## --backup-copy
Syntax:
```sql
kdb serve --backups --backup-schedule "..." --backup-copy
```
After each scheduled backup, KDB copies it to a second folder (another disk, a USB disk, a mounted network drive) and checks the copy there: every block checksum and every log checksum is read back from the copy. A backup on the same disk is lost with the disk.
- **Returns:** the log line says `copy checked: E:/kdb_copy/market/...` or `COPY FAILED: ...`.
- **Notes:** the copy keeps old backups by the same `--keep-daily` / `--keep-weekly`. For a cloud or another computer, use [--backup-command](#backup-command).
## --backup-command
Syntax:
```sql
kdb serve --backup-command ""
```
After **every** backup the server makes (the schedule, `BACKUP DATABASE`, **Backup now**, `POST /api/backup`), it runs this command to copy the backup off the server. KDB has no cloud code of its own: the command is any tool you have installed and logged in (rclone, the AWS CLI, scp, robocopy).
| In the command | Becomes |
|---|---|
| `{dir}` | the new backup folder, full path |
| `{database}` | the database's name |
| `{name}` | the backup folder's own name, like `20260924-160000-123` |
- Each value is put in quotes for the shell. Without any placeholder the folder is added at the end.
- The command runs through `/bin/sh` (Linux, Docker) or `cmd.exe` (Windows). Exit code `0` is success.
- It never holds up the database: the backup is finished when the command starts. Commands run one at a time, in order; at most 100 wait.
- **Returns:** a line in the server log (`backup copy command for 'market' OK in 5200 ms: ...` or `FAILED (exit code 1) ... - `), an event in the activity log, and `backup_command` in `/api/info` (below). The console shows `(copy failed)` next to **Last backup**.
- The tool must be inside the Docker image or on the host's `PATH`. Test the command by hand once.
Syntax:
```sql
rclone: --backup-command "rclone copy {dir} remote:kdb/{database}/{name}"
AWS S3: --backup-command "aws s3 cp --recursive {dir} s3://my-bucket/kdb/{database}/{name}"
scp: --backup-command "scp -r {dir} backup@10.0.0.9:/srv/kdb/{database}/"
Windows: --backup-command "robocopy {dir} \\nas\kdb\{name} /E"
```
A copy made with `--backup-command` is a plain folder: to use it, copy it back and restore it ([kdb restore](backup-and-safety.md#kdb-restore), or put it in the backup folder and use [POST /api/restore](http-server-info.md#api-restore)).
**Command line: A command after each backup: here it only prints the folder**
```bash
kdb serve --backup-command "echo copy {database} {name}"
```
Output:
```text
KDB 0.2.0-dev
console http://127.0.0.1:9900
data ./kdb-data (from KDB_DATA_DIR; one folder per database)
default default (used when a request names no database)
backups ./kdb-data/_backups/ (from default)
copy after each backup: echo copy {database} {name} (from --backup-command)
access no token
capacity 32 HTTP threads (default), 500 PostgreSQL connections (default), lock wait 5 s (default)
lanes reads using 100 ms of CPU (three times in a row) wait for one of 16 heavy slots, at most 16 HTTP in line (--lanes on)
reads 32 reads run at once (the CPUs, at least 2; follows the CPU limit), the rest wait in line (--read-slots)
commits synced to disk before they are confirmed (--sync-commits always)
time zone Asia/Katmandu (the computer's); also timers and backup schedules
readers read while a long trigger or CALL writes (--reader-yield on); idle open transaction rolled back after 30 s (default)
memory 26.95 GB for the whole process (85% of the machine's memory); table data 512.0 MB of it (--memory)
cpu 32 CPUs (the machine's); 16 query threads (half of the CPUs, follows the limit), limits checked every 10 s
limits statement timeout 1 min, result memory 1.00 GB, 5000000 groups, 32 statements per user
safety writes allowed; DROP TABLE kept 7 days (UNDROP TABLE); audit log on (SHOW AUDIT)
logs slow queries >= 1000 ms to ./kdb-data/_logs/slow_queries.jsonl; pg_stat_statements on; metrics at /metrics
recovery the default database is created when it is first used
Press Ctrl+C to stop.
```
## --backup-warn-hours
Syntax:
```sql
kdb serve ... --backup-schedule "..." --backup-warn-hours 26 (0 = no warning)
```
Each database is checked on its own. When a database's newest good backup is older than this, the log says so once an hour, naming the database: `WARNING: the last good backup of 'sales' is 30 hours old ...`. `/api/info` and the console show the same warning.
- A database with no backup at all counts from when the server first saw it: `WARNING: database 'test_01' has no good backup yet (known for 27 hours) ...`. A database made after today's round is backed up by the next one, so it is not overdue before that.
- When several databases are overdue, the one waiting longest is named and the rest are counted: `... (and 12 more databases)`.
- The server reads the backup folders at start and every hour after, so a backup made by hand (`BACKUP DATABASE`) counts too. It never opens a database to do so.
## Backup status in /api/info
Syntax:
```sql
GET /api/info -> "backup_schedule": {"schedule", "copy_to", "next_run_ms", "last_run_ms", "last_run_ok",
"last_ok_ms", "running", "warning", "databases": [...]}
```
- **Returns:** `null` when there is no schedule. `databases` lists the last round: `database`, `ok`, `error`, `dir`, `files`, `bytes`, `started_ms`, `took_ms`, `copy_dir`, `copy_ok`, `copy_error`.
- `warning` is `""`, `the last good backup of 'x' is N hours old` or `database 'x' has no good backup yet (known for N hours)`, with `(and N more databases)` when several are overdue. `databases_overdue` is how many are.
- `last_ok_ms` is the newest good backup of the database whose newest backup is oldest (0 when a database has none).
Syntax:
```sql
GET /api/info -> "backup_command": {"set", "runs", "failed", "dropped", "queued", "running",
"last": {"database", "dir", "ok", "exit_code", "output", "started_ms", "took_ms"}}
```
- `null` without a copy command. `output` is the end of what the command printed, shown to admins only (`""` for other users).