Skip to content

Image cache invalidation

The image-serving layer caches every generated variant aggressively — once at CloudFront and once in the variants S3 bucket. To make image edits visible to viewers, those caches need to be invalidated when the underlying originals or crop metadata change. This page explains the automatic invalidation pipeline and the manual escape hatch.

The serving and management responsibilities here are split across the two parts of the service — see Image service for the architecture overview.

Every change you make through the management API ends up in one of two places:

  • Originals and binary uploads → S3 (originals bucket).
  • Pack metadata, crop info, location configs → DynamoDB (image-pack table).

The pack table has a DynamoDB Stream attached. Every meaningful write (changed imageLocations, changed mainImageLocation, replaced original) emits a stream event, which is consumed by an invalidation aggregator:

admin write
DynamoDB pack table ─── stream ───► dynamodb-imagepacks-stream λ
invalidation-queue DynamoDB table
every 5 min ▼
image-invalidation λ (scheduled)
├─ CloudFront create-invalidation
└─ S3 delete of /<tenant>/<imagePack>/*

Why the 5-minute aggregator instead of a per-write invalidation:

  • Burst-friendly. A bulk re-ingest that touches 1000 packs in a minute becomes a handful of CloudFront invalidations instead of 1000.
  • Cheaper. CloudFront wildcard invalidations are batched in groups of 15 per request (the CloudFront cap).
  • Deduplicated. Repeat writes to the same pack within a window collapse to one entry.

End-to-end latency for a normal change is roughly: write → ≤ 5 minutes → CloudFront purge → next request fetches a fresh variant from image-processing.

The stream handler is intentionally selective. It enqueues an invalidation when:

  • A pack’s imageLocations map changes.
  • A pack’s mainImageLocation changes.
  • An original is replaced (upload / fetchImage to an existing location).

Cosmetic-only updates (e.g. updating an assetId reference without touching imagery) do not invalidate the serving cache — there’s no variant to drop.

For the rare case when you need to invalidate sooner than the 5-minute window (e.g. emergency takedown), the platform unified API exposes a DELETE endpoint:

Terminal window
curl --request DELETE \
--url 'https://manage.api.<region>.vmnd.tv/<environment>-lof/images/<tenant>/<imagePackId>' \
--header 'Authorization: Bearer <admin token>'

This:

  1. Issues a CloudFront cache invalidation for the (tenant, pack) prefix.
  2. Deletes the cached variants from S3 under /<tenant>/<imagePackId>/*.

The next viewer request lands on the image-processing Lambda and regenerates the variant from the (now updated) original.

The endpoint is authenticated through the platform’s unified-API authorizer using the same Bearer JWT you use for other admin APIs — see API authentication.

A second, lower-blast-radius option is to bump the cache-busting suffix on the imagePackId wherever the asset references it. URLs in the form

…/api/v2/img/62e89a0ce4b0003c6f22b5a3-1662654467834

include a -<timestamp> suffix after the canonical pack ID. The handler treats anything after the last - as opaque — the pack still resolves — but the URL string is unique. Clients fetching the new URL will miss the existing cache and regenerate.

This is the recommended pattern when:

  • You want change visibility tied to a specific publish (the new URL ships only when the new asset/category metadata ships).
  • You want to avoid invalidating in-flight CDN caches that other consumers might still be reading.

See Image URLs and parameters → Cache-busting.

When you change a location config (force=true)

Section titled “When you change a location config (force=true)”

Replacing the size or resize behavior of an image location (PUT /adminAPI/imageLocationConfig/<name>?force=true) effectively invalidates every variant for that location across all packs — there’s no per-pack write to trigger the stream. The first viewer request for each pack at the new location will pay the resize cost.

In practice:

  • Plan location-config changes during off-peak hours when traffic to image-processing is acceptable.
  • Make sure image-processing has the concurrency headroom for the spike (managed by the platform — talk to operations if you expect a large rebuild).
Terminal window
# Force a fresh request that bypasses CloudFront's edge cache
curl -I "https://<tenant>.image-service.<region>.vmnd.tv/api/v2/img/<imagePackId>?location=main&date=$(date +%s)"

Look at X-Cache and Age headers in the response. A regenerated variant returns X-Cache: Miss from cloudfront (or similar) and Age: 0.

SymptomLikely causeFix
Edited image still showing after uploadWithin the 5-min aggregation windowWait, or trigger the manual DELETE above.
Update never propagatesEdit didn’t touch fields tracked by the stream (e.g. only metadata)Re-upload or change mainImageLocation / imageLocations to emit a stream entry; or hit DELETE.
Whole location looks stale after a config changeforce=true was omitted on the location config updateRe-issue the PUT with ?force=true.
Some viewers see new, others see oldA CDN edge node further down (ISP, app cache) is holding the responseUse the cache-busting suffix so the URL string itself changes.