CloudFront CDN in Practice (4) — Image Resizing and Video Transcoding (Media Serving)

CloudFront CDN in Practice (4) — Image Resizing and Video Transcoding (Media Serving)


Introduction

Through Part 3, we covered caching, protecting, and monitoring static and dynamic content with CloudFront. But real services have one more thing — media. User-uploaded images must be shrunk to fit the screen, and video must be converted to a quality that fits the device and network for smooth playback.

This final Part 4 covers how to handle image resizing and video transcoding within a CloudFront architecture. The key insight: these are both “transforms” in name only — their weight is the opposite.


TL;DR

  • The two transforms are opposite in weight. Image resizing is light (milliseconds). Video conversion is heavy (minutes). So the approaches are opposite too.
  • Images: transform on demand, then cache. Transform once when a request arrives, and CloudFront caches the result. Handle it with Lambda@Edge or S3 Object Lambda.
  • Variants via query string, but allowlist only. Put parameters like ?w=300 in the cache key to cache per size, but accept only allowed values (prevents an explosion of variants).
  • Video: pre-convert with a dedicated service. Don’t transcode in Lambda. On upload, MediaConvert converts to multiple-quality streaming formats and stores them in S3. Lambda only triggers the job.
  • Video is still delivered by CloudFront. The output (streaming chunks) are static files in S3, so CloudFront caches and delivers them. Being large, the CDN benefit is even bigger.

1. The Two Transforms Differ in Weight

The first thing to do when designing media transforms is to classify: is this a light task or a heavy one?

CriterionImage resizingVideo transcoding
DurationMilliseconds to hundreds of msTens of seconds to minutes
WhenSynchronous, at request timeAsynchronous, pre-converted at upload
ToolLambda@Edge / S3 Object LambdaMediaConvert (Lambda only triggers)
OutputOne transformed imageMany per-quality streaming chunks
CloudFrontCache variants by query-string keyCache streaming chunks statically

This table is the summary of all of Part 4. Images are “make on demand and cache”; video is “make everything ahead of time and deliver.”


2. Image Resizing — Transform on Demand, Then Cache

2.1 Principle: transform only on the first request, cache the variant

Keep one original (photo.jpg) and transform when a ?w=300&format=webp request comes. Don’t transform every time. Transform only on the first request (miss); CloudFront caches that variant so later requests (hit) respond without transforming.

flowchart TB
    req["GET /photo.jpg?w=300&format=webp"] --> cf{"CloudFront:<br/>variant in cache?"}
    cf -->|HIT| serve["Serve the variant immediately"]
    cf -->|MISS| lambda["Lambda: fetch original, resize"]
    lambda --> store["Store variant in cache"] --> serve

2.2 What to transform with

MethodTrait
Lambda@Edge (origin-response)On a CloudFront miss, fetch the S3 original, transform with sharp, return. The most common pattern. Watch the generated-response size limit (~1MB) and cold starts
S3 Object LambdaIntercept S3 GetObject to transform. More robust for large outputs
Serverless Image HandlerAn AWS official solution (CloudFront+Lambda+sharp). Thumbor-style URLs, deployable out of the box
3rd-partyCloudinary / imgix — you don’t run it yourself

Note: Resizing directly in the Spring Boot app is discouraged. It increases app load, and routing every variant through the app erodes the edge-caching benefit. Splitting it into “a dedicated transform function + CloudFront caching” is the standard.

2.3 Query string in the cache key — but allowlist only

To cache different copies per size/format, you must include query strings in the cache key. But don’t accept arbitrary queries. ?w=1, ?w=2, … would create infinitely many variants, blowing up the cache and letting attackers poison it (cache-busting attack). Only put allowed parameters in the key.

resource "aws_cloudfront_cache_policy" "image" {
  name        = "image-resize"
  default_ttl = 86400
  max_ttl     = 31536000
  min_ttl     = 0

  parameters_in_cache_key_and_forwarded_to_origin {
    enable_accept_encoding_gzip   = true
    enable_accept_encoding_brotli = true

    query_strings_config {
      query_string_behavior = "whitelist"
      query_strings {
        items = ["w", "h", "format", "q"]   # only the allowed variant params
      }
    }
    cookies_config  { cookie_behavior  = "none" }
    headers_config  { header_behavior  = "none" }
  }
}

Additionally, validate the w/h/q ranges and format set in the app/Lambda, rejecting or normalizing out-of-range values.

2.4 Lambda@Edge resize example (conceptual)

// origin-response Lambda@Edge — resize with sharp
const sharp = require('sharp');

exports.handler = async (event) => {
  const response = event.Records[0].cf.response;
  const request = event.Records[0].cf.request;
  const params = new URLSearchParams(request.querystring);
  const width = parseInt(params.get('w') || '0', 10);

  // out of allowed range → original as-is
  if (!width || width > 2000) return response;

  // (resize the original bytes fetched from S3 — real impl includes the fetch)
  const resized = await sharp(originalBuffer)
    .resize({ width })
    .webp({ quality: 80 })
    .toBuffer();

  response.body = resized.toString('base64');
  response.bodyEncoding = 'base64';
  response.headers['content-type'] = [{ key: 'Content-Type', value: 'image/webp' }];
  response.headers['cache-control'] = [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }];
  return response;
};

Caution: Lambda@Edge’s generated-response size limit (~1MB) can make it unsuitable for large images. In that case, transform with S3 Object Lambda or a regional Lambda (function URL) and put CloudFront in front — a more robust setup.


3. Video Transcoding — Pre-convert with MediaConvert

3.1 Why you must not transcode directly in Lambda

Video encoding takes tens of seconds to minutes and is CPU-heavy. Lambda’s time/resource limits make it unsuitable for real transcoding workloads (Lambda@Edge even more so). A dedicated managed service does the transcoding; Lambda only orchestrates by “starting the job.”

3.2 The VOD pipeline (recorded/uploaded video)

AWS Elemental MediaConvert converts file-based video into multiple qualities (360p–1080p) in streaming formats (HLS/DASH). It makes several renditions for adaptive bitrate (auto-switching quality to match the network).

flowchart TB
    up["Upload original → S3"] -->|S3 event| lambda["Lambda<br/>(only creates a MediaConvert job)"]
    lambda --> mc["MediaConvert<br/>(per-quality HLS/DASH encode)"]
    mc --> out["Output → S3<br/>(.m3u8 + segments)"]
    out --> cf["CloudFront<br/>(streaming delivery)"]
    cf --> player["Player"]

Lambda just receives the S3 upload event and creates a MediaConvert job (conceptual):

// S3 upload trigger → create a MediaConvert job (Lambda doesn't transcode)
const { MediaConvertClient, CreateJobCommand } = require('@aws-sdk/client-mediaconvert');

exports.handler = async (event) => {
  const key = event.Records[0].s3.object.key;
  const client = new MediaConvertClient({ endpoint: process.env.MC_ENDPOINT });

  await client.send(new CreateJobCommand({
    Role: process.env.MC_ROLE_ARN,
    Settings: {
      Inputs: [{ FileInput: `s3://uploads-bucket/${key}` }],
      OutputGroups: [/* HLS group: 1080p/720p/480p renditions, segment length, etc. */],
    },
  }));
};

Keep just the MediaConvert queue and IAM role in Terraform; create the actual jobs at runtime via the SDK.

resource "aws_media_convert_queue" "vod" {
  name = "vod-queue"
}
# + an IAM role (aws_iam_role) letting MediaConvert read/write S3

3.3 Live streaming

Live has a different pipeline: MediaLive (real-time encoding) → MediaPackage (packaging/origin) → CloudFront. MediaLive encodes the incoming live feed, MediaPackage packages it as HLS/DASH, and CloudFront delivers it to viewers.

Encoder → MediaLive (encode) → MediaPackage (package/origin) → CloudFront → Viewers

4. Serving Video with CloudFront

The transcoding output — playlists (.m3u8) and segments (.ts/.m4s) — are ultimately static files in S3. So Part 1’s static-caching principles apply directly. There are just a few video-specific caching points.

4.1 Segments long, live playlists short

FileVODLive
Segment (.ts/.m4s)Doesn’t change → cache longDoesn’t change → cache long
Playlist (.m3u8)Doesn’t change → cache longKeeps updating → short (seconds)

For live, caching the playlist long hides new segments and playback stalls. Set just the live .m3u8 TTL to a few seconds.

# Segments: cache long
ordered_cache_behavior {
  path_pattern    = "*.ts"
  target_origin_id = "s3-media"
  viewer_protocol_policy = "redirect-to-https"
  allowed_methods = ["GET", "HEAD"]
  cached_methods  = ["GET", "HEAD"]
  cache_policy_id = data.aws_cloudfront_cache_policy.optimized.id
  compress        = false   # video is already compressed
}

# Live playlist: cache short (a separate short-TTL policy)
ordered_cache_behavior {
  path_pattern     = "*.m3u8"
  target_origin_id = "s3-media"
  viewer_protocol_policy = "redirect-to-https"
  allowed_methods  = ["GET", "HEAD"]
  cached_methods   = ["GET", "HEAD"]
  cache_policy_id  = aws_cloudfront_cache_policy.short_playlist.id   # default_ttl a few seconds
}

Note: Video segments are already-compressed binary, so leave compress (gzip/brotli) off. The text .m3u8 can have it on.

4.2 Video is delivered by CloudFront too — even more so

Video has large traffic, and many viewers repeatedly request the same segments. So the edge-caching benefit is far bigger than for images.

  • Large, high-bandwidth: serving from S3 directly explodes cost/latency → edge caching cuts it
  • Repeated segments: many viewers fetch the same chunk of a popular video → high hit ratio
  • Global viewing: edge distribution is playback quality (less buffering)
  • Range requests: players use byte-range requests, and the S3 origin supports them by default

Protect paid/member video with the Signed URL/cookies from Part 3. Since HLS has many files (playlist + segments), a Signed Cookie that authorizes a whole path at once is more convenient than signing every file. “This user may watch /premium/movie123/* for 1 hour” is handled with a single cookie.


5. In Practice — Build-It-Yourself vs Managed/Pre-generated

The Lambda@Edge and MediaConvert approaches above are the “build it yourself on AWS” way. But what teams more commonly pick depends on size and stack, and the Part 4 approach is not the only right answer.

Images

ApproachFrequency in practiceBest for
Pre-generate on upload (fixed thumbnail/medium/large sizes)Very commonFixed-size product images, avatars
Managed (Cloudinary, imgix)CommonPay instead of building
Frontend built-in (Next.js Image, etc.)Common nowadaysWhen the frontend is Next/Vercel
On-demand Lambda@Edge (§2)ModerateMany dynamic sizes, building on AWS

If you only have a few fixed sizes, pre-generation is simpler and more common than on-demand.

Video

ApproachFrequency in practiceBest for
Managed platform (Mux, Cloudflare Stream, api.video)CommonSmall/mid teams, fast adoption
MediaConvert+S3+CloudFront yourself (§3)At scale or AWS-all-inBig control/cost/traffic needs
YouTube/Vimeo embedCommon for non-core videoWhen video isn’t the core product

Building a video pipeline is a lot of work, so small teams often start with a managed platform (Mux, Cloudflare Stream). Rolling your own MediaConvert is the choice when you need scale and control.

Bottom line: Parts 1–3 (static/dynamic caching) are the fundamentals of nearly every service, but media transformation is a “build vs managed/pre-generate” trade-off. The smaller the team, the more common it is to start managed (images = Cloudinary, video = Mux/Cloudflare Stream) and bring the pipeline in-house as scale, control, and cost grow. Drop the misconception that you “must” hand-write Lambda@Edge/MediaConvert.


Recap — Wrapping Up the Series

Across four parts, we covered CloudFront from concepts to media serving.

PartTopicCore
Part 1ConceptsEdge cache, cache key, Cache-Control/TTL, invalidation vs versioning
Part 2Hands-onSpring Boot+Kotlin origin + behavior split + Terraform + X-Cache verification
Part 3OperationsPrivate content, edge logic, security (domain/OAC/WAF), monitoring
Part 4MediaImage resizing (on-demand + cache), video transcoding (MediaConvert), HLS/DASH serving

The conclusion for media serving is one sentence: “Transform light images on the fly and cache them; pre-convert heavy video with MediaConvert; but deliver both via CloudFront.” Separating the transcoding services (MediaConvert/MediaLive) from the delivery layer (CloudFront) makes the media pipeline clear.


Appendix

A. Decision cheat sheet

SituationChoice
Image size/format variantsLambda@Edge / S3 Object Lambda + query-string caching (allowlist)
Large image transformsS3 Object Lambda / regional Lambda (avoid Lambda@Edge size limit)
Ready-made image solutionAWS Serverless Image Handler
Recorded video (VOD) transformMediaConvert (Lambda only triggers)
Live streamingMediaLive + MediaPackage
Video deliveryCloudFront (segments long, live playlist short)
Protect private videoSigned Cookie (authorize a whole path)

B. Glossary

TermDescription
TranscodingConverting video to a different codec/resolution/bitrate
HLS/DASHStreaming formats that split video into small chunks
Adaptive bitrate (ABR)Auto-switching quality based on network conditions
SegmentA few-seconds video chunk (.ts/.m4s)
Playlist (.m3u8)An index file listing the segments and their order
MediaConvertManaged file-based video transcoding service (VOD)
MediaLive / MediaPackageLive encoding / packaging services
S3 Object LambdaA feature injecting a transform when fetching an S3 object

C. References

Shop on Amazon

As an Amazon Associate, I earn from qualifying purchases.