# Soft Delete

Soft delete protects your bucket against accidental deletion. When soft delete is enabled, deleting an object does not immediately remove its data. Tigris keeps the deleted object for a configurable retention window during which you can list it, restore it, or permanently remove it. Once the retention window expires, Tigris cleans the data up automatically.

The same retention policy also protects the bucket itself: deleting a bucket with soft delete enabled moves the bucket into a soft-deleted state instead of hard deleting it. A soft-deleted bucket can be restored back into an active bucket within the retention window, and is permanently removed once the retention window expires.

## How soft delete works[​](#how-soft-delete-works "Direct link to How soft delete works")

When soft delete is enabled on a bucket:

* A delete on an object marks the object as soft-deleted instead of removing its data. The object stops appearing in regular listings and reads, but its data is preserved and still counts toward your bucket's object count and storage usage until it is permanently removed.
* Soft-deleted objects remain restorable for the retention window. You can list them, restore them to a live state, or permanently delete them ahead of the schedule.
* Deleting a bucket with soft delete enabled moves the bucket itself into a soft-deleted state. The bucket and its data stay recoverable for the retention window.
* After the retention window passes, Tigris permanently removes the soft-deleted data and reclaims its storage.
* Soft-deleted data continues to consume storage and is billed at the same rate as live data until it is removed.

At a glance, the lifecycle looks like this:

```
     DELETE        ──►   soft-deleted state   ──┬──►   permanently removed

bucket or object         (recoverable)          │      (async, after retention)

                                                │

                              ◄─── restore ─────┘
```

Cleanup runs asynchronously, so data is removed shortly after the retention window passes.

## Enabling soft delete on a bucket[​](#enabling-soft-delete-on-a-bucket "Direct link to Enabling soft delete on a bucket")

Soft delete is configured per bucket from the Tigris Dashboard.

You can enable it when creating a bucket by toggling **Enable Soft Delete** in the Create Bucket dialog and choosing a retention window:

![Enable soft delete during bucket creation](/docs/assets/images/soft-delete-create-bucket-1708d231801cdd9fd0c18c77f1a10e90.png)

For an existing bucket, open **Bucket Settings → Data Management** and toggle **Enable Soft Delete**:

![Enable soft delete from bucket settings](/docs/assets/images/soft-delete-settings-79046af0d8db6e6390cd08075d72329d.png)

A **Soft Delete** column on the buckets list marks which buckets have soft delete enabled, showing **Enabled** or **Disabled** for each row.

![Buckets list with the Soft Delete column and the Deleted item in the sidebar](/docs/assets/images/soft-delete-col-segment-2b252b1846069225d2654111e9b418f6.png)

### Retention window[​](#retention-window "Direct link to Retention window")

The retention window controls how long deleted buckets and objects remain recoverable. It must be between **7 and 90 days**, and defaults to **7 days** when soft delete is first enabled.

### Enabling soft delete programmatically[​](#enabling-soft-delete-programmatically "Direct link to Enabling soft delete programmatically")

Set the `X-Tigris-Soft-Delete` header on `CreateBucket` — use `true` for the default 7-day retention, or a number between `7` and `90` for a custom window.

```
PUT /my-bucket HTTP/1.1

Host: t3.storage.dev

X-Tigris-Soft-Delete: 30
```

The Go SDK and the CLI take the retention window as an argument instead. The TypeScript SDK has no soft-delete option on bucket creation, so it sets the window after the bucket exists.

* Go
* TypeScript
* CLI

```
import (

	"log"



	"github.com/aws/aws-sdk-go-v2/aws"

	"github.com/aws/aws-sdk-go-v2/service/s3"

	"github.com/tigrisdata/storage-go"

)



client, err := storage.New(ctx)

if err != nil {

	log.Fatal(err)

}



_, err = client.CreateBucketWithSoftDelete(ctx, &storage.CreateBucketWithSoftDeleteInput{

	CreateBucketInput: &s3.CreateBucketInput{Bucket: aws.String("my-bucket")},

	RetentionDays:     30,

})

if err != nil {

	log.Fatal(err)

}
```

For a bucket that already exists, `SetBucketSoftDelete` turns soft delete on or off. `RetentionDays` is ignored when disabling, and `0` selects the default 7-day window:

```
_, err = client.SetBucketSoftDelete(ctx, &storage.SetBucketSoftDeleteInput{

	Bucket:        "my-bucket",

	Enabled:       true,

	RetentionDays: 30,

})

if err != nil {

	log.Fatal(err)

}
```

`createBucket` has no soft-delete option, so turn it on with `updateBucket` after the bucket exists:

```
import { updateBucket } from "@tigrisdata/storage";



await updateBucket("my-bucket", {

  softDelete: { enabled: true, retentionDays: 30 },

});
```

`retentionDays` must be between 7 and 90. The API enforces the range, not the SDK, so an out-of-range value fails on the request rather than at the call site.

```
tigris buckets set my-bucket --soft-delete enable --retention-days 30
```

`--retention-days` is required when enabling, and must be between 7 and 90. Turn it back off with `--soft-delete disable`:

```
tigris buckets set my-bucket --soft-delete disable
```

`tigris buckets get my-bucket` shows the current setting as a **Soft Delete** row, e.g. `Enabled (30 day retention)`.

warning

Enabling soft delete does not take effect immediately — it takes a few seconds to reach the data plane. An object deleted inside that window is hard deleted and is not recoverable, no matter how you look for it afterwards; in testing, a delete issued within the first second of enabling was unrecoverable, while deletes a few seconds later were recoverable. Automation that enables soft delete and then deletes objects must wait before it relies on recovery.

## Deleting a bucket with soft delete enabled[​](#deleting-a-bucket-with-soft-delete-enabled "Direct link to Deleting a bucket with soft delete enabled")

When you delete a bucket that has soft delete enabled, the delete confirmation dialog reflects that the action is recoverable instead of permanent. It tells you the bucket can be restored within its retention period — and that after that window passes, the deletion is permanent. The action button is labeled **Soft Delete** rather than **Delete**, and you still confirm by typing the bucket name.

![Delete bucket confirmation dialog for a soft-delete-enabled bucket](/docs/assets/images/soft-delete-bucket-modal-7a4acdb3351a79e845d8311743993dd2.png)

This is the cue that the bucket is moving into the soft-deleted state covered below rather than being permanently removed.

## Restoring a deleted bucket[​](#restoring-a-deleted-bucket "Direct link to Restoring a deleted bucket")

A bucket deleted while soft delete was enabled is not removed immediately. It moves to a soft-deleted state where it is hidden from your normal bucket list but stays recoverable until the retention window expires.

In the Tigris Dashboard sidebar, **Buckets** expands into **All**, **Forks**, **Owned by me**, and **Deleted**. Every bucket that was deleted while soft delete was enabled appears under **Deleted**. Each row in this view is a soft-deleted bucket that is still within its retention window.

To restore a bucket, open **Buckets → Deleted** in the sidebar. Then open the row's overflow menu (`...`) and choose **Restore**:

![Restore a soft-deleted bucket from Buckets, Deleted in the sidebar](/docs/assets/images/soft-delete-restore-bucket-fd8214e94f849db9ae6e80b4a05e5d00.png)

After a successful restore, the bucket is fully active again — it moves back to the regular bucket list and every object it held at the time of deletion is live and readable, exactly as it was before the bucket was deleted. If you want to remove a soft-deleted bucket immediately instead of waiting for the retention window to expire, choose **Delete** from the same menu.

While a bucket is in the soft-deleted state, its name stays reserved — you cannot create a new bucket with the same name until either the bucket is restored or permanently removed.

### Restoring a bucket programmatically[​](#restoring-a-bucket-programmatically "Direct link to Restoring a bucket programmatically")

A soft-deleted bucket is hidden rather than gone: `HeadBucket` answers 404 and so do its objects. Send `restore` to bring it back:

```
POST /my-bucket?restore HTTP/1.1

Host: t3.storage.dev
```

The response is 200 with an empty body. The bucket and every object it held are live again, and listings return them as before.

The AWS S3 SDKs have no bucket-restore operation, so over those this is a plain signed POST. The Tigris SDKs and the CLI wrap it:

* Go
* TypeScript
* CLI

```
_, err := client.RestoreBucket(ctx, &storage.RestoreBucketInput{Bucket: "my-bucket"})

if err != nil {

	log.Fatal(err)

}
```

```
import { restoreBucket } from "@tigrisdata/storage";



await restoreBucket("my-bucket");
```

List the recoverable buckets, then restore one:

```
tigris buckets list --deleted

tigris buckets restore my-bucket
```

Restoring a bucket that is already active returns 400.

## Finding soft-deleted objects[​](#finding-soft-deleted-objects "Direct link to Finding soft-deleted objects")

Inside a soft-delete-enabled bucket, the files browser splits into an **All files** and a **Deleted files** segment above the list. **All files** is the regular live view; **Deleted files** is where every soft-deleted object in this bucket shows up while it's within its retention window.

![Files browser with All files and Deleted files segments](/docs/assets/images/soft-delete-files-segment-b74b0cbba817419afff3b411b4dc3cbf.png)

## Deleting an object in a soft-delete-enabled bucket[​](#deleting-an-object-in-a-soft-delete-enabled-bucket "Direct link to Deleting an object in a soft-delete-enabled bucket")

Deleting an object from the **All files** view brings up a confirmation dialog that mirrors the bucket-level one. It tells you the file will be moved to the **Deleted files** tab and can be restored within the bucket's retention period — and that after that window passes, the file is permanently removed.

![Soft delete confirmation dialog for an object](/docs/assets/images/soft-delete-object-delete-modal-92c1533fba8089e332124ef18f56fa36.png)

## Restoring a deleted object[​](#restoring-a-deleted-object "Direct link to Restoring a deleted object")

Every delete on a key produces its own soft-deleted version, stamped with the time it was deleted. The same key can therefore have many soft-deleted versions stacked up if it has been written and deleted more than once. For example, if you write an object at key `a`, delete it, write a fresh copy at `a`, delete that, then repeat one more time, the key will have three soft-deleted versions — one for each delete — all independently restorable until their retention windows expire.

In the **Deleted files** segment, selecting a deleted object opens its **Version history** panel on the right, which lists each soft-deleted version of that key along with its timestamp, version ID, size, and ETag. To restore the object, pick the version you want to recover and click **Restore this version**:

![Restore a soft-deleted object from the Deleted files tab](/docs/assets/images/soft-delete-restore-object-d4a06468e6590c92e80f3592b651211b.png)

The selected version becomes live immediately and is visible to all subsequent reads.

To restore objects programmatically, or to restore many at once, see [Working with soft delete programmatically](#working-with-soft-delete-programmatically) below.

## Permanently deleting an object[​](#permanently-deleting-an-object "Direct link to Permanently deleting an object")

Permanently deleting a soft-deleted object is always scoped to a specific version of that object. There is no single "delete the object" button — because the same key can have many soft-deleted versions stacked up, you pick which version to purge from the **Version history** panel.

To do this, open the **Version history** panel for the object in the **Deleted files** segment, select the version you want to purge, and click the red **Delete** button at the bottom of the panel. The panel shows a **Permanently delete this file?** confirmation just above the button, calling out that the version will be permanently deleted and cannot be restored.

![Permanently delete a soft-deleted version from the Version history panel](/docs/assets/images/soft-delete-permanent-delete-version-509108a1913ea01df1ec1dc94f3c4a32.png)

Unlike the regular delete from the **All files** view, this action bypasses the retention window entirely — the selected version is purged immediately and is not recoverable. To fully remove an object that has multiple soft-deleted versions, repeat this for each version you want gone.

## Working with soft delete programmatically[​](#working-with-soft-delete-programmatically "Direct link to Working with soft delete programmatically")

The **Deleted files** view and the actions in the Tigris Dashboard are also available through the S3 API: list the deleted objects in a bucket, restore one of them, or delete one permanently before its retention window ends. The object operations are standard S3 requests carrying one Tigris header.

Objects that you deleted before you enabled soft delete are not recoverable.

The Tigris SDKs wrap each of these calls.

### Listing the deleted objects in a bucket[​](#listing-the-deleted-objects-in-a-bucket "Direct link to Listing the deleted objects in a bucket")

`ListObjectVersions` returns every deleted version of every key, each with the version ID that a restore or a permanent delete needs. It accepts `prefix`, `delimiter`, `max-keys`, and the usual pagination parameters, and it works on a bucket without versioning.

```
GET /my-bucket?versions&prefix=contracts/ HTTP/1.1

Host: t3.storage.dev

X-Tigris-Soft-Delete: true
```

`LastModified` on each entry is the time of the delete, not the time of the last write. A key deleted more than once has one entry for each delete, newest first.

* Go
* TypeScript
* CLI

[`storage-go`](https://github.com/tigrisdata/storage-go) wraps the listing, so there is no header to attach.

```
import (

	"fmt"

	"log"



	"github.com/tigrisdata/storage-go"

)



client, err := storage.New(ctx)

if err != nil {

	log.Fatal(err)

}



// Every deleted version of every key, newest delete first.

in := &storage.ListSoftDeletedObjectsInput{Bucket: "my-bucket", Prefix: "contracts/"}



deleted, err := client.ListSoftDeletedObjects(ctx, in)

if err != nil {

	log.Fatal(err)

}



for _, obj := range deleted.Objects {

	// LastModified is the time of the delete. VersionID is what a restore takes.

	fmt.Println(obj.Key, obj.VersionID, obj.Size, obj.LastModified)

}



// Carry the markers forward while deleted.IsTruncated is true.

in.KeyMarker = deleted.NextKeyMarker

in.VersionIDMarker = deleted.NextVersionIDMarker
```

```
import { listVersions } from "@tigrisdata/storage";



// Every deleted version of every key, newest delete first.

const { data: deleted, error } = await listVersions({

  deleted: true,

  prefix: "contracts/",

});



if (error) {

  throw error;

}



for (const version of deleted.versions) {

  // lastModified is the time of the delete. versionId is what a restore takes.

  console.log(

    version.name,

    version.versionId,

    version.size,

    version.lastModified,

  );

}



// Carry the markers forward while deleted.hasMore is true.

if (deleted.hasMore) {

  await listVersions({

    deleted: true,

    prefix: "contracts/",

    keyMarker: deleted.nextKeyMarker,

    versionIdMarker: deleted.nextVersionIdMarker,

  });

}
```

`--deleted` switches the listing over to the deleted objects. Use `list-versions` when you need the version IDs that a restore or a permanent delete takes:

```
# Every deleted version of every key under the prefix, with version IDs.

tigris objects list-versions my-bucket --prefix contracts/ --deleted
```

`objects list --deleted` answers the simpler question — which keys are deleted — without the per-version detail:

```
tigris objects list my-bucket --prefix contracts/ --deleted
```

`--prefix` works on both. Paging flags do not carry across: `objects list` takes `--limit` and `--page-token`, and `objects list-versions` takes `--limit`, `--key-marker`, and `--version-id-marker`. For scripting, `--format json` emits the same shape as the SDK:

```
tigris objects list-versions my-bucket --prefix contracts/ --deleted --format json \

  | jq -r '.versions[] | "\(.name)\t\(.versionId)\t\(.lastModified)"'
```

When the result is paginated, the table output prints the `--key-marker` and `--version-id-marker` values for the next page.

| Field          | Meaning                                                                                       |
| -------------- | --------------------------------------------------------------------------------------------- |
| `Key`          | Key of the deleted object.                                                                    |
| `VersionId`    | ID of this deleted version, in nanoseconds. A restore or a permanent delete takes this value. |
| `LastModified` | Time of the delete.                                                                           |
| `Size`, `ETag` | Size and ETag of the deleted object.                                                          |
| `SoftDeleted`  | Tigris extension. `true` on every entry of this listing.                                      |

Deleted objects come back as `Version` elements, not as `DeleteMarker` elements. The AWS `DeleteMarker` type has no field for a size or an ETag. An AWS SDK reads these entries as object versions, so a `listVersions()` call finds them in `versions` and not in `deleteMarkers`.

Two things to know about this listing:

* `IsLatest` is always `false` on a deleted entry.
* `GetObject` and `HeadObject` never read a deleted object. Restore the object first.

### Restoring an object[​](#restoring-an-object "Direct link to Restoring an object")

Send `RestoreObject` with `X-Tigris-Restore-Type: soft-delete`:

```
POST /my-bucket/contracts/2026/q1.pdf?restore HTTP/1.1

Host: t3.storage.dev

X-Tigris-Restore-Type: soft-delete

X-Tigris-Restore-Version: 1787441627070249004
```

The response is 200 with an empty body, and the object is immediately live for all subsequent reads. The request body is ignored.

`X-Tigris-Restore-Version` takes a `VersionId` from the versions listing. Without this header, Tigris restores the most recent deleted version of the key.

* Go
* TypeScript
* CLI

```
// An empty VersionID restores the most recent deleted version of the key.

_, err := client.RestoreSoftDeletedObject(ctx, &storage.RestoreSoftDeletedObjectInput{

	Bucket:    "my-bucket",

	Key:       "contracts/2026/q1.pdf",

	VersionID: "1787441627070249004",

})

if err != nil {

	log.Fatal(err)

}
```

```
import { restoreDeletedObject } from "@tigrisdata/storage";



const { error } = await restoreDeletedObject(

  "contracts/2026/q1.pdf",

  "1787441627070249004",

);



// These calls return an error rather than throwing one.

if (error) {

  throw error;

}
```

```
tigris objects restore-deleted my-bucket contracts/2026/q1.pdf \

  --version-id 1787441627070249004
```

`--version-id` is required. Take the value from `tigris objects list-versions <bucket> --deleted`.

A restore keeps the content, the ETag, the content type, the user metadata, and the storage class of the deleted object. It gives the object a new `Last-Modified`, which is the time of the restore.

warning

A restore replaces a live object at the same key. The replaced object becomes a new deleted version.

The deleted version stays in the deleted listing until its retention window ends, also after a restore. A second restore of the same version writes the same content again.

These are the errors of a restore:

* 400 `InvalidArgument` / `Soft-delete is not enabled on this bucket` — soft delete is off on the bucket.
* 400 `InvalidArgument` / `Invalid restore version: must be unix nano timestamp` — the header value is not a number.
* 404 `NoSuchKey` — the key has no deleted version, the retention window ended, or the version ID is not a version of this key.

### Restoring many objects[​](#restoring-many-objects "Direct link to Restoring many objects")

Tigris has no single bulk-restore call. Restore one key for each request. Each restore is one transaction, so a failure affects one key only. Repeat the run to pick up the keys that failed.

This example restores every object under a prefix that was deleted after a given time, using [`storage-go`](https://github.com/tigrisdata/storage-go):

```
package main



import (

	"context"

	"fmt"

	"log"

	"time"



	"github.com/tigrisdata/storage-go"

)



const (

	bucket = "my-bucket"

	prefix = "contracts/"

)



func main() {

	ctx := context.Background()

	deletedAfter := time.Date(2026, 8, 21, 12, 0, 0, 0, time.UTC)



	client, err := storage.New(ctx)

	if err != nil {

		log.Fatal(err)

	}



	// Every deleted version under the prefix that was deleted after the cutoff.

	var toRestore []storage.SoftDeletedObject



	in := &storage.ListSoftDeletedObjectsInput{Bucket: bucket, Prefix: prefix}

	for {

		page, err := client.ListSoftDeletedObjects(ctx, in)

		if err != nil {

			log.Fatal(err)

		}



		for _, obj := range page.Objects {

			// LastModified is the time of the delete.

			if obj.LastModified.After(deletedAfter) {

				toRestore = append(toRestore, obj)

			}

		}



		if !page.IsTruncated {

			break

		}



		in.KeyMarker = page.NextKeyMarker

		in.VersionIDMarker = page.NextVersionIDMarker

	}



	fmt.Printf("restoring %d objects\n", len(toRestore))



	for _, obj := range toRestore {

		_, err := client.RestoreSoftDeletedObject(ctx, &storage.RestoreSoftDeletedObjectInput{

			Bucket:    bucket,

			Key:       obj.Key,

			VersionID: obj.VersionID,

		})

		if err != nil {

			// One failure does not stop the run: each restore is its own

			// transaction, so the rest are unaffected.

			log.Printf("restore %s: %v", obj.Key, err)

			continue

		}



		fmt.Println("restored", obj.Key)

	}

}
```

`LastModified` is the time of the delete, so the comparison against `deletedAfter` selects the objects of one incident window. To restore the most recent delete of a key instead of a specific version, send the restore without `X-Tigris-Restore-Version`.

### Permanently deleting a version[​](#permanently-deleting-a-version "Direct link to Permanently deleting a version")

A permanent delete removes one deleted version before its retention window ends and frees its storage:

```
DELETE /my-bucket/contracts/2026/q1.pdf?versionId=1787441627070249004 HTTP/1.1

Host: t3.storage.dev

X-Tigris-Soft-Delete: true
```

The response is 204.

warning

A permanent delete cannot be undone. The object is not recoverable after this call.

* Go
* TypeScript
* CLI

```
// The version id is required. storage-go returns ErrMissingVersionID for an

// empty value, and the server refuses the call with 400 InvalidArgument.

_, err := client.PermanentlyDeleteObject(ctx, "my-bucket",

	"contracts/2026/q1.pdf", "1787441627070249004")

if err != nil {

	log.Fatal(err)

}
```

```
import { purgeDeletedObject } from "@tigrisdata/storage";



const { error } = await purgeDeletedObject(

  "contracts/2026/q1.pdf",

  "1787441627070249004",

);



if (error) {

  throw error;

}
```

```
tigris objects purge my-bucket contracts/2026/q1.pdf \

  --version-id 1787441627070249004
```

This prompts for confirmation because it cannot be undone. Pass `--yes` to skip the prompt in a script — the command refuses to run unattended without it.

Note this is `objects purge`, not `objects delete --version-id`: the latter hard-deletes a version of a *live* object on a versioned bucket, and is rejected when aimed at a soft-deleted version.

`versionId` names the version to remove, so it is required. A `DELETE` that carries the `X-Tigris-Soft-Delete` header without one is refused:

```
400 InvalidArgument

VersionId is required for permanent soft-delete removal
```

* Take the value of `versionId` from `VersionId` in the versions listing.
* This call is the one `DELETE` with a `versionId` that a bucket without versioning accepts. The `X-Tigris-Soft-Delete` header selects this behavior.
* A purge on a bucket without soft delete returns 400 `Soft-delete is not enabled on this bucket`.

## Soft delete on snapshot buckets[​](#soft-delete-on-snapshot-buckets "Direct link to Soft delete on snapshot buckets")

On a snapshot-enabled bucket, deleting a specific version moves that version into the soft-deleted state, where it appears in the **Deleted files** tab and can be restored from its Version history panel the same way. A plain delete on a snapshot-enabled bucket behaves as usual — it records a delete marker and leaves earlier versions live and accessible.

## Things to note[​](#things-to-note "Direct link to Things to note")

* Retention applies per bucket. Every soft-deleted bucket and object in the bucket uses the same retention window.
* Disabling soft delete on a bucket does not purge data that is already soft-deleted. It continues to age out on its original retention schedule.
* Changing the retention value affects new soft deletes from that point forward. Already soft-deleted data keeps the retention it was created with.
