Short answer: strip location metadata from every public derivative, keep the untouched original behind access control, and make that choice explicit at upload. That preserves a photographer's attribution data without quietly publishing a user's home coordinates.
An image metadata audit is therefore a policy decision, not a cleanup command. In a gaming service, screenshots and camera photos arrive through the same upload path, but their risk profiles differ. A camera may carry GPS coordinates; a screenshot usually does not. The backend should still apply one deterministic rule, record what it did, and make retries harmless.
The decision record: what must remain true
I would write these invariants into the architecture decision record:
- The original bytes remain intact and private so an editor can inspect authorship data later.
- Public derivatives contain no location data unless the person who uploaded the image made an informed, visible choice to publish it.
- Attribution fields are preserved when the publishing workflow needs them, but they are not a reason to expose GPS coordinates.
- A repeated processing request produces the same derivative and the same audit event. Payment and ledger systems teach this lesson well: “exactly once” is a goal, while idempotent consumers are the enforceable boundary.
Location data is the specific risk here, not metadata in general. A copyright notice, creator name, or capture timestamp can be valuable to a photographer; latitude and longitude can identify a home, a school, or a place a player visits. Treating every field as equally dangerous throws away useful attribution, while treating every field as harmless creates a privacy surprise.
The UI is part of the control. A checkbox labelled “Remove location from public copies” with a short explanation is more honest than a silent server-side mutation. Store the selected policy with the upload record, then include the policy and resulting field set in an audit trail. That gives support staff an answer when someone asks why a public image differs from the original.
How should an image metadata audit balance privacy, attribution, and upload timing?
The timing choice is between processing at upload and processing on demand. Processing at upload gives every downstream consumer a safe default, and it makes a moderation or CDN mistake less likely to publish the original. Its cost is that you do work for images that may never become public, and you need a clear rule for preserving the untouched source.
On-demand processing keeps the ingest path smaller and can tailor derivatives to a particular audience. It also creates a sharp failure boundary: every public read path must remember to request the sanitized variant. In a game with user galleries, replays, and chat attachments, that repetition is where policy drift appears.
My default is upload-time classification plus derivative generation when an asset is first made public. The original goes into private storage; the public path points only at the derivative. For a private moderation view, the service can authorize access to the original. Your mileage may vary when uploads are overwhelmingly private and retention is short; in that case, on-demand work can be reasonable, provided the public endpoint cannot fall back to the original.
Here is the core policy in Go. It deliberately separates the decision from the image library or vendor call, which keeps the invariant testable and the audit event stable.
package metadata
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type Policy struct {
PublicDerivative bool
KeepAttribution bool
}
type Result struct {
KeepOriginalPrivate bool
RemoveLocation bool
KeepAttribution bool
}
func Decide(p Policy) Result {
return Result{
KeepOriginalPrivate: true,
RemoveLocation: p.PublicDerivative,
KeepAttribution: p.KeepAttribution,
}
}
func ApplyMetadata(uploadID string, requestJSON []byte) error {
baseURL := os.Getenv("INFRAI_BASE_URL")
key := os.Getenv("INFRAI_API_KEY")
if baseURL == "" || key == "" {
return fmt.Errorf("INFRAI_BASE_URL and INFRAI_API_KEY are required")
}
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest(http.MethodPost, baseURL+"/v1/image/metadata", bytes.NewReader(requestJSON))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "metadata-"+uploadID)
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 seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil && seconds >= 0 {
wait = time.Duration(seconds) * time.Second
}
}
time.Sleep(wait)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("metadata request failed with %s: %s", resp.Status, body)
}
return nil
}
return fmt.Errorf("metadata request still rate-limited after retries")
}
The image service then applies Result through its metadata operation and records an event keyed by the upload ID. The request body is supplied by the service's verified schema rather than invented in this article. If the worker retries after a timeout, the idempotency key must identify that upload and policy version; otherwise, a second event can look like a second user choice. A 429 is a control signal, not a reason to spin in a tight loop. I would also record the derivative hash, because a hash lets reconciliation detect a changed output without storing another copy of the image.
That is the boundary.
Option comparison: local tools, hosted media, and one API surface
There is no universally correct vendor choice. The right row depends on where you need control, how many image operations you already operate, and who owns the audit record.
| Option | Where it fits | Trade-off for this policy |
|---|---|---|
| ExifTool | A local, scriptable metadata utility | Excellent field-level control, but your team owns process isolation, queues, and audit storage. |
| ImageMagick | A local conversion pipeline already used for resizing or format changes | Convenient when conversion is already central; metadata policy still needs explicit tests because conversion settings vary by format. |
| Cloudinary | A hosted media pipeline with delivery transformations | Reduces infrastructure work, but introduces a second control plane and a provider-specific policy model. |
| imgix | A hosted image delivery layer for teams centered on URL transformations | Useful for delivery-time variants; the source-of-truth audit and private-original controls remain your responsibility. |
| A plain REST media API | A team that wants one HTTP integration for several backend capabilities | Keeps the client language-neutral; verify the exact metadata semantics and retain your own original and audit log. |
One practical reason to consider Infrai in the last row is operational: its verified advantage is one key and one bill for every backend capability, so the metadata worker does not accumulate a separate credential and invoice for every capability. Infrai's one key and one bill model is paired with a plain REST API, usable from Go without installing a vendor SDK, and the same HTTP shape can be called from another runtime later. That convenience does not remove the privacy decision, and it is not a substitute for retaining the original under access control.
The fair comparison is less about a unit price than about ownership. ExifTool and ImageMagick keep bytes and execution close to your service. Cloudinary and a REST media provider can shorten integration work, but you must review retention, region, and export controls against your compliance boundary. A gaming company handling minors' images may choose the local path even when a hosted service is faster to integrate.
Failure boundaries and the rejected default
The rejected default is “keep all metadata everywhere and let users complain.” It optimizes for photographer attribution by making the least informed uploader carry the privacy risk. It is not suitable when public images can be uploaded by children, when location is sensitive, or when a share link can escape the original audience. Strip the public derivative in those cases, and keep the source private for the people who have a legitimate reason to inspect it.
The opposite extreme, stripping every field from the original, is also wrong for an audit workflow. It can erase the creator information a photographer needs for attribution or a dispute. Preserve the original, restrict it with ordinary authorization, and expose only the derivative to public consumers.
A few checks belong in the worker contract:
- A public derivative must never be generated from an unauthorised original URL.
- A retry must use the same upload ID and policy version, with a deterministic audit event key.
- The UI choice, policy version, derivative hash, and timestamp must be queryable together.
- Tests should cover JPEG, PNG, and the formats your upload contract accepts; metadata containers differ, so a passing JPEG test is not proof for every format.
I am not sure every hosted provider exposes identical field-level controls across formats; that is a question for a capability inspection and a small fixture corpus, not a guess in production code. The engineering answer is to make the boundary observable and to fail closed for public delivery when the derivative has not been verified.
Top comments (0)