Performance 7 min read

Why a small image upload can use so much memory

A 311 KiB PNG decoded to 61 MiB of pixels in our test. Measure the difference and add byte, pixel, format and animation limits to a Sharp upload pipeline.

By ImageGuide Team · Published September 5, 2026
image uploadsmemorySharpsecurityimage dimensions

Our 4000 × 4000 solid-color PNG occupied 318,636 bytes on disk. Decoding it to an RGBA buffer produced 64,000,000 bytes. The Node process’s resident memory rose from about 73 MiB to 144 MiB during the operation.

The upload was small because a flat color compresses well. The decoder still had to produce sixteen million pixels. A file-size limit alone cannot describe that work.

Separate compressed bytes from decoded pixels

For an 8-bit RGBA buffer, the calculation is:

width × height × 4 bytes
4000 × 4000 × 4 = 64,000,000 bytes = about 61 MiB

This is the size of one raw buffer. It does not include the compressed input, decoder state, intermediate images, encoded output or the rest of the process. Different channel counts and bit depths change the calculation.

Resizing a large image to a thumbnail does not make the source dimensions irrelevant. A decoder may have useful downsampling optimizations, but you should not make admission limits depend on an optimization being available for every format and operation.

What we measured

We generated three ordinary solid-color PNGs and decoded each in a fresh child process. This was a bounded experiment, not a malformed-file or decompression-bomb test. Sharp 0.35.3 ran with one worker thread and its operation cache disabled on Linux with Node.js 22.22.0.

Input dimensions PNG size Raw RGBA buffer RSS before decode RSS after decode
1000 × 1000 22.5 KiB 3.8 MiB 73.1 MiB 79.8 MiB
2000 × 2000 82.9 KiB 15.3 MiB 72.7 MiB 93.4 MiB
4000 × 4000 311.2 KiB 61.0 MiB 73.0 MiB 144.2 MiB

RSS is the process’s resident memory, sampled before and after the decode while the raw output remained referenced. These are not isolated decoder allocations or sampled peak measurements. The raw report also includes the process high-water mark, environment and library versions. Host allocation behavior and previous work change real production results.

Download the 1000-pixel, 2000-pixel and 4000-pixel inputs. From the ImageGuide checkout, run:

node public/experiments/2026-09/run.mjs memory

The script checks each raw-buffer length and verifies that a lower pixel limit rejects the largest fixture. Each child has a 30-second process timeout.

Put limits before the expensive work

Use separate limits for separate costs:

Limit What it bounds
Request bytes Incoming compressed data
Decoded pixels and channels A large part of image memory demand
Accepted formats Which image-processing paths your service supports
Animation or page count Repeated work inside one file
Active jobs and queue length Aggregate resource use
Worker memory and runtime Damage from an unexpectedly expensive operation

Sharp provides input pixel and channel limits. Keep them enabled. A server should also enforce the request-body cap while bytes arrive, before collecting the complete upload into memory.

A small photo-upload boundary

This example accepts static JPEG and WebP photos, caps compressed input at 5,000,000 bytes and decoded input at sixteen million pixels. It returns an at-most-1600-pixel WebP. The numbers are an example policy, not recommended limits for every service.

import sharp from 'sharp';

export async function resizeUpload(chunks) {
  const buffers = [];
  let bytes = 0;
  for await (const chunk of chunks) {
    const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
    bytes += buffer.length;
    if (bytes > 5_000_000) throw new Error('Upload exceeds 5 MB');
    buffers.push(buffer);
  }
  if (bytes === 0) throw new Error('Empty upload');

  const input = Buffer.concat(buffers);
  const options = {
    limitInputPixels: 16_000_000,
    limitInputChannels: 4,
    failOn: 'warning'
  };
  const metadata = await sharp(input, options).metadata();
  if (!['jpeg', 'webp'].includes(metadata.format)) {
    throw new Error('Upload a JPEG or WebP photo');
  }
  if ((metadata.pages ?? 1) !== 1) {
    throw new Error('Animated images are not accepted');
  }
  return sharp(input, options)
    .autoOrient()
    .resize({ width: 1600, withoutEnlargement: true })
    .webp({ quality: 80 })
    .timeout({ seconds: 5 })
    .toBuffer();
}

The downloadable module accepts an async iterable of byte chunks, such as a Node request carrying a raw image body. For multipart forms, pass the file stream from your multipart parser and cap the complete request separately. Stop consuming the upload and return an appropriate client error when a limit is exceeded. Do not call this only after an unlimited body parser has buffered the request.

Format detection requires parsing input metadata. The accepted-format check is an application policy, not a sandbox around that parser. Keep native decoders updated and apply a process or container memory ceiling. Sharp’s security guidance recommends operating-system resource limits for unbounded memory growth and CPU use. Its processing timeout is not a deadline for receiving a slow upload or waiting in a queue.

The runnable check covers a valid split upload, empty input, excess bytes across chunks, corrupt bytes, an unsupported PNG, excess pixels and an animated WebP:

node public/experiments/2026-09/check-upload.mjs

Size the worker pool from measurements

One job completing safely says little about twenty concurrent jobs. Measure your actual transform under the worker’s memory limit, including the largest accepted file. Reserve memory for the process and queues, then choose concurrency with room for variation.

Keep animation processing in a separate policy if you support it. Frame dimensions, total frames and duration each affect cost. Avoid silently converting an animation to its first frame unless that is the documented product behavior.

Start with a byte cap and a pixel cap, then test rejection as carefully as successful conversion. An upload boundary is complete only when expensive files leave through a controlled error path.

Related Resources

Resize and deliver images with Sirv

Upload your images to Sirv, then request the sizes and formats your pages need.

Get started