This guide walks through the workflow one stage at a time. For convenience, the complete, runnable script is available in the
doc-examples repository, which assembles every stage into a single file with one subcommand per stage.Use case
Use this pattern in the following situations:- The data is a replica and the source of truth lives elsewhere.
- You want to reduce storage cost or attack surface by removing data your workloads no longer access.
- A bucket contains a mix of active and inactive objects and you want to remove only the inactive ones, leaving active objects untouched.
- You are consolidating from one bucket to another, for example a regional migration, and want to confirm the old bucket is unused before decommissioning.
- A small probability of incorrectly identifying an accessed object as cold is acceptable.
- You require a hard guarantee that an object hasn’t been accessed.
- You have a hard regulatory obligation to delete data by a specific deadline. Use a lifecycle policy with expiration rules instead.
- You need auditable proof of deletion timing for compliance, for example regulated right-to-erasure workflows with service-level agreements.
- AI Object Storage is the system of record for the data and reimporting it is impossible or prohibitively expensive.
- Objects must be retained for a fixed period regardless of access.
Prerequisites
Before running this workflow, confirm the following:-
Inventory reports include
LastAccessedDate. This field isn’t part of every inventory schema by default. You must select it when you configure the inventory report, and it depends on access tracking being active for the bucket. IfLastAccessedDateis absent, the identification filter silently skips every row and produces an empty candidates list. Verify the field appears in the report’smanifest.json(fileSchema) before relying on the output. -
The caller has the required permissions. A bucket access policy grants the action an API call requires, and those names don’t always match the call. This workflow requires:
s3:PutBucketVersioningto enable versioning before deleting.s3:GetObjectTaggingands3:PutObjectTaggingto read and write the quarantine tag.s3:ListBucketto list object versions. TheListObjectVersionscall used by the cleanup and recovery steps maps to this action.s3:DeleteObjectfor the delete step, which writes delete markers without specifying a version ID.s3:DeleteObjectVersion, additionally required by the cleanup and recovery steps, which delete specific versions by ID.s3:GetObjectands3:ListBucketon the inventory destination bucket, to download the inventory report files.
s3:PutLifecycleConfiguration. - Versioning can be enabled on the bucket. The delete step enables it if it isn’t already on. If your organization restricts versioning, confirm this is permitted before starting.
Pattern overview
The pattern has four stages:- Identify candidates from an inventory report using
LastAccessedDate. - Quarantine candidates for a buffer window during which any access updates
LastAccessedDate. - Delete with versioning enabled, so deletes are recoverable if a mistake is caught after the fact.
- Clean up by permanently removing the recoverable versions and the delete markers, once you’re confident the deletions were correct.
default profile instead of the cw profile created in the following steps. In that case, omit --profile cw from the commands.
Configure CoreWeave credentials
Configure CoreWeave credentials
-
Create a
cwprofile:Create a new profile -
When prompted, provide the following values:
- AWS Access Key ID: The Access Key ID of your CoreWeave AI Object Storage Access Key.
- AWS Secret Access Key: The Secret Key of your CoreWeave AI Object Storage Access Key.
- Default region name (Optional): To set a default region, see CoreWeave Availability Zones.
- Default output format: Use
jsonfor JSON output.
-
Set the default endpoint URL to the appropriate endpoint for your use case:
- The primary endpoint,
https://cwobject.com, for use outside a CoreWeave cluster. - The LOTA endpoint,
http://cwlota.com, for use inside a CoreWeave cluster. The LOTA endpoint routes to the LOTA path for best performance.
Set the primary endpoint for local development - The primary endpoint,
-
Set the S3
addressing_styletovirtual:Set virtual addressing style
--profile cw to your AWS CLI commands, or set AWS_PROFILE=cw in your environment.If you set endpoint_url and s3.addressing_style directly in your code (for example, in a Boto3 Config object), you can skip steps 3 and 4. The profile only needs the access key, secret key, and region.Stage 1: Identify candidates
Generate an inventory report for the bucket. The report includes aLastAccessedDate field that records the most recent time an object was read, as an RFC 3339 timestamp in UTC, for example 2026-05-19T22:08:11Z.
Filter for objects where LastAccessedDate is older than your threshold, for example 90 days. Consider only current live objects: skip non-latest versions, delete markers, and any rows with an empty LastAccessedDate. The resulting candidates list is consumed by Stage 2.
The examples in this guide assume the inventory file is a plain, uncompressed CSV. AI Object Storage inventory CSVs are headerless. The column schema is set by your inventory configuration and is listed in the report’s manifest.json (fileSchema). The FIELDS list in the snippets matches one common configuration. Adjust it to match your schema. If your inventory reports are GZIP-compressed or in another format such as Parquet, decompress or convert them first.
The following snippets open a single local file, inventory.csv. Producing that file is a step the examples assume you have already done: read the inventory manifest.json, download the one or more data files it references, and concatenate them into the local CSV. See the Inventory Reporting guide for the manifest layout and data-file locations.
Set your credentials and the target bucket before running this stage:
Stage 1: Identify cold candidates from an inventory report
Stage 2: Quarantine
Don’t delete candidates immediately. Instead, mark them and hold for a buffer window. This guide uses a tag-based quarantine: apply a tag such asquarantine_set=YYYY-MM-DD to each candidate. The approach is lightweight and modifies objects in place. You can use other approaches as well.
The quarantine_set tag is advisory. It records that an object is in quarantine and when the buffer window started, which makes the quarantine state visible when you inspect the bucket and lets you reconstruct the candidate set if the persisted state file is lost. The persisted state file, described later in this stage, not the tag, is the source of truth that drives the Stage 3 re-check and cleanup.
PutObjectTagging replaces the full tag set on an object. The following example fetches existing tags with GetObjectTagging and merges in the quarantine tag before writing the set back, preserving any other tags already on the object. The merge also replaces any prior quarantine_set tag, so it’s safe to re-run the workflow.
This snippet reuses the s3 client and candidates list from Stage 1.
Stage 2: Tag candidates for quarantine
GetObjectTagging call and write the quarantine tag directly. If you don’t need the quarantine state to be visible on the objects themselves, you can skip tagging altogether and rely solely on the persisted state file.
For large candidate lists, parallelize tagging with concurrent.futures.ThreadPoolExecutor and tune max_pool_connections in the Boto3 Config. See Performance best practices for details.
Leave quarantined objects in place for the buffer window, for example 30 days. Any access during this window updates LastAccessedDate and is caught by the re-check at Stage 3.
Persist state between stages. Stage 3 runs after the buffer window has elapsed, typically days or weeks later, so the original Python process has long since exited. Before exiting Stage 2, save the candidate key list and the quarantine date, the value used for the quarantine_set tag, to a durable location such as a local JSON file, a database, or object metadata. Stage 3 reloads both values to perform the re-check.
Stage 2: Persist quarantine state
quarantine_state.json. Wait for the buffer window to elapse before proceeding to Stage 3.
Stage 3: Delete
Stage 3 typically runs in a new process after the buffer window has elapsed. Recreate the Boto3 client and re-import the modules used in the following snippets. Set your credentials and the target bucket again in the new shell:Stage 3: Set up the client
Stage 3: Enable versioning
LastAccessedDate for each quarantined object. Make sure this report was generated after the buffer window closed. Inventory is produced on a schedule and has delivery latency, so a report generated mid-window would re-check against stale access times and could confirm an object that was in fact accessed near the end of the window. Build a confirmed_candidates list of keys whose LastAccessedDate hasn’t advanced past the quarantine start date.
Stage 3: Re-check access times after the buffer window
confirmed_candidates. They keep their quarantine_set tag, which is harmless, and are never deleted. If you prefer to clear the tag from spared objects, do so here, but it isn’t required for correctness.
Now delete the confirmed candidates. With versioning enabled, each delete writes a delete marker on top of the object rather than removing it outright. The version that was current becomes noncurrent and serves as your recovery copy.
Stage 3: Delete confirmed candidates
confirmed_candidates to a durable location before exiting, because Stage 4 needs it.
Stage 3: Persist the confirmed deletion list
Stage 4: Clean up
After the deletes in Stage 3, each deleted key has two artifacts: the noncurrent version, which is the former live object and your recovery copy, and the delete marker, which is the current version that makes the object appear gone. This guide removes both explicitly, by version ID, for only the keys this workflow deleted. Targeting specific version IDs of specific keys is the most strongly scoped option available. The cleanup can only ever touch versions of keys inconfirmed_candidates, and it never relies on a lifecycle rule’s filter matching a noncurrent version, a behavior that varies between S3 implementations and can silently do nothing. A simpler bucket-wide lifecycle alternative is described at the end of this section, but it’s only safe on a bucket dedicated to this pattern.
The recovery window is operational. Because cleanup is a script you run rather than a timer, the recovery copy survives until you run cleanup. Wait at least your recovery window, for example 30 days, after the Stage 3 delete before running it. Until then, you can undo a mistaken deletion.
Optional: Recovery before cleanup
To undo a mistaken deletion, remove the current delete marker for the key. The noncurrent recovery copy is promoted back to current. This snippet uses the same Boto3 client setup shown in Stage 3.Stage 4: Restore a single mistakenly deleted key
confirmed_candidates.json, so concurrent runs would overwrite each other’s changes (last write wins) and leave some recovered keys still in the list. Instead, recover the whole set in one pass: remove every delete marker, then filter the state file once.
Stage 4: Restore several mistakenly deleted keys
Prefix=key instead of scanning the whole bucket, the same tradeoff noted for the cleanup step.
Cleanup after the recovery window
Permanently delete the recovery copies and the delete markers for the confirmed keys. The code purges a key only if its current version is still a delete marker, that is, the object is still in the deleted state. If a key was restored with the recovery snippet shown earlier, or re-created by a workload after Stage 3, its current version is a live object, and the key is skipped so the live data is never touched. Cleanup typically runs in a new process after the recovery window has elapsed. Set your credentials and the target bucket again in the new shell:Stage 4: Permanently remove versions and delete markers
Bucket-wide lifecycle policy
If the bucket is dedicated to this pattern, meaning it holds no versioned data from any other source, you can let a lifecycle policy do the cleanup instead of running the explicit code shown earlier. Apply a policy with two rules: one to expire noncurrent versions after your recovery window, and one to remove the resulting expired delete markers.Stage 4 alternative: bucket-wide lifecycle cleanup
Accuracy and caveats
The following section describes the limitations and safety properties of this pattern.LastAccessedDate is approximate and without guarantees: it comes from access log delivery, which has a small probability of dropped events.
The buffer window, not the LastAccessedDate field itself, is what gives this pattern its safety margin. The longer the window, the lower the residual risk of deleting an object that was actually accessed. A 30-day buffer is a reasonable default. Workloads where re-access is rarer but more consequential should use longer windows.
Versioning provides a final safety net. Even if a deletion is incorrect, you can recover the object by removing its delete marker, but only until you run the cleanup step, which permanently removes the recovery copy. Because cleanup is a script you run rather than a lifecycle timer, you control the recovery window directly. Wait at least as long as your workload requires before running it.
Scoping in a shared bucket. Every destructive action in this workflow is scoped to the specific keys confirmed as cold. The Stage 3 delete operates on confirmed_candidates, and the cleanup deletes only specific version IDs belonging to those keys, and only when the key is still in a deleted state. Actively read objects, versions created by overwrites of unrelated objects, and objects restored or re-created after Stage 3 are never affected. The one exception is the optional bucket-wide lifecycle alternative, which is not key-scoped.