Skip to content

BackupSchedule

BackupSchedule takes logical backups of one CamundaCluster on a cron schedule. You create it, or another tool creates it for you.

At each trigger the operator creates one backup of the kind that matches the secondary storage of the cluster: LogicalBackupElasticsearch or LogicalBackupRDBMS. That backup then runs on its own. The schedule also deletes its own finished backups beyond spec.retained.

kubectl get backupschedules lists the schedules with Ready, its reason, the cron expression, and the age. kubectl get backupschedules -o wide adds the cluster, the last schedule, and the last backup.

Before you create a schedule, make sure that:

  • The CamundaCluster has spec.storageRef and spec.backupStorageRef.
  • The SecondaryStorageConfig that the cluster names exists.
  • The schedule lives in the namespace of the cluster.

The smallest schedule names the cluster and the cron expression:

apiVersion: core.camunda.io/v1
kind: BackupSchedule
metadata:
  name: my-cluster-schedule
  namespace: my-cluster-ns
spec:
  clusterRef:
    name: my-cluster
  schedule: "0 2 * * *"
graph LR
    BS[BackupSchedule] -.->|clusterRef| CC[CamundaCluster]
    CC -.->|storageRef| SSC[SecondaryStorageConfig]
    BS -->|creates at each trigger| LB["LogicalBackupElasticsearch or LogicalBackupRDBMS"]
    BS -->|prunes beyond spec.retained| LB
    LB -.->|clusterRef| CC
Hold "Alt" / "Option" to enable pan & zoom

The cron expression

spec.schedule is a five-field cron expression: minute, hour, day of month, month, and day of week. The operator evaluates it in UTC. The time zone of the cluster, of the node, and of your workstation does not change the trigger time.

apiVersion: core.camunda.io/v1
kind: BackupSchedule
metadata:
  name: my-cluster-schedule
  namespace: my-cluster-ns
spec:
  # 02:00 UTC every day.
  schedule: "0 2 * * *"
  # ... the rest of your schedule

The first trigger comes after the creation of the schedule. Every later trigger follows status.lastScheduleTime.

One trigger creates at most one backup. If the operator is down for a longer time, the triggers of that time do not queue up. Only the latest one fires, and the schedule continues from there.

You can change spec.schedule and spec.retained at any time. The new values apply at once.

The backups that a schedule creates

The name of a created backup is <schedule>-<unix-timestamp> of the trigger. The labels name the cluster and the schedule:

apiVersion: core.camunda.io/v1
kind: LogicalBackupRDBMS
metadata:
  name: my-cluster-schedule-1787104800
  namespace: my-cluster-ns
  labels:
    camunda.io/cluster: my-cluster
    camunda.io/backup-schedule: my-cluster-schedule
spec:
  clusterRef:
    name: my-cluster

To list the backups of one schedule, select on the label:

kubectl get lbrdbms -n my-cluster-ns -l camunda.io/backup-schedule=my-cluster-schedule

For a schedule name of more than 63 characters, the label value is shortened and ends in a hash. kubectl get lbrdbms --show-labels shows it.

Each creation records the Normal event BackupCreated on the schedule. The event names the kind and the backup.

Skipped triggers

The operator creates no backup when:

  • The cluster is suspended. This is the case with spec.suspend: true and while a restore holds the cluster. It is also the case while the Ready reason of the cluster is StorageAlreadyAttached or WaitingForHandover, because the cluster then waits for its backend. Read Ready on the CamundaCluster for the cause. Backups start again at the next trigger after the cluster resumes.
  • The backup cannot start on the cluster. Both kinds need a usable status.management on the cluster. A LogicalBackupRDBMS also needs the Ready condition of the cluster at True. A LogicalBackupElasticsearch needs status.management.backupRepository instead, and does not need Ready. The event names the cause and the state of the Ready condition of the cluster. Correct the cause on the CamundaCluster.
  • A backup of this schedule is not Completed or Failed yet. Two backups of one schedule never overlap. If a backup often runs past the next trigger, give the schedule a longer gap.
  • An object already holds the name of the new backup and belongs to something else. Only that one trigger is skipped, because the name carries the trigger time. Find out what created that object, and remove it if it does not belong there.
  • A reference does not resolve. The CamundaCluster is missing, or it has no spec.storageRef or spec.backupStorageRef, or its SecondaryStorageConfig is missing. The Ready condition of the schedule names the cause. Create the resource, or set the missing field.
  • spec.schedule is not a valid cron expression. No trigger fires at all, and Ready names spec.schedule. Correct the expression.
  • More than one trigger passed while the operator was not running. The schedule takes the latest of them, see The cron expression. The earlier triggers record no event.

The first three cases record the Normal event TriggerSkipped. A name that belongs to something else records the Warning event BackupNameTaken. A reference that does not resolve and an invalid cron expression show on Ready of the schedule, with no event.

A skipped trigger is not retried. status.lastScheduleTime moves to it, and the next backup runs at the next trigger of the cron expression.

kubectl describe backupschedule my-cluster-schedule -n my-cluster-ns shows the events:

Type    Reason          Message
Normal  BackupCreated   Created LogicalBackupRDBMS "my-cluster-schedule-1787104800"
Normal  TriggerSkipped  Skipped the trigger at 2026-08-20T02:00:00Z: CamundaCluster "my-cluster" is suspended
Normal  TriggerSkipped  Skipped the trigger at 2026-08-21T02:00:00Z: backup "my-cluster-schedule-1787104800" has not finished
Warning BackupNameTaken Skipped the trigger at 2026-08-22T02:00:00Z: LogicalBackupRDBMS "my-cluster-schedule-1787364000" already exists and belongs to something else
Normal  TriggerSkipped  Skipped the trigger at 2026-08-23T02:00:00Z: CamundaCluster "my-cluster" is not ready: Ready is False with reason InvalidReference

Retention

spec.retained bounds how many backups of this schedule are kept, per terminal phase:

apiVersion: core.camunda.io/v1
kind: BackupSchedule
metadata:
  name: my-cluster-schedule
  namespace: my-cluster-ns
spec:
  retained:
    # Two weeks of nightly backups.
    completed: 14
    failed: 1
  # ... the rest of your schedule

The operator deletes the oldest completed backups beyond retained.completed, by completion time. It deletes the oldest failed backups beyond retained.failed the same way. A backup that is still Pending or Running is never deleted. A schedule that sets no bounds keeps 7 completed and 3 failed backups. If you lower a bound, the overflow is deleted at once.

The operator deletes only the backups that carry the label camunda.io/backup-schedule of this schedule. A backup that you create by hand carries no such label, so no schedule deletes it, unless you add that label.

A deleted backup takes its stored artifacts with it. For an Elasticsearch backup these are the snapshots and the partition backup. For a relational backup this is the database dump. If the cluster or a storage contract of the backup is gone, the artifacts stay in the bucket, and the backup records a Warning event. The Deletion section of LogicalBackupElasticsearch and LogicalBackupRDBMS names the cases. Each deletion records the Normal event BackupPruned.

Backups that outlive the primary storage

A LogicalBackupRDBMS is one database dump plus the Zeebe backup of its time. The two together are one restore point. Zeebe deletes the primary-storage backups that fall outside spec.backup.primaryStorage.retention.window of the cluster, P7D by default. A dump that is older than that window can no longer be restored.

For a relational cluster the operator compares the age of the oldest retained dump with that window. The age is retained.completed times the interval between two triggers. If the dumps live longer than the window, the operator records the Warning event RetentionWindowExceeded at the trigger:

Type     Reason                   Message
Warning  RetentionWindowExceeded  the schedule keeps 14 completed backups at one every 24h0m0s, so the oldest
                                  dump lives about 336h0m0s, longer than the primary-storage retention window
                                  P7D of the cluster; dumps older than the window can no longer be restored

To clear the warning, lower retained.completed, take the backups less often, or raise the retention window of the cluster.

The operator records no such warning when spec.backup.primaryStorage.retention.cleanupSchedule of the cluster is none, because Zeebe then deletes no primary-storage backup. The check does not run for an Elasticsearch cluster.

Deletion

When you delete a BackupSchedule, its backups and their artifacts stay. No trigger fires again, and no retention applies to those backups. Delete the backups you no longer want. Or create a schedule of the same name again, and its retention applies to them again.

A schedule has no suspend field. To stop the backups for a time, delete the schedule.

Status

Type Reason Meaning What to do
Ready Healthy The references resolve and the schedule can run its backups. Nothing.
Ready InvalidReference The CamundaCluster does not exist, it has no spec.storageRef or spec.backupStorageRef, its SecondaryStorageConfig does not exist, or spec.schedule is not a valid cron expression. The message names the cause. Create the resource, set the missing field, or correct spec.schedule.

Ready reports the references and the cron expression only. A skipped trigger is an event, see Skipped triggers.

A healthy schedule that took a backup reads:

status:
  # The most recent trigger that the schedule consumed. A skipped trigger is consumed too.
  lastScheduleTime: "2026-08-19T02:00:00Z"
  # The backup that the schedule created most recently.
  lastBackupName: my-cluster-schedule-1787104800
  # The last generation that the operator reconciled.
  observedGeneration: 2
  conditions:
    - type: Ready
      status: "True"
      reason: Healthy
      message: the schedule can run its backups
      observedGeneration: 2
      lastTransitionTime: "2026-08-19T02:00:00Z"

A schedule whose cluster is gone reads:

status:
  lastScheduleTime: "2026-08-19T02:00:00Z"
  lastBackupName: my-cluster-schedule-1787104800
  observedGeneration: 2
  conditions:
    - type: Ready
      status: "False"
      reason: InvalidReference
      message: CamundaCluster my-cluster-ns/my-cluster does not exist
      observedGeneration: 2
      lastTransitionTime: "2026-08-20T02:00:00Z"

Spec reference

Every field, with its type, whether it is required, and its default:

apiVersion: core.camunda.io/v1
kind: BackupSchedule
metadata:
  name: my-cluster-schedule
  namespace: my-cluster-ns
spec:
  # object. Required. The CamundaCluster to back up, in the namespace of this resource. It needs a storageRef and a backupStorageRef.
  clusterRef:
    # string. Required. Name of the CamundaCluster.
    name: my-cluster
  # string. Required. Five-field cron expression (minute, hour, day of month, month, day of week), evaluated in UTC.
  schedule: "0 2 * * *"
  # object. Optional. How many backups of this schedule are kept. The defaults below apply when you set nothing.
  retained:
    # integer. Optional, default: 7. How many completed backups the schedule keeps. Minimum 1.
    completed: 7
    # integer. Optional, default: 3. How many failed backups the schedule keeps. Minimum 0. Zero deletes a failed backup at the first look after it fails.
    failed: 3

Validation rules

  • spec.clusterRef.name is required and must not be empty. The cluster must live in the namespace of the schedule.
  • spec.schedule is required. The API server checks the shape only: five fields, separated by whitespace, of the cron characters. A value out of range, for example the minute 99, passes admission. The operator then reports InvalidReference on Ready. Seconds fields and @daily style descriptors are rejected at admission.
  • spec.retained.completed must be 1 or more. spec.retained.failed must be 0 or more.
  • The spec is mutable. Every field can be changed on a live schedule.

A production-shaped example

A nightly schedule that keeps two weeks of backups and only the last failure:

apiVersion: core.camunda.io/v1
kind: BackupSchedule
metadata:
  name: my-cluster-schedule
  namespace: my-cluster-ns
  labels:
    camunda.io/cluster: my-cluster
spec:
  clusterRef:
    name: my-cluster
  # 02:00 UTC every day, outside business hours.
  schedule: "0 2 * * *"
  retained:
    completed: 14
    failed: 1

kubectl get backupschedules -n my-cluster-ns -o wide then shows the cron expression, the cluster, the last schedule, and the last backup that the schedule created.

  • CamundaCluster: the cluster that the schedule backs up. Its suspension gates the triggers, and its spec.backup.primaryStorage.retention.window bounds how long a dump stays restorable.
  • SecondaryStorageConfig: its spec.type selects the backup kind that each trigger creates.
  • LogicalBackupElasticsearch: the backup kind for an Elasticsearch cluster.
  • LogicalBackupRDBMS: the backup kind for a relational cluster.
  • Backup guide: how to set up backup storage and take a backup.