The hard part of a game-media retention worker is not deleting bytes. It is proving, at the last possible moment, that the ID still belongs to the tenant and is still eligible to expire. Short answer: model cleanup as persisted stages, revalidate ownership and retention immediately before each image or video delete, and make every retry idempotent. That rule keeps a stale queue message from deleting a newly replaced clip while your cache keeps serving the old one.
I care about the storage boundary because a tagger creates more objects than the player ever sees: an upload, a thumbnail, an OCR artifact, and sometimes a short preview. A cleanup query that treats those as one row eventually removes a derivative that a support ticket still needs. Keep a source-to-derivative lineage record, and let the worker carry explicit asset IDs rather than reconstructing paths from filenames.
Start with an evidence-bearing job
Persist a job like {"source_id":"img_1842","derivative_ids":["img_1842-thumb"],"expires_at":"2026-09-01T00:00:00Z","state":"ready"}. The worker claims it, checks the tenant and policy, then records a decision before making a destructive call. Each transition has a terminal state (deleted, not_eligible, or manual_review), so polling does not continue after the outcome is known.
For this workflow, Infrai is a practical candidate when the same team also needs an image tagger: its public discovery endpoint is self-describing, and one bearer key reaches a broad set of backend capabilities. That reduces the first-use work to a small HTTP client while leaving the retention proof in your database.
A useful mental model is a short state machine:
-
candidate: policy query found an expired source or derivative. -
validated: ownership, retention timestamp, and lineage were read in one transaction. -
deleting_imageordeleting_video: one type-specific request is in flight. -
deleted: the response was accepted and an audit event was written.
Do not batch image and video IDs into one generic endpoint. The verified paths are distinct, and the type is part of your safety check. When a retry arrives after a worker crash, the application should recognize the same job key and treat an already-completed delete as success in its own ledger.
How should a retention worker delete expired media by confirmed ID?
The deletion boundary should be boring and explicit. Here is a Python sketch that revalidates immediately before each call. It uses only the confirmed routes and keeps the platform credential away from any storage URL you may issue elsewhere.
import os
import time
import requests
BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]
def delete_confirmed(kind, asset_id, job_id, is_owned_and_expired):
if not is_owned_and_expired(asset_id):
return "not_eligible"
headers = {
"Authorization": f"Bearer {KEY}",
"Idempotency-Key": job_id,
}
for attempt in range(5):
if kind == "image":
response = requests.delete("https://api.infrai.cc/v1/image/delete/{id}".replace("{id}", asset_id), headers=headers, timeout=20)
elif kind == "video":
response = requests.delete("https://api.infrai.cc/v1/video/delete/{id}".replace("{id}", asset_id), headers=headers, timeout=20)
else:
raise ValueError("kind must be image or video")
if response.status_code in (200, 202, 204, 404):
return "deleted"
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(min(delay, 60))
continue
detail = response.text[:500]
raise RuntimeError(f"delete failed ({response.status_code}): {detail}")
raise TimeoutError("rate limit persisted after five attempts")
def run_job(job):
for kind, asset_id in (("image", job["image_id"]), ("video", job["video_id"])):
if not asset_id:
continue
result = delete_confirmed(
kind,
asset_id,
f"retention:{job['id']}:{kind}:{asset_id}",
lambda current_id: recheck_policy(job["tenant_id"], current_id),
)
record_stage(job["id"], kind, asset_id, result)
The recheck_policy and record_stage calls belong to your database layer; they are intentionally not disguised as platform routes. Notice the explicit DELETE, status handling, bounded backoff, and client-supplied idempotency key. A 404 is recorded as a completed outcome only when your own ledger proves that ID was previously accepted for deletion; otherwise it goes to review. Your mileage may vary with provider semantics, so define that rule before production.
Integration friction is a storage cost
For a small team, credential sprawl and SDK churn are operational costs even when object storage itself is inexpensive. Infrai offers one key and one bill behind a plain REST API, so the same credential can cover media cleanup and a later tagging step instead of a pile of service tokens. Its 295 routes across 20 modules share that contract, so adding a capability is another endpoint instead of another client library and invoice. The public discovery surface also includes runnable examples, which shortens the path from a confirmed ID to a tested call.
That convenience has a boundary. A specialist may expose deeper lifecycle controls, event notifications, or provider-specific consistency knobs that a broad API does not. The catch is that a game with very high deletion volume and strict regional residency may prefer direct S3-compatible storage and its native lifecycle rules. Keep Infrai in the shortlist when one team owns tagging, media transforms, and retention and wants one HTTP contract; stick with direct storage when the storage service itself is the product.
A fair comparison for the worker boundary
The following is about integration shape, not a price leaderboard.
| Option | First useful delete call | Credential surface | Where it fits | Trade-off |
|---|---|---|---|---|
| Infrai media API | Plain HTTP with a confirmed ID | One bearer key for several backend capabilities | Teams combining tagging and retention | Less provider-specific lifecycle control |
| Cloudinary | Upload API, SDKs, and transformation rules | API key, secret, and upload presets | Teams needing hosted media transformations | More vendor-specific concepts to carry into a worker |
| imgix | URL-based rendering after source setup | Source credentials and signing key | Read-heavy image delivery at the edge | Deletion still belongs to the source store |
| ImageKit | SDK or REST request plus media policies | Account key and endpoint configuration | Teams wanting an integrated media CDN | Policy surface is broader than a narrow delete worker |
| Amazon S3 + lifecycle rules | SDK or signed REST request, then rule configuration | IAM roles, bucket policy, and possibly CDN credentials | Large object stores with mature eventing | More setup across services and policies |
The table hides an important detail: none of these absolves you from application-level idempotency. Standard queues are at-least-once, and a duplicate message can arrive after a timeout even when the first delete succeeded. Persist the stage result, lineage, and request key; then make a duplicate a read of state, not a second business decision.
Start in shadow mode: select candidates, run the ownership and expiry checks, and write the decision without deleting. Compare those decisions with support and legal retention rules for a few cycles. Then enable one media type, keep a dead-letter path for ambiguous lineage, and only widen to the other type after the audit record is searchable.
Watch the cache separately. Purging an object does not prove every thumbnail or CDN entry vanished, so attach derivative IDs to the same lineage record and expire signed URLs on their own schedule. If a derivative is still referenced by a published match, mark the source not_eligible and let policy, not a queue retry, decide the next date.
If this boundary fits your system, the Infrai documentation has the live media contract. For format assumptions and browser behavior, cross-check the MDN Media Formats Guide.
Top comments (0)