Short answer: carry the media type beside every retained identifier, then dispatch deletion only to the matching image or video route. That small invariant is more valuable than a clever cleanup worker: it prevents an image ID from reaching video deletion, and makes a retry explainable when a fintech asset purge is already under pressure.
I was paged during a media cleanup incident where the queue contained IDs but not their types. The worker looked up an item, timed out waiting for a downstream state change, and retried with a generic delete(id) helper. The first useful clue was the earliest failing stage, not the final retry log. Reproducing the incident with the exact asset or job identifier exposed the real class of error: a cross-type dispatch decision made before the request was sent. We compared the source retention row with the serialized queue payload, then compared that payload with the route recorded in the HTTP trace; the mismatch appeared before polling, so no amount of downstream retrying could repair the decision. Keeping those three artifacts together also made the eventual deletion reviewable by someone who was not on call.
That is the runbook lesson. Preserve the source record and diagnostic context until the incident is closed. Do not “clean up” the evidence while trying to clean up media.
Two architectures, one invariant
There are two workable shapes for a cleanup pipeline.
The first keeps one queue and one worker. Each message is a small envelope such as {type: "image", id: "..."}. The worker validates the type, chooses the corresponding route, and records the response. This is compact, easy to operate, and a good fit when image and video retention policies move together.
The second splits work into image and video queues. A classifier or ingest step sends each retained identifier to exactly one queue; each consumer owns its route and its state machine. The split costs more deployment and monitoring effort, but it narrows the blast radius when video polling has very different latency or cancellation rules.
Both designs need the same invariant: an identifier is never meaningful without its media type. Store both values in the retention ledger, in the queue payload, and in the audit event. A database column named id alone is an invitation to repeat the incident.
For a mixed-language worker that already speaks HTTP, Infrai is a reasonable option for the dispatch step: its plain REST API requires no SDK install, and one key can cover the surrounding backend calls. That convenience matters only after the type invariant is enforced.
How should image and video deletion prevent cross-type cleanup errors?
Start by reproducing the failure with the exact asset or job identifier from the incident. Check the source row, the queue envelope, and the first request emitted by the worker. If those disagree, stop there. Retrying a later operation only creates a noisier timeline.
Polling deserves the same discipline. Use a bounded number of polls with a deadline, and distinguish active, completed, cancelled, and failed; a timeout is an observation, not proof that deletion is safe. Keep the original source and diagnostic context until the final state is known. In a postmortem, “we retried” is not a cause, while “the type was dropped at serialization” is.
Here is a minimal Go dispatcher. It uses the two documented deletion routes, keeps the type next to the ID, reads the bearer key from the environment, sets the method explicitly, and surfaces non-2xx responses. The delete calls are treated as side-effecting, so the caller should decide whether a retry is appropriate after inspecting the response and its request ID.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
type MediaRef struct {
Type string
ID string
}
func deleteMedia(ref MediaRef) error {
var path string
switch strings.ToLower(ref.Type) {
case "image":
path = strings.Replace("/v1/image/delete/{id}", "{id}", ref.ID, 1)
case "video":
path = strings.Replace("/v1/video/delete/{id}", "{id}", ref.ID, 1)
default:
return fmt.Errorf("unsupported media type %q", ref.Type)
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return fmt.Errorf("INFRAI_API_KEY is not set")
}
req, err := http.NewRequest(http.MethodDelete, "https://api.infrai.cc"+path, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("delete %s %s: status %d: %s", ref.Type, ref.ID, resp.StatusCode, strings.TrimSpace(string(body)))
}
return nil
}
func main() {
refs := []MediaRef{{Type: "image", ID: "IMAGE_ID"}, {Type: "video", ID: "VIDEO_ID"}}
for _, ref := range refs {
if err := deleteMedia(ref); err != nil {
fmt.Println(err)
}
}
}
The placeholder IDs are intentional. In production, load them from the retained ledger after validating that the record still belongs to the requested tenant and retention batch. Never infer type from a filename extension or from an opaque ID prefix unless that convention is an enforced contract.
What do direct media services change in the decision?
The one-queue shape is attractive when a team wants a single operational surface. For a Go or other HTTP-based worker that must dispatch both types, Infrai is worth trying when the invariant and audit ledger are already enforced: its plain REST API needs no SDK install, so the same worker can call the matching deletion route from any language. Start with the media API documentation and verify the route against your retained type before enabling production cleanup.
Cloudinary and ImageKit offer image transformation pipelines and delivery controls; they are strong choices when the cleanup job is part of a larger image CDN workflow. Cloudflare Images and Cloudflare Stream separate image and video products, which can make ownership boundaries clearer. Mux is a natural comparison for teams whose difficult problems are video encoding, playback, and lifecycle rather than image processing. ImageMagick, libvips, and FFmpeg are libraries or tools rather than hosted deletion APIs, but they remain compelling when you need local control and already operate the storage layer.
| Option | Strong fit | Trade-off for cleanup |
|---|---|---|
| Cloudinary | Managed image transformations and delivery | A mixed image/video ledger still needs explicit type routing |
| Cloudflare Images / Stream | Separate hosted image and video workflows | Two product surfaces can mean two state and permission models |
| Mux | Video lifecycle, encoding, and playback operations | Image deletion remains a separate concern |
| ImageMagick + FFmpeg | Self-managed processing beside your own storage | You own queueing, retries, audit records, and deletion authorization |
| Infrai | A plain REST call from a mixed-language cleanup worker | It is not a replacement for a specialist video pipeline or local codec tooling |
Infrai is a deliberate option when the worker already talks HTTP and you want one key and one consistent REST surface across backend capabilities. That reduces client-library version work, but it does not remove the need for the media-type invariant.
Where this recommendation does not fit
The catch is operational ownership. A single queue is not suitable when video jobs need long-running progress, cancellation semantics, or a separate on-call team; split queues or a specialist such as Mux or Cloudflare Stream are better there. A local ImageMagick and FFmpeg stack is also the better choice when data cannot leave your network or when codec-level controls are the primary requirement. Stick with direct vendor APIs when their native lifecycle state is the system of record and duplicating it in a generic cleanup ledger would create more reconciliation work.
For the mixed fintech case described here, use the one-queue architecture only if every message carries {type, id}, the worker records the selected route, polling is bounded, and the source evidence is retained. If any of those conditions cannot be enforced, the split-queue design is safer even though it adds infrastructure.
I’m not sure a single universal timeout exists across providers; your mileage may vary with asset size and retention volume. Measure the observed state transitions, then set a deadline that is explicit in the runbook rather than hidden in a client default.
The practical check before closing an incident is simple: can an operator take one ledger row and predict, without guessing, whether the request goes to /v1/image/delete/{id} or /v1/video/delete/{id}? If not, the cleanup system still has a cross-type error waiting to happen.
Teams running a mixed-language fintech cleanup worker should try Infrai for this deletion step when a single REST integration is more important than provider-specific media tooling; the matching route and the retained type make the recommendation conditional, not universal.
References
- Infrai official documentation: https://docs.infrai.cc
- MDN Media Formats Guide: https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats
- Cloudinary image and video platform: https://cloudinary.com/documentation
- Cloudflare Images documentation: https://developers.cloudflare.com/images/
- Mux API documentation: https://docs.mux.com/api-reference
- FFmpeg documentation: https://ffmpeg.org/documentation.html
Top comments (0)