Incident Response

ClickHouse Backup and Restore: BACKUP, Incrementals and S3 in Practice

Replicas protect you from a dead disk, not from a DROP TABLE that reaches every replica in seconds. Here is how to set up ClickHouse backups that you can actually restore, including on multi-terabyte clusters.

· 11 min read · AG Data Team

A working ClickHouse backup is one you have restored at least once. ClickHouse has native BACKUP and RESTORE commands that write to a local disk, S3 or Azure, support incremental backups and run in the background. They are simple to start with, and most of the real work is in the details: where backups go, how long the incremental chain gets, and whether a restore fits in your recovery window.

The first point is the one teams most often skip. The ClickHouse documentation puts it directly: replication protects from hardware failures, but it does not protect against human errors, and in many cases such mistakes affect all replicas. A wrong ALTER ... DELETE, a DROP on the wrong cluster or a bad migration is replicated as faithfully as a good insert.

Step 1: give ClickHouse a place to write backups

For a local or mounted target, define a disk and allow it for backups. This is the configuration from the official guide, and it needs a server restart:

<!-- /etc/clickhouse-server/config.d/backup_disk.xml, then restart -->
<clickhouse>
    <storage_configuration>
        <disks>
            <backups>
                <type>local</type>
                <path>/backups/</path>
            </backups>
        </disks>
    </storage_configuration>
    <backups>
        <allowed_disk>backups</allowed_disk>
        <allowed_path>/backups/</allowed_path>
    </backups>
</clickhouse>

For S3 you do not need a disk at all: the S3() destination takes the endpoint and path, the access key and the secret directly in the command. Azure Blob Storage works the same way with AzureBlobStorage(). A local disk on the same server is only a staging area; a backup that lives on the machine it protects is not a backup.

Step 2: take a full backup and watch it

-- full backup of one database, in the background
BACKUP DATABASE analytics
TO S3('https://s3.eu-central-1.amazonaws.com/acme-ch-backups/analytics/2026-11-12-full',
      '<key id>', '<secret>')
ASYNC;

-- returns an id; follow it
SELECT id, name, status, num_files, uncompressed_size, compressed_size, error
FROM system.backups
ORDER BY start_time DESC
LIMIT 5;

Without ASYNC the command blocks until the backup is complete, which on a large database means a client session that must stay open for hours. With ASYNC it returns an id immediately. In system.backups the status moves from CREATING_BACKUP to BACKUP_CREATED, or to BACKUP_FAILED with the reason in error. Restores report RESTORING, RESTORED or RESTORE_FAILED. Every operation is also recorded in system.backup_log, which is what your monitoring should alert on.

You can back up a single table, a database, or ALL with exclusions, and the syntax supports EXCEPT TABLES and EXCEPT DATA FROM TABLES to keep a definition without its rows, for example for large staging tables you can rebuild.

Step 3: incremental backups with base_backup

A full backup of a multi-terabyte database takes time and space, so the usual pattern is a periodic full backup plus incrementals. An incremental stores only what changed since the backup named in base_backup:

BACKUP DATABASE analytics
TO S3('https://s3.../analytics/2026-11-13-incr', '<key id>', '<secret>')
SETTINGS base_backup = S3('https://s3.../analytics/2026-11-12-full',
                          '<key id>', '<secret>')
ASYNC;

The catch is in the documentation: restoring an incremental backup requires all the backups it is built on. If someone applies an S3 lifecycle rule that deletes the full backup after 30 days, every incremental based on it becomes useless. Keep the chain short, start a new full backup regularly, and let retention delete whole chains, never single links.

Restore next to the data, not over it. RESTORE refuses to write into a table that already has rows. The setting allow_non_empty_tables removes that check, but the documentation warns that it can duplicate data. Restore into a new name with AS, compare, then swap or copy what you need.

Step 4: restore, the part that matters

-- restore next to the original, never over it
RESTORE TABLE analytics.events AS analytics.events_restored
FROM S3('https://s3.../analytics/2026-11-13-incr', '<key id>', '<secret>');

-- only the schema, e.g. to rebuild an empty replica of the structure
RESTORE DATABASE analytics FROM Disk('backups', 'analytics-full.zip')
SETTINGS structure_only = 1;

Restoring under a new name lets you recover a few partitions after a bad delete without touching the live table. If you are recovering from an accidental mass delete, our notes on deleting rows in ClickHouse explain why lightweight and mutation deletes behave differently, which matters when you decide what to copy back. Settings such as restore_table_data and restore_access_entities control what a restore brings back; users, roles and quotas are included only if they were created with SQL, not defined in users.xml.

Clusters, concurrency and alternatives

What changes with multi-terabyte data

Key takeaways

ClickHouse Backups S3 Disaster Recovery
Work with us

Not sure your ClickHouse backups would restore?

We design and test ClickHouse backup and restore for production clusters, from S3 incrementals to full restore rehearsals with measured recovery time.

Get in touch More from the blog