Skip to main content
This guide describes a pattern for identifying and deleting cold objects in CoreWeave AI Object Storage buckets when the source of truth for that data lives elsewhere, for example in another cloud, a database, or an upstream system. It uses Inventory Reporting, a quarantine buffer window, and versioning to provide layers of defense against accidentally deleting recently accessed objects. The pattern runs safely against a shared bucket, one that holds a mix of objects still being read and objects that are no longer being read, and deletes only the latter. Every destructive action is scoped to the specific object versions the workflow confirms as cold, so the workflow never affects unrelated objects and their versions.
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.
Use a different approach in the following situations:
  • 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. If LastAccessedDate is absent, the identification filter silently skips every row and produces an empty candidates list. Verify the field appears in the report’s manifest.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:PutBucketVersioning to enable versioning before deleting.
    • s3:GetObjectTagging and s3:PutObjectTagging to read and write the quarantine tag.
    • s3:ListBucket to list object versions. The ListObjectVersions call used by the cleanup and recovery steps maps to this action.
    • s3:DeleteObject for 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:GetObject and s3:ListBucket on the inventory destination bucket, to download the inventory report files.
    The bucket-wide lifecycle alternative additionally needs 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:
  1. Identify candidates from an inventory report using LastAccessedDate.
  2. Quarantine candidates for a buffer window during which any access updates LastAccessedDate.
  3. Delete with versioning enabled, so deletes are recoverable if a mistake is caught after the fact.
  4. Clean up by permanently removing the recoverable versions and the delete markers, once you’re confident the deletions were correct.
The Boto3 examples use environment variables for credentials and the target bucket. Set them before you start:
Alternatively, configure your CoreWeave credentials to work with the AWS CLI. We recommend using a separate profile for CoreWeave AI Object Storage to avoid conflicts with your other AWS profiles and S3-compatible services. If you don’t set up this configuration, you might encounter errors when using AI Object Storage. If you have no other AWS profiles, you can use the default profile instead of the cw profile created in the following steps. In that case, omit --profile cw from the commands.
  1. Create a cw profile:
    Create a new profile
  2. 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 json for JSON output.
  3. 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
  4. Set the S3 addressing_style to virtual:
    Set virtual addressing style
To use this profile, pass --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.
Stages 1 and 2 run in the same process. Stages 3 and 4 typically run later, in separate processes, so each one recreates the client and reloads state from a file. The first code block in each stage sets up a complete, runnable client and reads the same environment variables, so set them again in each new shell before you run that stage. Later snippets within the same stage reuse that client.

Stage 1: Identify candidates

Generate an inventory report for the bucket. The report includes a LastAccessedDate 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 as quarantine_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
If your objects don’t carry tags, you can skip the 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.
AI Object Storage limits objects to 10 tags. If a candidate is already at the limit, the tagging call fails with a 400 Bad Request (Tags cannot be more than 10). Either remove an existing tag first or use a different quarantine mechanism.
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
Stages 1 and 2 are now complete. Each candidate object is tagged with the quarantine date and the candidate list is saved to 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
Before you delete, ensure versioning is enabled on the bucket. The call is idempotent, so it’s safe to run whether versioning is already on. With versioning enabled, each delete writes a delete marker rather than permanently removing the object, giving you a recovery copy until Stage 4 cleanup runs.
Stage 3: Enable versioning
Pull a fresh inventory report and re-check 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
The re-check intentionally excludes objects accessed during the buffer window from 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
If you later identify a deletion as a mistake, you can restore the object by removing its delete marker. See the recovery snippet in Stage 4. Persist 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 in confirmed_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
To recover several keys at once, don’t run the single-key snippet in parallel processes. Each one rewrites 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
For a small recovery set relative to bucket size, list per key with 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
Deleting by version ID removes only the listed versions, so no unrelated object is affected. Because the loop deletes every version of a still-deleted key in one pass, both the recovery copy and the delete marker, the key is fully removed with no expired delete marker left behind, and no ordering hazard arises from promoting a recovery copy back to current.

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
Both rules use an empty filter and therefore apply bucket-wide. NoncurrentVersionExpiration doesn’t act on current live objects, so active objects are never deleted, but it expires noncurrent versions created by unrelated overwrites elsewhere in the bucket. ExpiredObjectDeleteMarker can’t be combined with a tag filter, so it likewise can’t be scoped. For a shared bucket with active writes, the case this guide targets, these rules are too broad and the explicit, key-scoped cleanup described earlier is the correct choice. Use the lifecycle policy only when the entire bucket is yours to clean up.

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.
Last modified on July 20, 2026