User-Generated Platforms, chapter 14 of 48
Images at Scale
Image variants made on upload or on demand, AVIF, WebP and HEIC negotiation, GIF to video, EXIF and location stripping, and provenance that must survive.
Every image a user uploads is the wrong file to serve. It's too big for a feed, in a format half the browsers can't read (an iPhone photo arrives as HEIC), rotated by a flag rather than in its pixels, and it carries metadata the uploader probably never meant to publish: the GPS coordinates of their home, the camera's serial number, the time to the second. An animated GIF is worse, often megabytes for a few seconds of motion. So a platform never serves the upload. It serves variants: resized, re-encoded, rotated, stripped copies made for each place the image appears and each browser that asks for it.
That makes the image pipeline a set of decisions with costs on both sides. Make variants eagerly and every upload pays for sizes nobody may request; make them on demand and a cold cache turns a traffic spike into an encoding storm. Negotiate the newest format and the cache splits by browser; serve JPEG everywhere and the delivery bill and page weight stay high. Strip every byte of metadata and the location leak is closed, but so is the record that says an image was generated by AI, which platforms now read to label content and regulators increasingly expect to survive. This chapter covers variants (eager or on demand), format negotiation across AVIF, WebP, HEIC and JPEG XL, automatic quality and Save-Data, GIF to video, metadata and orientation, and provenance fields that must survive stripping.
The chapter extends media_assets and content_media from The Core Data Model and owns media_variants, the table every derived file of an asset lives in, video renditions included. What happens before an image reaches this pipeline (signed uploads, validation, decompression bombs, scanning against hash lists) is The Upload Path; where variants are stored and what it costs to deliver them is Storage, Dedup & Delivery Economics; how a platform labels AI-generated content as policy is AI on User-Generated Platforms. Generic image performance on a site's own pages (explicit dimensions, srcset, lazy loading) is the playbook's Web Performance.
How the Platforms Do It#
| Platform or vendor | Variants | Formats | Metadata | Animation |
|---|---|---|---|---|
| Facebook Haystack (2010 paper) | Four sizes made at upload, each stored in three places | Not described | Not described | Not described |
| Cloudflare Images | Named variants (up to 100 per account) and URL transformations, billed per unique transformation | format=auto picks AVIF, then WebP, from the Accept header; falls back to WebP or JPEG when an image is too large to encode to AVIF quickly |
Default keeps only the EXIF copyright tag; only JPEG output can keep metadata; rotation and color profile applied even when stripped | All frames kept by default; anim=false recommended for user uploads |
| Cloudinary | URL transformations | f_auto picks AVIF, JPEG XL or WebP per browser; images under 5,000 pixels get WebP, not AVIF |
Any transformation strips metadata except a few resolution fields and IPTC DigitalSourceType (kept in JPEG and PNG, lost in AVIF, WebP and JPEG XL) | Publishes a 6.3 MB GIF that becomes a 319 KB MP4 |
| Imgur | Not public | Not public | Not public | GIFs converted to an MP4-based format it called GIFV (October 2014) |
| Wolt (public engineering post) | On-demand encoding failed on a cold cache; moved to batch, then lazy encoding of requested combinations | AVIF and WebP by Accept header with Vary: Accept |
Not covered | Not covered |
| Apple iPhone (the uploader's side) | Not applicable | Captures HEIF when the camera is set to High Efficiency; may share as JPEG | The share sheet's Options can turn off Location per share | Not covered |
| YouTube and Meta (the reader's side) | Not applicable | Not applicable | Read C2PA and IPTC metadata on upload to label capture or AI generation | Not applicable |
Two patterns run through the table. Vendors converge on the same defaults: negotiate AVIF then WebP from the request, fall back to JPEG, strip metadata except a field or two, apply orientation into the pixels, and treat animation as a cost problem. The fields worth keeping are the ones defaults drop: Cloudflare's default keeps the copyright tag but not IPTC provenance, Cloudinary keeps the provenance field only in JPEG and PNG, and both lose everything when the output is AVIF or WebP, which is exactly what format negotiation picks. A platform that wants provenance to survive has to carry it outside the served bytes.
Variants: Eager or On Demand#
A variant is a derived file: a size, a crop, a format and a quality, made from the original. There are two ways to make them.
Eager variants are made when the upload finishes, before the asset goes ready. Facebook's Haystack paper (OSDI 2010) is the canonical description: for each uploaded photo the web servers generated four images of different sizes, so 65 billion photos became over 260 billion images and more than 20 petabytes, and because each size was stored in three locations, Haystack wrote 12 images for every photo uploaded. Users uploaded one billion photos (about 60 terabytes) a week and Facebook served over one million images a second at peak. Eager sizes give predictable latency (every size exists before anyone asks) and predictable storage (sizes times uploads), and they're what a feed needs: the thumbnail and the feed-width image of a new post will be requested within seconds of publishing.
On-demand variants are made the first time a URL asks for them and cached afterwards. Cloudflare and Cloudinary both work this way: the URL carries the transformation (width, fit, format, quality) and the vendor makes, caches and bills it. The appeal is that a redesign needs no backfill, and sizes nobody requests never exist. The risk is the cold cache. Wolt's engineering team published what happened when it added AVIF and WebP to an on-demand image proxy that sat behind a CDN with a cache hit rate above 98%: the heavier encoders overloaded the proxy when the cache was cold, autoscaling couldn't add capacity fast enough, memory spiked and the change was rolled back. The team moved to batch encoding, then to lazy encoding of only the size and format combinations users actually requested. The general shape of that failure (a cache that hides how expensive the origin is until it empties) is in Application Caching.
The long tail decides which is cheaper. Haystack existed because most requests for old photos missed the CDN: caching the whole long tail "would not be cost effective," and the photos that did miss went to storage that needed several disk operations to find each file. A platform with a long-tailed archive (every image host, every community with years of posts) serves a meaningful share of its requests from origin, and every on-demand variant requested there for the first time is an encode, not a read.
My default is a hybrid:
- Eager for the sizes every new post needs, made in the processing step and stored: a thumbnail and one or two feed widths, in the formats the platform negotiates. The asset doesn't go
readyuntil they exist, so the post never publishes with a broken image. - Lazy for everything else, and kept. Other sizes and formats are made on first request through the pipeline, then written to
media_variantsand storage, so the second request is a read even after the CDN evicts it. - An allowlist of sizes, never a free width parameter. A URL that accepts any width lets anyone mint unlimited unique transformations, each an encode and, on per-transformation pricing, a charge. Cloudflare counts unique transformations per calendar month at $0.50 per 1,000 beyond the included 5,000 (as of October 2026), and caps device pixel ratio at 2; a platform's own pipeline should snap requested widths to a short list of breakpoints (Cloudflare's
width=autodefaults are 320, 768, 960 and 1,200 pixels) and refuse the rest. - Variants are versioned. Each variant records the transformation spec and the pipeline version that made it, so a change in encoder settings can be rolled out by regenerating stale variants in the background rather than invalidating everything at once (Background Jobs).
Variants are also where pixel limits bite. Cloudflare's published limits (as of October 2026) accept images up to 100 megapixels of area and 12,000 pixels on a side, but cap AVIF output at 1,200 pixels on a side, and deliver animations over 50 megapixels (all frames counted) without any transformation at all. A platform's own pipeline needs the same guards, decided at upload (Validation and Hostile Files), so a single hostile or enormous file can't stall the encoders.
Format Negotiation#
The formats, as of October 2026, per MDN:
| Format | Browser support | Use on a user-generated platform |
|---|---|---|
| JPEG | Universal | The fallback for photographs, and the only format several pipelines can carry metadata in |
| WebP | Every current major browser (Safari from 14 on macOS Big Sur) | The safe modern default, still and animated |
| AVIF | Chrome 85, Edge 121, Firefox 93, Safari 16.1 | The smallest files for photographs; slow to encode |
| JPEG XL | Chrome 155, Edge 155, Firefox 158, Safari 17 | Newly broad; MDN still advises an AVIF, WebP or JPEG alternative |
| HEIC | Safari only (caniuse puts global support at 16.93%) | Accept as input; never serve |
| GIF | Universal | Accept as input; serve animation as video (GIF to Video) |
There are two ways to pick one per viewer. Markup negotiation lists alternatives in a <picture> element and lets the browser take the first type it supports. It works on the platform's own pages and needs no cache tricks, but it doesn't help a hotlinked image, an embed on someone else's site, or an API client that gets one URL. Server negotiation serves one URL and picks the format from the request's Accept header, which is what Cloudflare's format=auto and Cloudinary's f_auto do. Every response then varies by Accept, and the cache has to be told so (Vary: Accept, the rule in HTTP Caching), or a cache will hand an AVIF to a browser that can't decode it. Raw Accept strings differ by browser version, so a CDN that keys on the whole header fragments its cache; the practical pattern is to normalize the header into a small set of buckets (AVIF-capable, WebP-capable, neither) and key on the bucket.
Two traps sit in the details:
- What a browser decodes and what it advertises can differ. MDN's list of default
Acceptvalues shows Chrome and Edge 121 and later, and Firefox 92 and later, advertisingimage/avif, while Safari's advertisesimage/webpbut notimage/avif, even though Safari decodes AVIF from 16.1. Server negotiation byAccepttherefore serves Safari WebP. That's a fine outcome; the mistake is to sniff user agents to "fix" it and multiply the cache keys. - AVIF costs encode time. Cloudflare's docs say AVIF "is slow to encode" and that an image too large to encode quickly falls back to WebP or JPEG; Cloudinary doesn't serve AVIF for small images at all. Wolt measured an AVIF encoder setting that finished in under a second against the next slower setting taking three to four seconds. AVIF is worth it for images that are viewed many times (feed images, popular posts) and often not for the long tail, which is a reason to make AVIF eagerly for the hot sizes and let the long tail fall back to WebP.
HEIC is an input format. iPhones from the iPhone 7 on capture photos as HEIF when the camera is set to High Efficiency, and Apple notes that photos shared by AirDrop, Messages or email "might automatically be shared in a more compatible format, such as JPEG," while iCloud Photos keeps the original. So uploads arrive as both, depending on how the photo left the phone. Only Safari renders HEIC, and caniuse's note gives the reason: it "is complex and expensive to license." The pipeline decodes HEIC at upload and serves every variant in web formats; Cloudflare's transformation service, for example, accepts HEIC as an input but its output formats are AVIF, WebP and JPEG. Keep the HEIC original as the source of truth; never link to it.
Billing follows the negotiation. Cloudflare excludes format and quality when it counts unique transformations, so negotiating three formats for one size costs one transformation, not three; a platform's own pipeline pays per encode and per stored file, so each negotiated format multiplies variant storage. That's the case for keeping the negotiated set small (AVIF, WebP, JPEG) and adding JPEG XL only when its share of requests justifies another copy of every hot image.
Quality and Save-Data#
Fixed quality settings are a guess. A setting that's invisible on a busy photograph shows banding on a sky or a screenshot, and one quality value for every image either wastes bytes on the easy ones or damages the hard ones. Vendors replace the number with a perceptual target:
- Cloudinary's
q_autohasbest,good(the default),ecoandlowmodes that pick a quality per image to hold a perceived level. - Cloudflare defaults to quality 85 on a 1 to 100 scale and also accepts perceptual levels (
high,medium-high,medium-low,low), applied to JPEG, WebP and AVIF output. - Wolt's tuning matched visual quality at AVIF 58, WebP 82 and JPEG 85, a reminder that the same number means different things per encoder.
A user-generated platform should tune by content class, not by page: screenshots, text and line art need a lossless or near-lossless path (PNG or lossless WebP), photographs take aggressive perceptual quality, and artwork uploaded by creators who will complain about compression may need a higher floor. The class can be detected (few colors and sharp edges suggest a screenshot) and recorded with the variant.
Save-Data is a request header (Save-Data: on) a browser sends when the user has asked to reduce data use. Cloudinary switches q_auto from good to eco when it sees the header; Cloudflare's slow-connection-quality option lowers quality on Chromium browsers when client hints report a round-trip time above 150 milliseconds or a downlink below 5 Mbps. MDN marks Save-Data as experimental and not Baseline, so it's an optimization for the browsers that send it, never something a design depends on. Any response shaped by it must list it in Vary (MDN's example is Vary: Accept-Encoding, Save-Data), or a cache will keep serving the low-quality image after the user moves to Wi-Fi.
GIF to Video#
An animated GIF is a sequence of palette images with no inter-frame compression worth the name, and video codecs beat it by an order of magnitude. web.dev's example turns a 3.7 MB GIF into a 551 KB MP4 and a 341 KB WebM; Cloudinary's turns a 6.3 MB GIF into a 319 KB MP4, which it calls 95% smaller. Imgur made the same move at platform scale on October 9, 2014, converting uploaded GIFs into an MP4-based format it called GIFV and raising its GIF upload limit from 5 MB to 50 MB in the same announcement, because a video file of the bigger animation was still cheaper to serve than the old capped GIF.
The pipeline for an animation asset (core kind = 'animation'):
- Detect animation on upload, not by extension: a
.gifmay be one frame, a WebP or PNG may be animated (APNG), and an animated file has a frame count and total pixel area that the validation step must bound (Validation and Hostile Files). - Make an MP4 (H.264) and optionally a WebM, silent, with even dimensions (libx264 refuses odd ones, so crop or pad by a pixel), plus a still poster frame for previews, link cards and reduced-motion settings. web.dev's recipe uses a constant-quality encode rather than a target bitrate.
- Play it like a GIF. The player uses
autoplay,loop,mutedandplaysinline, so it starts on its own, repeats, and doesn't take over the screen on a phone. - Decide what "download" means. Users who download a GIF expect a GIF. Keep the original for download, or regenerate an optimized GIF on request, and say which in the product.
- Short clips are not the video pipeline. A looping animation doesn't need an encoding ladder, adaptive streaming or captions; one or two progressive files are enough. Uploads that exceed a duration or size threshold, or that have audio, are videos and go to Video on Demand.
Animated AVIF and animated WebP are alternatives (Cloudinary's f_auto can deliver either), and on vendors that bill per image rather than per video minute they can be cheaper to run. Where animation is a cost and not the point, Cloudflare recommends anim=false for user uploads, serving the first frame only, which is the right default for avatars and thumbnails even on platforms that animate posts.
Metadata, Location and Orientation#
Camera files carry EXIF and XMP metadata the uploader rarely sees: GPS coordinates, the time of capture, the camera make, model and body serial number (EXIF tag 0xA431), the lens, the editing software, and maker notes in each manufacturer's private format. A photo of a child's bedroom posted from the bedroom publishes the house. Precise geolocation is personal information under COPPA's definition, and the UK Children's Code requires geolocation to be off by default (Children's Privacy); under the GDPR, keeping what the purpose doesn't need fails privacy by default.
The defaults across the industry are consistent: strip on the way out. Cloudflare's default keeps only the EXIF copyright tag, its keep option "preserves most EXIF metadata, including GPS location," and any output other than JPEG discards all metadata regardless; Cloudinary strips metadata from every transformed image except a short list of resolution and color fields and the IPTC DigitalSourceType. Users can strip at the source too: Apple's share sheet has an Options control to turn off Location for a share (the sheet then shows "Location Not Included"), Photos can set a photo to "No Location," and the Camera collects location only if it has Location Services access. A platform can't rely on any of those having happened.
My rules:
- Strip every served variant by default, keeping only an allowlist: a copyright notice if the platform supports attribution, color profile information converted into the pixels, and the provenance fields in the next section. Photography communities that display exposure data (aperture, shutter speed, ISO, lens) copy those values into the database at upload and render them as text; they still never serve the GPS block or the serial number.
- Keep the original, access-controlled. The original upload, with its metadata, is the source for regenerating variants and is evidence in a case (Scanning Before Publish). It's never publicly addressable, and the platform records that it carried location, so a breach review or a data-subject request can find which stored files hold coordinates.
- Hiding is not stripping. A setting that hides a photo's location on the page while the downloadable original still carries the GPS block protects nobody who clicks download. If the product offers "download original," the original download is stripped too unless its owner chose otherwise for their own files.
- Location is a field the user sets, not one the file leaks. If location adds value (a travel post, a listing photo), the product asks for it and stores it on the content item, at a precision the user picked.
Orientation is metadata that changes pixels. Phones often store the sensor's native frame and record the rotation in the EXIF Orientation tag (values 1 to 8: normal, mirrored, rotated 180 degrees, rotated 90 degrees clockwise, and so on). Browsers honor it by default (CSS image-orientation: from-image), so an unstripped original looks right in the browser and a stripped variant that didn't apply the rotation first comes out sideways. The order is fixed: read the orientation, rotate or mirror the pixels, then strip the tag. Cloudflare applies EXIF rotation and the color profile "even when metadata is discarded" for this reason. Color profiles are the same problem in color: convert wide-gamut and CMYK images to the profile the variant is served in before discarding the profile, or colors shift.
Alt text is metadata the platform should add. A description of the image for screen readers belongs to the post's use of the file, not the file (the same image can illustrate two posts that need different descriptions), so it lives on content_media beside the position. The rule for what good alt text is lives in Accessibility.
Provenance That Must Survive#
Stripping removes the one class of metadata a platform now needs to read: where the image came from. Two standards carry it.
IPTC Digital Source Type is a controlled vocabulary written into the image's IPTC or XMP metadata. Its terms include digitalCapture (a camera), computationalCapture (several frames merged automatically, such as smartphone HDR), humanEdits, compositeWithTrainedAlgorithmicMedia (edited with generative AI, such as inpainting), trainedAlgorithmicMedia (generated by a model trained on captured content), algorithmicMedia, screenCapture, digitalCreation and compositeSynthetic. Terms are retired over time (digitalArt was retired in 2024 in favor of digitalCreation), so a reader maps old values to current ones.
C2PA Content Credentials are a signed manifest. In the C2PA 2.2 explainer's words, a manifest is "a cryptographically bound structure that records an asset's provenance": assertions about origin, edits and AI use, signed with the claim generator's certificate, and bound to the content by a cryptographic hash, so that "any alteration to either the asset or the provenance, however insignificant" breaks the binding. Edits made in C2PA-aware tools record the source files as ingredients, building a tree of provenance; the specification records actions (one of c2pa.created or c2pa.opened is mandatory in version 2.2) and can carry a digitalSourceType on them. A manifest store can be embedded in the file or kept external to it, and soft bindings (invisible watermarks or fingerprints) let a credential be found again after it's been stripped from the file. Validation tells the reader the credential is intact and who signed it; the explainer is explicit that provenance "alone cannot tell you whether the digital content is true."
Platforms read both. Meta said in February 2024 (updated April 2025) that it labels images using the C2PA and IPTC markers that Google, OpenAI, Microsoft, Adobe, Midjourney and Shutterstock add, while acknowledging that "there are ways that people can strip out invisible markers." YouTube shows a "Captured with a camera" disclosure on videos recorded with tools that have built-in C2PA support, and withholds it when edits or a re-upload through a non-supporting tool break the chain of provenance.
The conflict with the rest of this chapter is direct. Every variant is a re-encode, so its hash no longer matches the original's hard binding; most pipelines strip the IPTC field from AVIF and WebP output; and format negotiation picks AVIF and WebP. A platform that relies on metadata surviving in served files will lose it. The design that works:
- Read provenance at upload, from the original, before any transformation: whether a C2PA manifest is present, whether it validates, who signed it, which actions and digital source type it declares, and any IPTC DigitalSourceType. Store the result.
- Keep the manifest with the original. The original file (or its extracted manifest store) stays in storage, so the credential can be re-validated when trust lists change or shown to a reviewer, a researcher or a court.
- Carry the facts, not the bytes. The label a viewer sees, the input to the moderation pipeline and the export to a regulator come from the stored record, not from whatever survives in the served variant. Where the product wants credentials to travel with downloads, it re-signs: C2PA describes renditions made by non-editorial transformations such as re-encoding or scaling, and a platform that signs as a claim generator can record the original as an ingredient of its variant. That needs a signing certificate and key management, so it's a deliberate investment, not a default.
- Keep the fields that are cheap to keep. Where the output format can carry them (JPEG and PNG), copy the IPTC DigitalSourceType into served variants, as Cloudinary does by default.
- Absence proves nothing. Most cameras and phones don't write credentials, and stripping is trivial. A missing credential is "unknown," never "authentic"; a valid one says who signed, not that the content is true. How a platform turns these signals into labels, and what the law requires of generative content, is AI on User-Generated Platforms.
How to Build It#
The chapter's additions to the core model:
ALTER TABLE media_assets
ADD COLUMN metadata_policy TEXT NOT NULL DEFAULT 'strip'
CHECK (metadata_policy IN ('strip', 'keep_copyright',
'keep_camera')), -- what served variants may carry
ADD COLUMN source_orientation SMALLINT CHECK (source_orientation BETWEEN 1 AND 8),
-- EXIF value, applied to the pixels
ADD COLUMN had_location BOOLEAN NOT NULL DEFAULT FALSE, -- the original carries GPS
ADD COLUMN is_animated BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN frame_count INT CHECK (frame_count > 0),
ADD COLUMN content_class TEXT CHECK (content_class IN ('photo', 'graphic',
'screenshot', 'artwork')); -- tunes quality
ALTER TABLE content_media -- alt text belongs to this use of the file
ADD COLUMN alt_text TEXT,
ADD COLUMN alt_text_source TEXT CHECK (alt_text_source IN ('author', 'generated')),
ADD CONSTRAINT content_media_alt_text_source
CHECK ((alt_text IS NULL) = (alt_text_source IS NULL));
CREATE TABLE media_variants ( -- every derived file of an asset, video included
id UUID PRIMARY KEY DEFAULT uuidv7(),
media_asset_id UUID NOT NULL REFERENCES media_assets (id),
purpose TEXT NOT NULL CHECK (purpose IN ('thumbnail', 'feed', 'full', 'avatar',
'poster', 'storyboard', 'animation', 'rendition', 'download')),
spec TEXT NOT NULL, -- canonical transform: 'w=640;fit=scale-down;q=good'
pipeline_version INT NOT NULL, -- encoder settings that made it; stale ones regenerate
format TEXT NOT NULL, -- 'avif', 'webp', 'jpeg', 'png', 'mp4', 'webm', ...
width INT NOT NULL CHECK (width > 0),
height INT NOT NULL CHECK (height > 0),
byte_size BIGINT NOT NULL CHECK (byte_size > 0),
storage_key TEXT, -- null once purged
origin TEXT NOT NULL CHECK (origin IN ('eager', 'lazy')),
metadata_kept TEXT[] NOT NULL DEFAULT '{}', -- e.g. {'copyright', 'iptc_digital_source_type'}
state TEXT NOT NULL CHECK (state IN ('ready', 'failed', 'stale', 'purged')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (media_asset_id, spec, format)
);
CREATE TABLE media_provenance ( -- what the original said about where it came from
id UUID PRIMARY KEY DEFAULT uuidv7(),
media_asset_id UUID NOT NULL REFERENCES media_assets (id),
c2pa_state TEXT NOT NULL CHECK (c2pa_state IN ('absent', 'invalid',
'well_formed', 'valid', 'trusted')),
c2pa_manifest_key TEXT, -- the extracted manifest store, kept with the original
c2pa_signer TEXT, -- certificate subject of the claim signer
c2pa_claim_generator TEXT, -- the tool that wrote the claim
c2pa_actions TEXT[], -- 'c2pa.created', 'c2pa.opened', 'c2pa.edited', ...
digital_source_type TEXT, -- IPTC term, e.g. 'trainedAlgorithmicMedia'
digital_source_from TEXT CHECK (digital_source_from IN ('c2pa', 'iptc_xmp')),
validator TEXT NOT NULL, -- library and version, and the trust list version
checked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CHECK ((digital_source_type IS NULL) = (digital_source_from IS NULL)),
CHECK (c2pa_state = 'absent' OR c2pa_manifest_key IS NOT NULL)
);
CREATE INDEX media_provenance_latest ON media_provenance (media_asset_id, checked_at DESC);
Notes on the sketch:
- One variants table for every derived file. Images, animation videos, posters and video renditions are all rows in
media_variants; Video on Demand adds codec, bitrate and packaging columns rather than a parallel table, and Storage, Dedup & Delivery Economics adds the storage tier. A takedown or purge sweeps an asset's variants with one query, and the CDN purge list is the same query (Takedowns and Cache Purges). - The spec is canonical. Parameters are normalized and ordered before they become the
spec, and widths are snapped to the allowlist, so two URLs that mean the same transform hit the same row and the same cache key. The unique key includes the format, so negotiation creates one row per format actually served. - The asset goes
readywhen its eager variants exist. The processing job writes the eager rows, then movesmedia_assets.statefromprocessingtoreadyin the same transaction as the outbox event; lazy variants are added later by the delivery path, idempotently on the unique key (Background Jobs). - Pipeline versions make re-encoding a job. Raising quality, adding AVIF or fixing an orientation bug bumps the version; a background sweep marks old variants
staleand regenerates them hottest first, while the stale file keeps serving until its replacement exists. - Metadata policy is per asset, not per request.
metadata_policysays what served variants may carry;metadata_keptrecords what a given variant actually carries, because the output format can drop more than the policy allows (AVIF and WebP output keeps nothing on most pipelines).had_locationis the only trace of the coordinates outside the original. - Provenance is a history. Each check is a row; the latest one is current. Re-validation when a trust list or validator changes adds a row rather than overwriting the old verdict, so a label shown last year can still be explained. The label itself, and any user declaration that content is AI-generated, belong to the content item and to AI on User-Generated Platforms.
- Events:
media.variant_ready(for each eager variant and each first lazy one),media.variants_stale(a pipeline version change) andmedia.provenance_checkedgo through the outbox (Events & Async Messaging) beside the core'smedia.ready; feeds wait formedia.ready, the CDN purge worker listens for purges, and AI labeling and moderation consume the provenance result.
Auditing#
- What was served, and how it was made. Every variant's spec, format, pipeline version and the metadata fields it kept, so a complaint that a published photo revealed a location can be checked against what the platform actually served, not what the original contained.
- Which originals hold location. The
had_locationflag and the original's access log answer a breach review or a data-subject request without reading every file. - Every provenance check. The verdict, signer, claim generator, declared digital source type, validator version and time, kept as history, because the label a viewer saw and the evidence a regulator asks for must match a stored record.
- Alt text and its source, with the revision that published it, so an accessibility complaint can see what was there.
- Pipeline changes. When a pipeline version changed encoder, quality or stripping behavior, and which variants were regenerated, so a sudden change in file sizes, colors or orientation in a support ticket can be traced to a release.
What This Means for the System You Build#
- Never serve the upload. Every image a viewer gets is a variant: resized, re-encoded, rotated into its pixels and stripped.
- Make the sizes every new post needs eagerly, and the rest lazily, kept in storage so the long tail isn't re-encoded every time the CDN evicts it.
- Snap sizes to an allowlist; a free width parameter is an unbounded encode and billing surface.
- Negotiate AVIF, WebP and JPEG from
Accept, withVary: Acceptnormalized into a few buckets, and reserve AVIF for images viewed often enough to pay for the encode. - Accept HEIC, never serve it; decode at upload and keep the original as the source.
- Tune quality perceptually and by content class, honor Save-Data where browsers send it, and vary the cache on it.
- Turn GIFs into silent looping MP4s with a poster frame, and keep long or audible uploads in the video pipeline.
- Apply orientation and color before stripping, then strip every served variant to an allowlist; hiding location on a page is not removing it from a download.
- Read C2PA and IPTC provenance from the original at upload, store the verdict and the manifest, and drive labels from the record, because served variants won't carry it.
- Version the pipeline so encoder changes roll out as background regeneration, not a mass invalidation.
References#
Standards
- C2PA: Explainer, version 2.2, manifests as Content Credentials, hard and soft bindings, ingredients, validation and its limits
- C2PA: Specification, version 2.2, embedded or external manifest stores, renditions from non-editorial transformations, mandatory
c2pa.createdorc2pa.opened, digitalSourceType on actions, manifest validation states - IPTC: Digital Source Type vocabulary, the terms and their definitions, retired terms
- ExifTool: EXIF tags, Orientation values, Copyright, BodySerialNumber and maker notes
Browser and web platform
- MDN: Image file type and format guide, browser support and fallback advice for AVIF, WebP, JPEG XL, JPEG and GIF, the
<picture>element - MDN: List of default Accept values, which browsers advertise
image/avifandimage/webp - MDN: Save-Data, the header, its experimental status and
Vary: Save-Data - MDN: image-orientation, browsers apply EXIF orientation by default
- Can I use: HEIF/HEIC image format, Safari-only support, global share and the licensing note
- web.dev: Replace animated GIFs with video, size comparison, encoding commands and the autoplay, loop, muted and playsinline attributes
Vendor documentation
- Cloudflare Images: Features,
format=auto, AVIF fallback, metadata options, rotation and color applied when stripping, quality levels, slow-connection quality, animation,width=autobreakpoints and thedprcap - Cloudflare Images: Transform via URL, output formats and Accept-header handling
- Cloudflare Images: Limits, file size, area and dimension limits, the AVIF dimension cap, animation limits, HEIC among input formats
- Cloudflare Images: Create variants, named variants and the 100-variant limit
- Cloudflare Images: Pricing, unique transformations per calendar month excluding format and quality, prices and the free allowance
- Cloudinary: Image optimization,
f_autoand the small-image AVIF rule,q_automodes and Save-Data, format size comparison, the GIF to MP4 example, metadata stripping and DigitalSourceType retention by format
Platform engineering
- Beaver et al.: Finding a needle in Haystack: Facebook's photo storage (OSDI 2010), four sizes per upload, 260 billion images and 20 petabytes, weekly uploads, peak serving rate, 12 writes per upload, the long tail and CDN misses
- Wolt: Enhancing user experience with advanced image formats, the cold-cache failure of on-demand AVIF and WebP, lazy encoding,
Vary: Accept, matched quality settings and encode times
Platform documentation
- Apple: Using HEIF or HEVC media on Apple devices, HEIF capture from iPhone 7, Most Compatible, conversion to JPEG when shared
- Apple: Manage location metadata in Photos, turning off Location when sharing, removing location, Camera's Location Services access
- Meta: Labeling AI-generated images on Facebook, Instagram and Threads, reading C2PA and IPTC markers, the companies covered, stripping as a limit (February 2024, updated April 2025)
Press
- TechSpot: Imgur introduces GIFV, the October 9, 2014 conversion of GIFs to an MP4-based format and the 50 MB GIF limit
- Tubefilter: YouTube's "captured with a camera" disclosure, C2PA-capable capture tools and broken provenance chains (October 16, 2024)