Backups and restore¶
Backups are point-in-time snapshots of a managed (serverless) or BYOC index. Use them to protect against data loss, create copies of an index, or restore a previous state.
Pod-based indexes cannot be backed up. Their snapshot mechanism is a collection, which has
a different API. Restoring a collection needs an API version before 2026-07, and this SDK
cannot do it. See collections.
Create a backup¶
Pass the name of the index you want to back up, and optionally a name and description:
from pinecone import Pinecone
pc = Pinecone(api_key="your-api-key")
backup = pc.backups.create(
index_name="product-search",
name="pre-reindex-snapshot",
description="Backup before schema migration",
)
print(backup.backup_id) # e.g. "bk-abc123"
print(backup.status) # "Initializing"
create() returns as soon as the server accepts
the request. status is "Initializing" while the snapshot is built, then "Ready" or
"InitializationFailed". There is no timeout= to wait on, so poll
describe() until the status leaves
"Initializing".
pc.indexes.create_backup("product-search", name="pre-reindex-snapshot") is the same
operation under the indexes namespace.
List backups¶
list() returns a
BackupList holding one page, not every backup
in the project. To list all backups, follow the pagination token:
page = pc.backups.list(limit=100)
backups = {b.backup_id: b for b in page}
while page.pagination and page.pagination.next:
page = pc.backups.list(pagination_token=page.pagination.next)
backups.update({b.backup_id: b for b in page})
for backup in backups.values():
print(backup.backup_id, backup.name, backup.status)
The token names an offset, not a stable cursor. Backups created or deleted while you
walk the pages shift later offsets, so a walk can skip a backup or return one twice.
Keying by backup_id, as above, removes the duplicates. The token already carries the
page size, so the SDK drops limit from any call that passes a token.
For every backup of one index, use
list_backups(), which walks the pages for you:
for backup in pc.indexes.list_backups("product-search"):
print(backup.backup_id, backup.status)
pc.backups.list(index_name="product-search") also filters by index, but returns one
page like the project-wide call.
Backups of a deleted index¶
A backup outlives its source index. An index-scoped listing looks up the active index
with that name, so if every index that used the name has been deleted, the server
answers 404, not an empty list. Pass include_deleted=True to cover every index
that has ever used the name:
page = pc.backups.list(index_name="product-search", include_deleted=True, limit=100)
for backup in page:
if backup.source_index_deleted_at:
print(backup.backup_id, "source deleted at", backup.source_index_deleted_at)
A 404 with include_deleted=True means the name has never been used in this project.
The project-wide pc.backups.list() already includes backups of deleted indexes, and
passing include_deleted without index_name raises
PineconeValueError.
Describe a backup¶
backup = pc.backups.describe(backup_id="bk-abc123")
print(backup.source_index_name)
print(backup.status) # "Initializing" | "Ready" | "InitializationFailed"
print(backup.record_count)
print(backup.size_bytes)
if backup.schema is not None:
print(backup.schema.fields["your_vector"].metric)
schema is an IndexSchema, the same model
pc.indexes.describe(...).schema returns. It is None when the server returns no
schema for the backup, so check it before reading fields. dense_dimension is a
shortcut that reports the dimension when the schema has exactly one dense_vector
field, and None otherwise.
Restore a backup to a new index¶
create_index_from_backup() restores a backup into a new index.
It is the only way to restore; create() does not
accept a backup ID.
index = pc.create_index_from_backup(
name="product-search-restored",
backup_id="bk-abc123",
deletion_protection="enabled",
tags={"env": "production", "team": "search"},
)
print(index.name, index.status.state)
The call polls until the new index is ready, for 300 seconds by default, then raises
PineconeTimeoutError. The timeout does not cancel
the restore, which continues on the server; monitor it with the restore job below. Pass
a larger timeout to wait longer, or timeout=-1 to return as soon as the restore
starts:
result = pc.create_index_from_backup(
name="product-search-restored",
backup_id="bk-abc123",
timeout=-1,
)
print(result.restore_job_id)
What a restore does and does not carry over¶
Tags: tags you pass replace the backup’s tags. Omit them and the restored index gets the backup’s tags.
Deletion protection: never inherited. It defaults to
"disabled", so an index restored from a protected source comes back unprotected unless you passdeletion_protection="enabled".Read capacity: omit
read_capacityfor on-demand capacity, or pass a dedicated configuration to bring the index up on dedicated read nodes in the same call. The server rejects a dedicated configuration too small to hold the backup.
index = pc.create_index_from_backup(
name="product-search-restored",
backup_id="bk-abc123",
read_capacity={
"mode": "Dedicated",
"dedicated": {
"node_type": "t1",
"scaling": "Manual",
"manual": {"shards": 2, "replicas": 2},
},
},
)
The backup must be finished before you restore it. Restoring a backup whose status is
not yet "Ready" fails with FailedPreconditionError
(412), and the message says the backup “is not completed”.
Monitor restore jobs¶
Each call to create_index_from_backup starts a restore job. Describe one by ID:
job = pc.restore_jobs.describe(job_id="rj-abc123")
print(job.target_index_name)
print(job.status) # "Pending" | "Completed" | "Failed" | "Cancelled"
print(job.completed_at)
describe returns a RestoreJobModel. A restore
that is running reports "Pending"; there is no in-progress state, and
percent_complete is 100 once the job completes and None before that. Poll for a
terminal status:
import time
job = pc.restore_jobs.describe(job_id="rj-abc123")
while job.status == "Pending":
time.sleep(5)
job = pc.restore_jobs.describe(job_id="rj-abc123")
Warning
A NotFoundError from restore_jobs.describe does not
prove the job does not exist. The server also answers 404 on a transient failure and for
a job whose target index has been deleted. Treat a 404 as possibly transient unless you
know the ID is wrong.
list() returns a
RestoreJobList holding one page. Follow the token
to read more:
page = pc.restore_jobs.list(limit=100)
jobs = {j.restore_job_id: j for j in page}
while page.pagination and page.pagination.next:
page = pc.restore_jobs.list(pagination_token=page.pagination.next)
jobs.update({j.restore_job_id: j for j in page})
Warning
The restore-job listing is not exhaustive. Pages can overlap, pagination can end while
jobs remain, and jobs whose target index has been deleted are left out. Key the results
by restore_job_id as above, and never conclude a job does not exist because the
listing omits it. To track a specific restore, keep the restore_job_id that
create_index_from_backup(..., timeout=-1) returns and describe it.
Delete a backup¶
pc.backups.delete(backup_id="bk-abc123")
Deleting a backup does not affect the source index or any index restored from it. A
backup with a restore job still running cannot be deleted: the server answers with
FailedPreconditionError, naming the pending jobs. Wait
for the restore to finish, then delete.
Schedule automatic backups¶
A backup schedule takes backups of an index on a recurring cadence. Schedules live on
pc.backup_schedules and require a plan that includes backups; without it the server
answers ForbiddenError (403), even when the index does
not exist.
schedule = pc.backup_schedules.create(
index_name="product-search",
name="daily-compliance",
frequency="daily", # "daily" | "weekly" | "monthly"
retention_days=90,
)
print(schedule.schedule_id)
print(schedule.next_scheduled_run) # a datetime
The server picks the run time and reports it in next_scheduled_run; you cannot choose
an hour or timezone. retention_days is how long each produced backup is kept; the
schedule model reports it back as retention_expire_after_days. Each backup is named
"{name}-{run timestamp}", such as "daily-compliance-20260403T060000Z".
An index can have only one enabled schedule. Creating a second fails with
ConflictError (409). Pod-based indexes cannot have
schedules.
List the schedules on an index¶
Schedules are listed per index, disabled ones included.
iter_schedules() walks every
page; list() returns one.
for schedule in pc.backup_schedules.iter_schedules(index_name="product-search"):
print(schedule.schedule_id, schedule.frequency, schedule.enabled)
Describe, update, and delete a schedule¶
schedule = pc.backup_schedules.describe(schedule_id="sched-abc123")
update changes only the arguments you pass:
paused = pc.backup_schedules.update(schedule_id="sched-abc123", enabled=False)
print(paused.next_scheduled_run) # None while disabled
pc.backup_schedules.update(
schedule_id="sched-abc123", frequency="weekly", retention_days=30
)
Warning
Re-enabling a paused schedule with enabled=True plans its next run from the time of
the update, not from its old slot, so a pause shifts the cadence. Re-enabling raises
ConflictError if another schedule on the index is
already enabled.
pc.backup_schedules.delete(schedule_id="sched-abc123")
Deleting a schedule stops future runs. Backups it already produced are kept until their
retention period ends. A repeated delete of the same schedule_id raises
NotFoundError, so when retrying a delete whose
response was lost, treat a 404 as success.
Inspect what a schedule has produced¶
for run in pc.backup_schedules.iter_history(schedule_id="sched-abc123"):
print(run.backup_id, run.status, run.record_count)
Each history row is a backup the schedule produced or has planned. The next run is
listed as soon as it is planned, with status "Initializing" until it finishes, so filter
on run.status == "Ready" for completed backups. Fields such as record_count and
size_bytes can be None on rows that are not finished.
history() returns one page;
iter_history walks all of them.
See also¶
BackupModel: backup response modelRestoreJobModel: restore job modelBackupScheduleModel: schedule response modelBackupScheduleHistoryItem: one run produced by a scheduleServerless indexes: creating and configuring the indexes you back up
Collections: the pod-based snapshot mechanism