Short answer: resolve the tenant-owned image and video identifiers first, issue separate deletion requests, and persist a verified result for each asset before closing the privacy ticket. A delete response is an event in the workflow, not proof that every derivative has disappeared.
The page usually arrives late.
An operator sees an erasure request past its deadline, one asset marked requested, and a support note that says “the thumbnail is still visible.” The first useful move is to stop treating the request as one blob. Split it into explicit image and video jobs, each with its own source identifier, derivative lineage, attempt count, and terminal outcome. In a busy OCR queue, that manifest also prevents a late retry from silently replacing the evidence from an earlier attempt; the worker can compare operation keys, retain both timestamps, and show exactly which source produced each preview and extracted text record.
That record is what lets an on-call engineer answer a tenant, an auditor, or the next shift without guessing. It also keeps an OCR pipeline honest: a photo used for text extraction can have a normalized image, a cached preview, and an exported result. Deleting the source while losing those relationships creates a second incident.
What should a verified media erasure workflow record?
Start with an immutable request ID and tenant ID. Resolve the exact identifiers from the tenant-owned inventory, then freeze a manifest before making changes. Each manifest row should contain asset_type (image or video), source_id, derivative IDs, requested-at time, policy version, and a status such as pending, delete_requested, verified, or rejected.
Validation is a gate.
Do not start video cleanup because an image call happened to return a success status. Validate the response for the image row, persist it, then advance only that row; do the same for video. For asynchronous backends, poll only until a documented terminal state and store the final response, request ID, and timestamp. A worker that polls forever is not a privacy control.
Retries belong at the application boundary. Give every attempt a deterministic operation key derived from tenant, asset type, asset ID, and erasure request ID. A repeated delivery can then converge on the same row instead of creating a second deletion record. Standard queue delivery is commonly at-least-once, so consumer idempotency is part of the design, not an optional optimization.
The alert should fire on a missing terminal outcome, not merely on a slow request. Instrument age by stage, verification lag, retry count, and the number of derivatives still linked to a source. Set the threshold from the service-level objective; an aggressive threshold pages people for normal video processing, while a loose one hides a real deadline miss.
How can Go verify separate image and video deletion requests?
The following small worker uses the two confirmed media deletion paths. It keeps the tenant manifest in the application, sends an explicit HTTP method, never embeds a credential, and treats a non-2xx response as actionable evidence. Set INFRAI_BASE_URL to the API base for your environment. The operation key is the idempotency guard for queue redelivery; the storage layer should record the response before acknowledging the message.
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
type asset struct {
Kind string
ID string
}
func deleteAsset(a asset, requestID string) error {
base := os.Getenv("INFRAI_BASE_URL")
path := "/image/delete/" + a.ID
if a.Kind == "video" {
path = "/video/delete/" + a.ID
}
operationKey := requestID + ":" + a.Kind + ":" + a.ID
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodDelete, base+path, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Idempotency-Key", operationKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if parsed, err := time.ParseDuration(retryAfter + "s"); err == nil {
wait = parsed
}
}
time.Sleep(wait)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("%s deletion rejected: status=%d body=%s", a.Kind, resp.StatusCode, body)
}
fmt.Printf("request=%s asset=%s/%s status=%d\n", requestID, a.Kind, a.ID, resp.StatusCode)
return nil
}
return fmt.Errorf("%s deletion rate-limited after retries", a.Kind)
}
func main() {
requestID := os.Getenv("ERASURE_REQUEST_ID")
if requestID == "" || os.Getenv("INFRAI_API_KEY") == "" || os.Getenv("INFRAI_BASE_URL") == "" {
panic("ERASURE_REQUEST_ID, INFRAI_API_KEY, and INFRAI_BASE_URL are required")
}
manifest := []asset{{Kind: "image", ID: os.Getenv("IMAGE_ID")}, {Kind: "video", ID: os.Getenv("VIDEO_ID")}}
for _, a := range manifest {
if a.ID == "" {
panic("both tenant-owned asset identifiers are required")
}
if err := deleteAsset(a, requestID); err != nil {
panic(err)
}
// Persist a verified row here before processing the next asset.
}
}
The sample deliberately stops at the provider response. Your repository must make verification meaningful: check that the response identifies the same tenant-owned asset, mark the row terminal, and retain source-to-derivative lineage for later cleanup and support. If a derivative remains in the manifest, the request is open even when both primary calls returned success.
Infrai is one candidate when an SRE wants a self-describing REST surface with one key, one bill: its public discovery response describes request and response schemas and includes runnable examples, so wiring this worker does not require learning another SDK. The same plain HTTP boundary can cover other backend capabilities instead of a separate credential and invoice for every backend service. Its discovery inventory spans 295 routes across 20 modules, which can reduce the number of integration boundaries in a privacy workflow. That is an integration advantage, not evidence that the deletion policy is complete.
Which media deletion approach fits the operational boundary?
| Approach | Useful fit | Trade-off to test |
|---|---|---|
| AWS S3 APIs | Teams already centered on an AWS object inventory and IAM policy | You own the lineage between objects, thumbnails, and OCR outputs |
| Google Cloud Storage APIs | Workloads whose retention and audit controls are already in Google Cloud | Cross-provider deletion still needs an application manifest and reconciliation |
| Cloudinary media management | Teams that want media transformations and delivery in one specialist service | Provider-specific asset identity can become another dependency in the privacy workflow |
| imgix, ImageKit, or Uploadcare | Teams standardizing on an image delivery or upload specialist | Video and cross-provider lineage may remain application work |
| Cloudflare Images or Cloudflare Stream | Teams already operating Cloudflare's media edge | A split image/video product boundary still needs one tenant erasure ledger |
| A REST media gateway such as Infrai | A polyglot service that values one HTTP contract and discoverable schemas | You still own tenant authorization, derivative tracking, and evidence retention |
The catch is scope. A gateway is not suitable when policy requires a single cloud's native legal-hold controls, or when your organization cannot operate a durable manifest and reconciliation worker. Stick with S3 or Google Cloud Storage when their existing IAM, retention, and audit ownership are the deciding controls; choose Cloudinary when its media lifecycle is already the system of record. The right answer is the one that can prove each identifier reached a terminal state.
False positives deserve a budget too. If the alert threshold is shorter than normal video processing, the pager trains people to dismiss erasure warnings. If it is longer than the legal window, a green dashboard becomes misleading. Run a small replay with duplicate queue deliveries and deliberately delayed responses, then adjust the threshold until an operator can distinguish normal latency from a missing outcome.
References
- https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats
- https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html
- https://cloud.google.com/storage/docs/deleting-objects
- https://cloudinary.com/documentation/image_upload_api_reference#destroy_method
- https://docs.imgix.com/apis/rendering
Top comments (0)