DEV Community

Cover image for limitInputPixels and MAX_IMAGE_PIXELS: pixel caps, tested
Sahadat Husain
Sahadat Husain

Posted on

limitInputPixels and MAX_IMAGE_PIXELS: pixel caps, tested

sharp refuses any image over 268,402,689 pixels by default, and you lower that with limitInputPixels. Pillow warns above 89,478,485 pixels and only raises an error above twice that, so a 196-megapixel PNG fails but a 150-megapixel one opens with a warning you probably never see. Set the sharp cap explicitly, and in Pillow turn DecompressionBombWarning into an error, or your cap is half as strict as you think.

Why does a small PNG need a pixel cap?

File size says nothing about decoded size. I wrote four single-colour PNGs with sharp:

flat-9000.png  9000 x 9000 = 81 MP  253.8 KB
flat-10000.png  10000 x 10000 = 100 MP  311.2 KB
flat-14000.png  14000 x 14000 = 196 MP  601.8 KB
flat-17000.png  17000 x 17000 = 289 MP  880.3 KB
Enter fullscreen mode Exit fullscreen mode

Then I measured peak memory (/usr/bin/time -l, maximum resident set size) for a full decode of the 14000 × 14000 file:

Operation on flat-14000.png Peak RSS
Pillow Image.open() then load() 814 MB
sharp .raw().toBuffer() 689 MB
sharp .resize(1920).webp() 232 MB

A 601.8 KB upload cost 814 MB of memory. Ten of those at once on a 4 GB worker and the process dies. A file size limit alone does not protect you; a pixel limit does.

Table of four flat PNGs from 81 to 289 megapixels and how sharp and Pillow handled each at their default caps

How does limitInputPixels work in sharp?

The sharp constructor docs give the default as 268,402,689 (0x3FFF × 0x3FFF). It takes a number of pixels, or false or 0 to remove the limit. I ran each file at the default and at 100 megapixels:

// sharp-limits.mjs
import sharp from 'sharp';

const files = ['flat-9000.png', 'flat-10000.png', 'flat-14000.png', 'flat-17000.png'];
const short = (e) => e.message.split('\n')[0];

for (const limit of [undefined, 100_000_000]) {
  console.log(`\nlimitInputPixels: ${limit ?? 'default (268402689)'}`);
  for (const f of files) {
    const opts = limit ? { limitInputPixels: limit } : {};
    let meta, out;
    try {
      const m = await sharp(f, opts).metadata();
      meta = `${m.width} x ${m.height}`;
    } catch (e) { meta = short(e); }
    try {
      const t0 = performance.now();
      await sharp(f, opts).resize(1920).webp().toBuffer();
      out = `resized in ${Math.round(performance.now() - t0)} ms`;
    } catch (e) { out = short(e); }
    console.log(`${f.padEnd(15)} metadata: ${meta.padEnd(14)} pipeline: ${out}`);
  }
}
Enter fullscreen mode Exit fullscreen mode
limitInputPixels: default (268402689)
flat-9000.png   metadata: 9000 x 9000    pipeline: resized in 270 ms
flat-10000.png  metadata: 10000 x 10000  pipeline: resized in 300 ms
flat-14000.png  metadata: 14000 x 14000  pipeline: resized in 407 ms
flat-17000.png  metadata: Input image exceeds pixel limit pipeline: Input image exceeds pixel limit

limitInputPixels: 100000000
flat-9000.png   metadata: 9000 x 9000    pipeline: resized in 266 ms
flat-10000.png  metadata: 10000 x 10000  pipeline: resized in 309 ms
flat-14000.png  metadata: Input image exceeds pixel limit pipeline: Input image exceeds pixel limit
flat-17000.png  metadata: Input image exceeds pixel limit pipeline: Input image exceeds pixel limit
Enter fullscreen mode Exit fullscreen mode

Three things to take from that:

  • The check happens at the header. Even metadata() throws, and it threw in a median of 0.19 ms over 5 runs on the 289-megapixel file. You pay nothing for a refused file.
  • The limit is "exceeds", not "reaches". The 10000 × 10000 file is exactly 100,000,000 pixels and passed a cap of 100,000,000.
  • It applies to create too. My first try at making the 17000 × 17000 test image failed with the same error until I passed limitInputPixels: false to the constructor.

How does MAX_IMAGE_PIXELS work in Pillow?

The Pillow Image reference says it warns with DecompressionBombWarning over MAX_IMAGE_PIXELS, and raises DecompressionBombError when the pixel count is greater than twice that. None disables the check.

# pil_limits.py
import warnings
from PIL import Image

files = ['flat-9000.png', 'flat-10000.png', 'flat-14000.png', 'flat-17000.png']
print('default MAX_IMAGE_PIXELS:', Image.MAX_IMAGE_PIXELS)

def probe(limit):
    Image.MAX_IMAGE_PIXELS = limit
    print(f'\nMAX_IMAGE_PIXELS = {limit}')
    for f in files:
        with warnings.catch_warnings(record=True) as caught:
            warnings.simplefilter('always')
            try:
                with Image.open(f) as im:
                    result = f'opened {im.size[0]} x {im.size[1]}'
            except Image.DecompressionBombError as e:
                result = f'DecompressionBombError: {str(e)[:70]}'
        warned = [w.category.__name__ for w in caught]
        print(f'{f:<15} {result}  warnings={warned}')

probe(89_478_485)
probe(100_000_000)
Enter fullscreen mode Exit fullscreen mode
default MAX_IMAGE_PIXELS: 89478485

MAX_IMAGE_PIXELS = 89478485
flat-9000.png   opened 9000 x 9000  warnings=[]
flat-10000.png  opened 10000 x 10000  warnings=['DecompressionBombWarning']
flat-14000.png  DecompressionBombError: Image size (196000000 pixels) exceeds limit of 178956970 pixels, could  warnings=[]
flat-17000.png  DecompressionBombError: Image size (289000000 pixels) exceeds limit of 178956970 pixels, could  warnings=[]

MAX_IMAGE_PIXELS = 100000000
flat-9000.png   opened 9000 x 9000  warnings=[]
flat-10000.png  opened 10000 x 10000  warnings=[]
flat-14000.png  opened 14000 x 14000  warnings=['DecompressionBombWarning']
flat-17000.png  DecompressionBombError: Image size (289000000 pixels) exceeds limit of 200000000 pixels, could  warnings=[]
Enter fullscreen mode Exit fullscreen mode

Look at the second run. I set the cap to 100 megapixels, and the 196-megapixel file still opened. The error threshold is 200,000,000, twice the number I set.

Note: In Pillow, MAX_IMAGE_PIXELS is a warning threshold, not a limit. Python shows a given warning once per location by default, and many servers send warnings to a log nobody reads. Make it fatal with warnings.simplefilter('error', Image.DecompressionBombWarning), as Pillow's docs suggest.

With that one line added:

import warnings
from PIL import Image

Image.MAX_IMAGE_PIXELS = 100_000_000
warnings.simplefilter('error', Image.DecompressionBombWarning)
Enter fullscreen mode Exit fullscreen mode
flat-10000.png opened (10000, 10000)
flat-14000.png DecompressionBombWarning
Enter fullscreen mode Exit fullscreen mode

Now the cap means what it says. Catch both DecompressionBombWarning and DecompressionBombError and answer with a 4xx.

What cap should you set?

Size it from memory, not from what feels big. A full RGB decode needs width × height × 3 bytes before any library overhead: 588 MB for the 14000 × 14000 file, and Pillow peaked at 814 MB. So:

  • Pick a number your worker can hold a few of at once. A 100-megapixel cap means up to 300 MB of RGB per decode before overhead. Multiply by your concurrency.
  • Set it in both places when you use both libraries. sharp's default allows 268 megapixels; Pillow's error starts at 179 megapixels. A file can pass one and fail the other.
  • Resize in the same sharp pipeline. The resize to 1920 peaked at 232 MB against 689 MB for the raw decode, because libvips streams the image instead of holding it all.

Our own API's plans document lists maxPixels: 100000000 for the free plan. The same cap guards our PNG compressor, where a flat PNG like these can hide a huge image in a small file.

How I measured

  • Files: four single-colour PNGs made with sharp's create at compression level 9, 9000 × 9000 to 17000 × 17000.
  • sharp 0.35.4 (libvips 8.18.6) on Node 24.22.0. Pillow 11.3.0 on Python 3.9.6, in a venv.
  • Memory: maximum resident set size from /usr/bin/time -l, one process per run. Baselines were 44 MB for an empty Node process and 26 MB for Python importing Pillow.
  • Time: the resize times in the output are single runs; the 0.19 ms refusal is a median of 5. Medians of 5 for the resizes were 265 ms (81 MP) and 401 ms (196 MP).
  • Apple M1 Pro, macOS, 2026-10-17.

What I did not test

Real photographs, where decode memory and time are higher than for a flat colour. I did not test JPEG shrink-on-load, sharp's unlimited option, or Pillow's Image.draft().


I build Blinkimg, an image compressor and converter with a developer API. I wrote this article with help from an AI assistant (Claude), which ran the code on my machine; every number in it comes from those runs.

Top comments (0)