PointInTimeRestore¶
PointInTimeRestore rolls a relational CamundaCluster back to a point in time. The database server rolls back to that point, and the operator aligns the Zeebe primary storage with it. It needs no LogicalBackupRDBMS. It uses two continuous sources instead: point-in-time recovery of the database server, and the continuous primary-storage backups of Zeebe.
For an Elasticsearch cluster, use a LogicalRestoreElasticsearch. Elasticsearch has no point-in-time recovery.
One resource is one restore. The spec is immutable, and the restore runs once. To retry, create a new resource. kubectl get pitr lists the restores with their phase, cluster, and timestamp.
You do not suspend the cluster first. The restore does it, as Operations: Restore a cluster describes. That section also holds what every restore kind shares: when the cluster starts again, one operation at a time, failed restores, and GitOps. This kind writes no version on the cluster.
The smallest restore names the cluster and the point:
apiVersion: core.camunda.io/v1
kind: PointInTimeRestore
metadata:
name: my-cluster-pitr
namespace: my-cluster-ns
spec:
clusterRef:
name: my-cluster
timestamp: "2026-07-30T14:30:00Z"
graph LR
PITR[PointInTimeRestore] -.->|clusterRef| CC[CamundaCluster]
CC -.->|storageRef| SSC[SecondaryStorageConfig]
SSC -.->|databaseConfigRef| DBC[DatabaseConfig]
DBC -.->|serverRef| DBS["DatabaseServerConfig (pitr)"]
EXT["You, or the producer of the contract"] -->|rolls the server back| PG["PostgreSQL (external)"]
PITR -->|reads exporter positions| PG
PITR -->|restore Job per broker| PVC[Broker data volumes]
Who rolls the database back¶
spec.pitr.recovery on the DatabaseServerConfig of the cluster decides this:
pitr.recovery |
Who rolls the database back | What you do first |
|---|---|---|
external (default) |
You, before you create the restore. | Roll the database back to the point. Then create the restore. |
operator |
The producer of the contract, for example a DatabaseServer, when the restore asks. | Nothing. Create the restore. |
With external, the database must hold the state of spec.timestamp when you create the restore. On a self-hosted server, use the point-in-time recovery of PostgreSQL. On a managed service, use the point-in-time restore of the provider. If the provider creates a new instance, for example Amazon RDS, update host on the DatabaseServerConfig before you create the restore.
With operator, the restore writes the request in spec.recovery of the contract and waits in RestoringDatabase until spec.pitr.lastRecovery answers it. The request carries the UID of the restore, so an answer to an earlier restore never counts for this one. A rollback usually moves the endpoint on the contract. The restore continues when the contract reports Ready for the new endpoint, and the cluster holds that endpoint. The answer ends the restore when it is not a success:
| Answer | Result |
|---|---|
Unavailable |
The server never held the point. The restore fails with the reason PitrUnavailable. |
Failed |
The rollback started and did not finish. The restore fails with the reason Failed. |
status.failureMessage carries the message of the server.
The database during a rollback¶
With operator, the restore holds the database from just before its request until Completed or Failed. status.backend names the database and follows the endpoint of the contract. No other CamundaCluster starts on that database, and the next one reports WaitingForHandover. CamundaCluster: Secondary storage describes that wait. The hold is on the DatabaseServerConfig and the database name, so it covers the old endpoint and the new one. With external, the restore holds nothing, because the operator writes nothing into the database.
The server finishes a rollback that it started. So these changes during the rollback take effect only when the contract answers:
- If you delete the cluster, replace it, or point it at another database, the restore fails after the answer.
- If you delete the restore, it stays and holds the database until the answer.
- If no
DatabaseServerConfigcan answer, for example because you deleted it, the restore fails, or goes, 10 minutes later.
Before a cluster uses the database again, wait until spec.pitr.lastRecovery on the DatabaseServerConfig answers the request. A contract that stops declaring pitr.recovery: operator holds the restore until you set it back or delete the restore.
Choose the point to restore to¶
The point must lie inside the window that the continuous primary-storage backups of Zeebe cover. The operator turns these backups on for every relational cluster that names a backupStorageRef. Two fields of the CamundaCluster bound the window:
| Bound | Field on the cluster | Default |
|---|---|---|
| Zeebe started a backup after the point | spec.backup.primaryStorage.schedule |
PT1H |
| Zeebe still keeps that backup | spec.backup.primaryStorage.retention.window |
P7D |
Choose a point at least one backup interval before the cluster stopped writing, and inside the retention window. With the defaults, a point between one hour and seven days before the brokers stopped is safe. "Now" is always outside the window, because the newest backup is always behind the brokers.
To read the real window, ask the cluster while it still runs:
kubectl exec -n my-cluster-ns my-cluster-zeebe-0 -- \
curl -s localhost:9600/actuator/backupRuntime/state
In that answer, the checkpointTimestamp of a backup is the time that the backup finished. The Camunda API reference says that it is the time that the checkpoint was created. A backup covers the point when its checkpointPosition is at or past the exporter position that the database holds at the point. So a backup that finished after the point can fail to cover it.
The point must also lie within the retention period that the DatabaseServerConfig declares, and not in the future.
What goes in spec.timestamp¶
spec.timestamp is the point that the database holds after the rollback. The cluster comes back at a Zeebe checkpoint near that point, not at the point itself. spec.backup.primaryStorage.checkpointInterval sets how far apart the checkpoints are, and it defaults to PT15M. A shorter value brings the cluster back closer to the point. Camunda documents the rule in Point-in-time restore.
If the point is outside the window¶
The operator cannot find this out before it erases the broker volumes. Only the restore application can, because it compares log positions in the backup store. The restore then fails with the reason ExporterPositionNotCovered, and status.failureMessage names the cause. The backups stay whole. Choose an earlier point, roll the database back to it, and create a new restore.
Phases¶
status.phase records how far the restore got. A restore continues at that phase after the operator restarts.
| Phase | What happens |
|---|---|
Pending |
The restore waits. A rule in Requirements does not hold, another operation holds the cluster, or the cluster is not suspended yet. The restore suspends the cluster here. It erases nothing. |
RestoringDatabase |
Only with pitr.recovery: operator. The restore asked the contract for the rollback and waits for the answer. Nothing limits this wait while the contract exists. It erases nothing. |
ValidatingDatabaseState |
The operator reads the exporter position of every partition from the database. The restore stays in this phase only while the operator cannot reach the database. |
RestoringPrimaryStorage |
The operator deletes and creates the broker data volumes, and runs the Camunda restore application once per broker with --to=<spec.timestamp>. |
Completed |
The restore finished. The cluster starts again, unless you suspended it yourself or another hold remains. |
Failed |
The restore ended. status.failureMessage says why. |
Requirements¶
Each rule below holds the restore in Pending until it holds. Correct the cause, and the same resource continues.
| Rule | Reason while it does not hold |
|---|---|
The cluster, its SecondaryStorageConfig of type: rdbms, its DatabaseConfig, and its DatabaseServerConfig exist, in the namespace of the restore. |
InvalidReference |
The cluster names a spec.backupStorageRef. Without it, Zeebe takes no primary-storage backup. |
InvalidReference |
The DatabaseServerConfig publishes status.systemIdentifier for its current spec. |
InvalidReference |
The DatabaseServerConfig declares spec.pitr.enabled: true, and spec.timestamp lies within its retention period and not in the future. |
PitrUnavailable |
| The brokers run in UTC. See The database-state check. | PitrUnavailable |
Exactly one Database resource runs a logical database on the PostgreSQL instance, and it is the database of the cluster. |
SharedServer, or InvalidReference |
| The cluster holds its database, and nothing else writes it. | StorageAlreadyAttached, or WaitingForHandover |
The database holds no state after spec.timestamp. |
DatabaseNotRestored |
A dedicated server. Point-in-time recovery rolls back the whole PostgreSQL instance, not one logical database. So the operator counts the Database resources of every namespace that run a database on that instance. It identifies the instance by its system identifier. More than one holds the restore with SharedServer, and the message names each one as <namespace>/<name>. A Database that has not reached a server yet can be on any server, so it holds the restore with InvalidReference. Wait until every Database reports Ready, or delete the ones whose server no longer exists. A server that no Database uses also holds the restore with InvalidReference. Declare the database of the cluster as a Database on a server of its own.
The operator records the chain that it checked in status.storage. A cluster that you point at another database after the check fails the restore. Create a new restore for the database that the cluster uses now.
The database-state check¶
Before it touches a volume, the operator connects to the database with the application credentials in DatabaseConfig.spec.credentialsSecretRef. It reads last_updated of every partition from the exporter_position table, and it records the values in status.observedPositions.
The restore waits in Pending with the reason DatabaseNotRestored in three cases. A partition has no row, the table does not exist, or a value is later than spec.timestamp plus one minute. The minute covers the difference between the clock of the database and the source of your timestamp. The operator reads the table without a prefix, so a cluster that sets camunda.data.secondary-storage.rdbms.prefix does not pass this check.
If the operator cannot reach the database, the restore stays in ValidatingDatabaseState with the reason ConnectionFailed or MissingSecret, and it fails after 10 minutes. It touches no volume there.
The brokers must run in UTC. The broker writes last_updated with its own wall clock and no time zone, and the operator reads it as UTC. The operator reads the environment of the broker container: TZ, and -Duser.timezone in JAVA_TOOL_OPTIONS, JDK_JAVA_OPTIONS, _JAVA_OPTIONS, JAVA_OPTS, or EXTRA_JVM_OPTS. It reads them from extraEnv and from each ConfigMap and Secret in extraEnvFrom. A zone other than UTC holds the restore with PitrUnavailable. So does one of these variables that takes its value from a valueFrom reference, and a source that does not exist and is not optional. The message names the cause and the step that removes it. The restore then continues by itself.
The check proves that the database is not ahead of the point. It cannot prove that the database holds exactly that point. A database that holds an earlier point passes, and the restore application aligns Zeebe with it.
Primary storage¶
The operator deletes the data volume of every broker, creates it again, and runs the restore application once per broker. The new volume takes the size, the storage class, the access modes, and the labels of the claim template of the broker StatefulSet. The volumes belong to that StatefulSet, not to the restore. Deleting the restore leaves them in place.
The restore application reads the exporter position of each partition from the database. It restores a checkpoint whose position is not below that exporter position, so the restored Zeebe state is never behind the database.
Every Job carries the labels camunda.io/component: restore, camunda.io/point-in-time-restore: <restore name>, and camunda.io/cluster: <cluster name>. Its name is <restore name>-pitr-<broker>, for example my-cluster-pitr-pitr-0. A Job of that name from an earlier restore of the same name fails this restore, and the message names the Job. A failed restore keeps its Jobs, see A failed restore holds the broker volumes.
Deletion¶
Deleting the restore removes its Jobs and their pods. The broker volumes stay. The restore wrote nothing to the backup store. The restore removes its suspension hold from the cluster when the last Job pod is gone, and spec.suspend stays. A restore that you delete during the rollback of its database stays until the rollback ends, see The database during a rollback.
Status¶
| Type | Reason | Meaning | What to do |
|---|---|---|---|
Ready |
Progressing |
A phase of the restore runs, or the restore prepares the cluster. | Wait. The message names the work. |
Ready |
Completed |
The restore finished. Ready is True. The cluster starts again, unless you suspended it yourself or another hold remains. |
Nothing. |
Ready |
Failed |
The restore ended. | Read status.failureMessage. Correct the cause. Delete the failed restore, then create a new one. |
Ready |
ClusterNotSuspended |
Somebody removed the suspension hold of the restore and cleared spec.suspend, after Pending. |
Suspend the cluster again. The restore fails 10 minutes after the first outage. During a rollback, the 10 minutes start when the contract answers. |
Ready |
ClusterClaimed |
Another backup or restore holds the cluster, or a claim Lease that no backup or restore holds. The message names it. | Wait. The restore starts when the holder gives the cluster back. If the message names a claim Lease to delete, delete it. |
Ready |
InvalidReference |
A rule in Requirements does not hold, or the broker StatefulSet is gone. During a rollback: the cluster was deleted, replaced, or pointed at another database. | Correct what the message names. After a change during a rollback, wait for the answer, then create a new restore. |
Ready |
PitrUnavailable |
The server does not declare point-in-time recovery, or the point lies outside its retention period or in the future. Or the server answered Unavailable, or the brokers do not run in a UTC zone that the operator can read. |
Enable pitr on the server, or choose a point that the server holds. For the broker zone, do the step that the message names. |
Ready |
SharedServer |
More than one Database uses the PostgreSQL instance. The message names each one. |
Move the cluster to a server of its own. |
Ready |
StorageAlreadyAttached |
Another CamundaCluster holds the database of the cluster. The message names it. |
Move one of the two clusters to a database of its own, or delete the other cluster. |
Ready |
WaitingForHandover |
The cluster does not hold its database yet, or pods or a deleted restore still write it. The message names them. | Wait. |
Ready |
DatabaseNotRestored |
The database is ahead of spec.timestamp, or it has no position for a partition. No volume is touched. |
Roll the database back to the point, then wait. |
Ready |
ExporterPositionNotCovered |
The point lies outside the window of the primary-storage backups. The broker volumes are already erased. | Read If the point is outside the window. |
Ready |
MissingSecret |
A credentials Secret of the cluster is missing or lacks a key. Or a pod of a restore Job cannot start, because a Secret it needs does not exist. | Create the Secret that the message names. |
Ready |
ConnectionFailed |
The operator cannot read the database. | Correct the endpoint or the credentials. |
After Pending, a restore that loses a dependency waits 10 minutes, then fails with the reason Failed. Once the restore has deleted a broker volume, the 10 minutes count from the first outage, even when a dependency comes back in between.
Other status fields:
status.targetClusterUIDpins the cluster. A cluster that somebody deletes and creates again under the same name ends the restore.status.backendnames the database that the restore holds during a rollback. It is empty withpitr.recovery: external.status.storagerecords the storage chain that the restore checked, down to the system identifier of the server.status.observedPositionsholds thelast_updatedvalue that the check read for each partition.status.clusterSuspendedistruewhen this restore suspended the cluster.status.brokersis the broker count that the restore read from the broker StatefulSet.status.recreatedClaimsnames the broker data volumes that the restore deleted and created again.status.primaryJobNamesnames the restore Job of every broker, in broker order.status.terminalReasonis theReadyreason of the terminal phase.status.failureMessagesays why a failed restore ended.status.completionTimeis when the restore reached a terminal phase.status.observedGenerationis the last generation that the operator processed.
Fields¶
The API reference lists every field of PointInTimeRestore, with its type, whether it is required, its default, and its validation.
Validation rules¶
- The whole
specis immutable. spec.timestampis an RFC 3339 timestamp. The API server accepts a point in the future. The restore reportsPitrUnavailablefor it.clusterRefnever crosses a namespace.- The API server accepts a restore that breaks a rule of live state. The restore reports the breach on
Ready, see Requirements.
Roll back to just before a bad deployment¶
apiVersion: core.camunda.io/v1
kind: PointInTimeRestore
metadata:
name: my-cluster-pitr-pre-release
namespace: my-cluster-ns
spec:
clusterRef:
name: my-cluster
# One minute before the faulty process deployment. With pitr.recovery:
# external, the database is already rolled back to this point.
timestamp: "2026-07-30T14:29:00Z"
Related¶
- Operations: Restore a cluster: what every restore kind does to the cluster, and how to act on a failed restore.
- LogicalRestoreRDBMS and LogicalRestoreElasticsearch: restore a cluster from one of its own backups.
- CamundaCluster: referenced through
clusterRef. Itsspec.backup.primaryStoragesets the window of the continuous backups. - SecondaryStorageConfig: resolved through the
storageRefof the cluster. It must betype: rdbms. - DatabaseConfig: its
credentialsSecretRefholds the credentials that read the exporter positions. - DatabaseServerConfig: declares
pitr, the retention period, and who rolls the server back. This operator never uses its admin credentials. - DatabaseServer: a server that rolls itself back on request.
- ObjectStorageConfig: resolved through the
backupStorageRefof the cluster. It holds the continuous primary-storage backups.