PostgreSQL backups and recovery
When both the plan's backup.enabled and the service's
parameters.backup.enabled are true, a PostgreSQL service takes scheduled base
backups and archives its write-ahead log (WAL) continuously to object storage.
This page covers the backup policy, on-demand backups, the backup history, and
the two ways to restore: into a new service, or in place over the existing
service, from a selected backup or a point in time.
Restore a backup into a new service and check your data before you depend on it.
For other families and the shared restore request, see Back up and restore managed services. This page adds PostgreSQL backup settings, in-place restore, and point-in-time recovery.
Before you begin
- Your plan must set
backup.enabled, and the service'sparameters.backup.enabledmust staytrue, its default. Ask your provider for the values ofbackup.schedule,backup.retentionDays,backup.minRetentionDays,backup.maxRetentionDaysandbackup.minScheduleIntervalMinutes, and whether the plan setsbackup.pitr. - The
adminordeveloperrole to change a service, create operations or create a service from a backup. Every Project role can read the backup history. See Project roles. - For the in-Project placement this chapter covers, deleting a Project deletes its services and their data volumes whatever their deletion settings, without a final backup, and also removes the Project's backup history records. Copy out any data you need first. See Delete a Project.
Throughout this page, replace my-project with your Project's backing
namespace.
Backup policy
Three parameters control scheduled backups. All three can change after
creation: edit them in the complete ManagedService manifest and apply it
again.
spec:
parameters:
backup:
enabled: true
# Five-field cron expression, in UTC: every six hours.
schedule: "0 */6 * * *"
retentionDays: 14
| Parameter | Rules |
|---|---|
backup.enabled | Default true. Scheduled backups and WAL archiving run only when this parameter and the plan's backup.enabled are both true |
backup.schedule | Five fields, in UTC. Lists, ranges, steps and day or month names work. Seconds, @ shortcuts and time zone prefixes are refused. Runs must be at least the plan's backup.minScheduleIntervalMinutes apart, 60 minutes when the plan does not set it. Omit it to inherit the schedule that applies from your plan |
backup.retentionDays | Must be within the plan's backup.minRetentionDays and backup.maxRetentionDays, 1 to 365 days when the plan does not set them. Omit it to inherit the retention that applies from your plan |
A value outside the plan's bounds is refused: the service reports
Accepted=False with reason ParameterSchemaRejected and a message that
contains must be between <min> and <max> for this plan or
runs must be at least <minutes> minutes apart for this plan.
What the settings do:
- Turning backups off removes the backup schedule and WAL archiving configuration. Completed backups are not deleted, but the point-in-time recovery window stops advancing.
- Turning backups on again creates the schedule with an immediate first backup.
- Lowering
retentionDayscan permanently remove older recovery points. Reverting the manifest, for example with a Git rollback, does not bring them back. Retention describes a recovery window, not an exact age at which every object is removed. - A backup can take longer than the interval between scheduled runs.
The service reports the policy in force in status.engineDetails:
kubectl get managedservice orders-db -n my-project \
-o jsonpath='enabled={.status.engineDetails.backup\.enabled} schedule={.status.engineDetails.backup\.schedule} retention={.status.engineDetails.backup\.retentionPolicy} last={.status.engineDetails.backup\.lastScheduledAt} next={.status.engineDetails.backup\.nextScheduledAt}{"\n"}'
| Key | Meaning |
|---|---|
backup.enabled | Whether backups are configured on the database |
backup.schedule, backup.timeZone | The schedule in force and its time zone, UTC |
backup.retentionPolicy | The retention in force |
backup.lastScheduledAt, backup.nextScheduledAt | The last and next scheduled start |
backup.destination | Where backups are written |
Until an edit is applied, these keys show the previous values; confirm that the
configuration is current as described in
Read the status. A nextScheduledAt
value is not proof that a backup completed; use status.lastBackup and the
backup history for that.
Take a backup now
Create a Backup operation. It takes no parameters:
apiVersion: services.kube-dc.com/v1alpha1
kind: ServiceOperation
metadata:
name: orders-db-backup-1
namespace: my-project
spec:
serviceRef:
name: orders-db
# The service's metadata.uid.
serviceUID: REPLACE_WITH_SERVICE_UID
type: Backup
idempotencyKey: orders-db-backup-1
execution:
window: Immediate
kubectl apply -f orders-db-backup-1.yaml
kubectl get serviceoperation orders-db-backup-1 -n my-project -w
When the operation succeeds, status.result includes backupName. The operation is Rejected with BackupNotConfigured when backups
are not configured for the service, and with ClusterHibernated while the
service is hibernated. See Day-2 Operations for
phases and approval.
To find the history record of this backup, match on backupName, not on the
time. A scheduled backup can complete at nearly the same time:
BACKUP_NAME=$(kubectl get serviceoperation orders-db-backup-1 -n my-project -o jsonpath='{.status.result.backupName}')
kubectl get servicebackups -n my-project \
-o jsonpath="{range .items[?(@.spec.backupName==\"$BACKUP_NAME\")]}{.metadata.name}{\"\t\"}{.status.backup.phase}{\"\t\"}{.status.backup.id}{\"\n\"}{end}"
Backup history
The platform records each observed backup of a service as a ServiceBackup in
the Project. Project roles can read the records but cannot create, change or
delete them. A record has no owner, so it is kept after its service is deleted.
Deleting the Project removes the records. See
What remains after deletion.
List the backups of a service by its UID:
kubectl get servicebackups -n my-project \
-l services.kube-dc.com/service-uid=REPLACE_WITH_SERVICE_UID
Show the fields you need to choose a restore point:
kubectl get servicebackups -n my-project \
-l services.kube-dc.com/service-uid=REPLACE_WITH_SERVICE_UID \
-o custom-columns='NAME:.metadata.name,ORIGIN:.spec.origin,PHASE:.status.backup.phase,BACKUP_ID:.status.backup.id,MAJOR:.status.backup.engineMajor,COMPLETED:.status.backup.completedAt,PITR_FROM:.status.backup.recoveryWindow.earliest,PITR_TO:.status.backup.recoveryWindow.latest'
To find backups of a service that no longer exists, list all records in the Project and read the source name and UID from each record:
kubectl get servicebackups -n my-project \
-o custom-columns='SOURCE:.spec.serviceRef.name,SOURCE_UID:.spec.serviceUID,ORIGIN:.spec.origin,PHASE:.status.backup.phase,BACKUP_ID:.status.backup.id,COMPLETED:.status.backup.completedAt'
| Field | Meaning |
|---|---|
spec.serviceRef.name, spec.serviceUID | The service the backup belongs to. The name can be reused; the UID cannot |
spec.origin | Manual (a Backup operation), Scheduled, Final (the final backup of a service deleted with SnapshotAndDelete) or Other |
spec.backupName | The name the backup had when it was taken. A Backup operation reports it in status.result.backupName |
spec.planName | The plan of the service at the time. Provenance only |
status.backup.phase | Pending, Running, Completed or Failed |
status.backup.id | Physical backup ID. In-place restore uses this value. Restore-to-new uses the catalog record name and UID |
status.backup.engineMajor | The PostgreSQL major version that wrote the backup |
status.backup.startedAt, status.backup.completedAt | When the backup started and completed |
status.backup.message | A message from the backup, for example why it failed |
status.backup.recoveryWindow | The verified point-in-time recovery window of this backup: earliest, latest, timeline and verifiedAt. See Point-in-time recovery |
Only a record with phase Completed and a status.backup.id can be a restore
source. Completed means the backup completed at the recorded time. It does not
mean that the backup data is still retained in object storage. Before it creates
a database from a selected backup, a restore verifies that the backup exists in
the archive. Records have no automatic expiry, so a record can outlive its data.
status.lastBackup and status.recentBackups on the ManagedService show only
a bounded recent window. On a ServiceBackup, status.backup.lastArchivedWALTime
is a legacy diagnostic, not a point-in-time recovery limit; use
status.backup.recoveryWindow. The same holds for
status.lastBackup.lastArchivedWALTime on the ManagedService.
Choose a restore
The two restore paths use different requests:
| Property | Restore into a new service | Restore in place |
|---|---|---|
| Request | RestoreToNew operation with spec.restore | RestoreInPlace operation with spec.parameters |
| Source service | Can remain active or can already be deleted | Must exist with the exact service and engine UIDs |
| Data | Creates another service | Replaces existing data. Later changes are lost |
| Identity | New service name, UID, and credentials | Existing service identity remains |
| Backup selection | Exact ServiceBackup name and UID, with optional targetTime | Physical backupID or targetTime |
| Plan policy | Target plan must support the family, version, and RestoreToNew | Existing plan must permit RestoreInPlace |
Restore into a new service
A RestoreToNew operation selects a retained catalog record and an explicit target configuration.
Do not create ManagedService.spec.restoreFrom yourself. The platform generates that field and binds it to the restore operation.
Before you start, confirm that the target plan permits the backup's family and engine major version. Use a full engine release qualified by that plan. A restore does not perform a major upgrade.
-
Read the selected backup record:
kubectl get servicebackup <backup-record> -n <project> -o yamlReplace
<backup-record>with the catalog record name and<project>with the Project namespace. Checkstatus.backup.phase,status.recovery, and the archive retention deadline. Recordmetadata.uid,spec.serviceRef.name, andspec.serviceUID. -
Save this request as
restore-orders.yaml:apiVersion: services.kube-dc.com/v1alpha1kind: ServiceOperationmetadata:name: restore-orders-1namespace: <project>spec:serviceRef:name: <source-service>serviceUID: <source-uid>type: RestoreToNewidempotencyKey: restore-orders-1restore:backupRef:name: <backup-record>uid: <backup-record-uid>target:name: orders-restoredplanRef:name: <target-plan>engineVersion: "<qualified-version>"placement:mode: ProviderShareddataPlaneRef:name: <data-plane>connectivity:classRef:name: tenant-nativestorage:size: 20Giparameters:database: ordersowner: ordersexecution:window: ImmediateReplace the source and backup placeholders with values from the selected record. Replace
<target-plan>,<qualified-version>, and<data-plane>with compatible values from your provider. Adjust storage and database names for the backup. Omitted capacity fields use target plan defaults. -
Validate the request:
kubectl apply --dry-run=server -f restore-orders.yaml -
Submit the request:
kubectl apply -f restore-orders.yaml -
Watch the operation:
kubectl get serviceoperation restore-orders-1 -n <project> -w -
After
Succeeded, check thatorders-restoredisReady. -
Create a binding with the restored service's own UID.
-
Verify the recovered data through that binding before you move application traffic.
The source service remains unchanged. The target receives fresh credentials and deletionPolicy: Retain.
Recreate required credential policies for the target.
The restore contract uses these fields:
| Field | Purpose |
|---|---|
spec.serviceRef.name, spec.serviceUID | Identify the historical source, including a deleted source |
spec.restore.backupRef | Pins the catalog record by name and UID |
spec.restore.targetTime | Optional PostgreSQL recovery time within this exact backup's verified window |
spec.restore.target | Defines the target plan, engine release, placement, connectivity, and optional sizing |
spec.parameters is invalid for RestoreToNew.
Use spec.restore.target.parameters only for engine settings.
The target plan controls approval and the maintenance window.
A dry run checks the request schema. It does not prove that the archive can be restored.
For point-in-time recovery, add a whole-second RFC 3339 spec.restore.targetTime.
Omit it to restore the selected backup's consistency point.
Use Point-in-time recovery to check the window.
When a restore into a new service is refused
A refused restore ends with the operation in phase Rejected. Read its
status.reason and status.message:
| Reason | Message | Meaning |
|---|---|---|
ParameterSchemaRejected | RestoreToNew requires typed spec.restore with exact backupRef and an explicit new target | spec.restore is missing or incomplete, spec.parameters is set, or the target has the source's name |
RestoreSourceInvalid | For example backup has no unexpired retained recovery authority, targetTime is outside this backup's verified recovery window or target engine family and version must match the selected backup writer major | The backup record, its retention, the recovery time or the target's engine release does not fit the request |
PlanNotEntitled | target plan does not allow RestoreToNew | Choose a target plan that allows it |
PlanQuotaExceeded | plan <plan> allows <n> instances per project; delete an instance on this plan or restore into another plan | The Project already has as many services on the target plan as its maxInstancesPerProject allows. No target is created |
OperationConflict | duplicate idempotencyKey; see <operation> | An earlier operation used the same idempotencyKey |
Every restored service counts toward its plan's maxInstancesPerProject, like
any other service on that plan. A service that was refused before the platform
placed it, in phase Rejected, does not count. You cannot read the plan, so
ask your provider for its limit, and count the services in the Project:
kubectl get managedservices -n <project>
A service can count although you do not see it, or see it as Rejected: a
deleted service while the platform has not finished removing it or has kept its
engine under Retain, and a service that was refused after the platform had
placed it. If a restore is refused although you count fewer services than the
limit, ask your provider.
If you restore regularly, for example to test recovery, delete each restored
service when you have finished with it. A restored service has
deletionPolicy: Retain, which keeps its engine and volumes in the Project.
To remove them, set Delete and the confirmation annotation before you delete
the service, as described in
Delete a service.
A target can also fail after it was created. The operation then ends Rejected
and reports the target's own reason:
| Target phase | Operation message | What the target reports |
|---|---|---|
Rejected | restore target is Rejected: <message> | Accepted=False with the same reason, for example PlanQuotaExceeded when another service took the last place on the plan in the meantime. While you do not edit the target, its message ends with its restore operation has ended: delete this target and restore again |
Failed | restore target is Failed: <message> | Placed=False when the platform could not place the target, otherwise Reconciled=False with RestoreSourceInvalid |
In both cases the target cannot continue. Delete it, remove the cause, and
submit a new operation with a new name and idempotencyKey.
Earlier releases reported a full plan differently: the operation ended with
reason AdapterFailed and the message restore target is Rejected, and the
target reported RestoreSourceInvalid with
restore execution claim does not authorize this target. If you see this pair,
compare the number of services on the target plan with the plan's limit.
Restore in place
A RestoreInPlace operation replaces the database engine and data of an
existing service from a backup of the same service, and keeps the service and
its connection identity.
An in-place restore is destructive. Everything written after the selected backup or time is lost. Take a backup first if you might need the current data.
Requirements
- Your plan must allow
RestoreInPlaceinoperations.allowed, and must setcapacity.allocationProtocol: v1andbackup.enabled. It must also offer the PostgreSQL major version that wrote the selected backup:allowedImagesmust include an image of that version or, whenallowedImagesis empty,engineVersionmust equal it. If the plan does not listRestoreInPlaceinoperations.autoApprove, the operation waits for approval by your provider. - The operation must set
serviceUID. spec.deletionProtectionon the service must befalsewhen the platform checks the operation. Turn it off in a separate update first.- Break-glass superuser access must be off. See Break-glass superuser access.
- The backup, or the recovery window that covers
targetTime, must belong to aCompletedbackup of this service.
Steps
We recommend this order:
-
Stop application writes to the service.
-
Take a
Backupand wait for it to succeed, so that the current state can still be restored into a new service. -
Set
deletionProtection: falsein the service manifest and apply it. Wait until the change is current. -
Read the current engine UID:
kubectl get managedservice orders-db -n my-project -o jsonpath='{.status.engineDetails.engineUID}{"\n"}' -
Choose the restore point from the backup history.
-
Create the operation. This example restores backup
REPLACE_WITH_BACKUP_ID:apiVersion: services.kube-dc.com/v1alpha1kind: ServiceOperationmetadata:name: orders-db-restore-in-place-1namespace: my-projectspec:serviceRef:name: orders-db# The service's metadata.uid. Required for RestoreInPlace.serviceUID: REPLACE_WITH_SERVICE_UIDtype: RestoreInPlaceidempotencyKey: orders-db-restore-in-place-1parameters:# status.engineDetails.engineUID, read just before submitting.engineUID: REPLACE_WITH_ENGINE_UIDacknowledgeDataLoss: true# status.backup.id of a Completed backup of this service.backupID: REPLACE_WITH_BACKUP_IDexecution:window: ImmediateTo restore to a point in time instead, replace
backupIDwithtargetTime:apiVersion: services.kube-dc.com/v1alpha1kind: ServiceOperationmetadata:name: orders-db-restore-in-place-2namespace: my-projectspec:serviceRef:name: orders-dbserviceUID: REPLACE_WITH_SERVICE_UIDtype: RestoreInPlaceidempotencyKey: orders-db-restore-in-place-2parameters:engineUID: REPLACE_WITH_ENGINE_UIDacknowledgeDataLoss: true# Inside a verified recovery window of this service.# We recommend UTC with whole seconds.targetTime: "2026-01-15T10:40:00Z"execution:window: Immediate -
Watch the operation until it reaches a final phase, then wait for the service to be
Ready:kubectl get serviceoperation orders-db-restore-in-place-1 -n my-project -wkubectl get managedservice orders-db -n my-project -wA successful operation reports
restored in place with preserved connection material; resynchronize any existing-user policies against the new engine UID. -
Check your data through an existing binding.
-
If the service has existing-user credential policies, resynchronize each of them. See After a restore.
-
Take a new
Backup. -
Set
deletionProtection: trueagain, and resume application writes.
| Parameter | Description |
|---|---|
engineUID | Must equal the service's current status.engineDetails.engineUID |
acknowledgeDataLoss | Must be true |
backupID | The status.backup.id of a Completed backup of this service |
targetTime | A point in time in RFC 3339 format |
Set exactly one of backupID and targetTime. Any other parameter is refused.
What stays and what changes
| Stays the same | Changes |
|---|---|
The service name and metadata.uid | status.engineDetails.engineUID |
ServiceBinding objects; an owner binding and its CA were verified to keep working | The engine and its data volumes are replaced |
| Credential policies and existing backup history records | Data written after the restore point is gone |
| No additional preserved field | Backups taken afterwards use a new archive location and are recorded under the same service UID |
| No additional preserved field | Existing SQL logins managed by credential policies need a resync |
While the restore runs
- The database is being replaced; applications should retry.
- An operation that is already running is not cancelled, and deleting its record does not stop it. Do not create a second restore operation.
- If the operation stays
Runningfor a long time, ask your provider.
Refusals
When the initial validation refuses an in-place restore, the operation is
Rejected with one of these reasons:
| Reason | Message | Fix |
|---|---|---|
RestoreSourceInvalid | in-place restore requires the exact service UID, no deletion in progress and deletionProtection=false | Set serviceUID, and set deletionProtection: false first |
RestoreSourceInvalid | in-place restore requires acknowledgeDataLoss=true, current engineUID and exactly one backupID or targetTime | Read engineUID again; it changes after every in-place restore. Check the selector |
RestoreSourceInvalid | no verified same-service catalog backup covers the requested recovery point | The backup ID does not belong to a Completed backup of this service, or targetTime is outside every verified window |
PlanNotEntitled | plan <plan> does not allow RestoreInPlace | Your plan does not allow in-place restore |
Before you create a new operation, make sure the refused one did not already
start the restore: an in-place restore can be rejected after execution has
begun. When you cannot tell, keep the operation and ask your provider for its
outcome. When retrying is safe, fix the cause and create a new operation with a
new name and idempotencyKey.
Point-in-time recovery
A targetTime restores the database to a point in time, using a base backup
and the archived WAL after it. The plan field backup.pitr advertises this
capability. For each backup, the platform publishes a window of times it has
verified against the archive.
Read the windows from the backup history:
kubectl get servicebackups -n my-project \
-l services.kube-dc.com/service-uid=REPLACE_WITH_SERVICE_UID \
-o custom-columns='BACKUP_ID:.status.backup.id,COMPLETED:.status.backup.completedAt,EARLIEST:.status.backup.recoveryWindow.earliest,LATEST:.status.backup.recoveryWindow.latest,TIMELINE:.status.backup.recoveryWindow.timeline,VERIFIED:.status.backup.recoveryWindow.verifiedAt'
How a target is chosen and used:
- The target must be inside the
earliesttolatestwindow of aCompletedbackup of the service, and after that backup's completion. RestoreToNewuses the exact backup selected inrestore.backupRef.- An in-place restore selects a suitable same-service backup for its requested time.
- Recovery stops before the first transaction committed after the target.
- We recommend UTC with whole seconds, for example
2026-01-15T10:40:00Z.
The window trails the present:
- Recent changes become restorable only after their WAL has been archived and
the platform has verified it. A target from the last few minutes can be
refused; wait until
latestpasses your target, then submit a new request. - When backups are turned off, the window stops advancing.
A target outside every verified window is refused before any database is
created or replaced. Read the restore operation's status.reason and status.message for the refusal.
Limits
These cases are not yet qualified. Do not rely on them without testing, and ask your provider if you need them:
- Point-in-time recovery to a time across a PostgreSQL timeline change. A switchover, failover or in-place restore starts a new timeline. We recommend taking a new backup after such an event.
- Point-in-time recovery when WAL archiving had gaps or failures.
- Restoring a backup whose retention period has expired.
- Creating a new service from a backup that was taken after an in-place restore.
- Recovery from an interruption at every stage of an in-place restore, and the remaining failure and retry cases.