Fix Pillow Errors on Django Image Uploads in Production

Fix Pillow errors when uploading images to Django in production: Cannot use ImageField because Pillow is not installed, decoder jpeg not available, cannot identify image file, DecompressionBombError, and HEIC uploads.

Problem statement

Image uploads work on your laptop and fail on the server with one of these:

ERRORS: shop.Product.photo: (fields.E210) Cannot use ImageField because Pillow is not installed.
OSError: decoder jpeg not available
PIL.UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x7f...>
PIL.Image.DecompressionBombError: Image size (220000000 pixels) exceeds limit of 178956970 pixels

ImageField uses Pillow to verify that an upload is a real image and to read its dimensions. Production problems come from Pillow being missing from the deployed environment, being built without image codecs, receiving formats it cannot read (HEIC from iPhones, truncated files), or protecting the server from giant images.

How Django validates an imageUpload arrivesvia ImageFieldPillow opens itneeds codecsFormat checkJPEG, PNG, WebPSize limitsbomb protectionImageField depends on Pillow and its codecs being present in the production environment.
  1. Pillow is not installed: add Pillow to production requirements and install it in the venv or image Django runs from.
  2. decoder jpeg not available: Pillow was compiled without libjpeg; install a binary wheel (pip install --only-binary=:all: Pillow) or the libjpeg dev package before building.
  3. cannot identify image file: the upload is not a supported image (HEIC, SVG, truncated); return a clear form error or add a HEIC plugin.
  4. DecompressionBombError: reject huge images, or raise Image.MAX_IMAGE_PIXELS deliberately.
  5. Check DATA_UPLOAD_MAX_MEMORY_SIZE, FILE_UPLOAD_MAX_MEMORY_SIZE, and Nginx client_max_body_size for size-related failures.

Step-by-step solution

1. Confirm Pillow is in the running environment

/srv/app/.venv/bin/python -c "import PIL; print(PIL.__version__)"
docker compose exec web python -c "import PIL; print(PIL.__version__)"

If this fails, Pillow is in a dev requirements file only, or the server runs a different venv than you installed into. Add it to the requirements that production installs, rebuild the image, and restart.

2. Check which codecs Pillow has

python -m PIL.report

Look for lines such as --- JPEG support ok and --- WEBP support ok. A source build on a machine without libjpeg, zlib, or libwebp headers produces a Pillow that installs fine but cannot read those formats.

Prefer the official binary wheels, which bundle the codecs:

pip install --only-binary=:all: Pillow

Pillow publishes wheels for Linux (glibc and musl), macOS, and Windows on common architectures. If you must build from source (unusual platforms), install the dev packages first, for example libjpeg62-turbo-dev zlib1g-dev libwebp-dev libtiff-dev on Debian.

error to causeErrorCause and fixPillow is not installed (E210)missing from prodrequirements or wrong venvdecoder jpeg not availablebuilt without libjpeg;use the binary wheelcannot identify image fileHEIC, SVG, or truncatedfile; clear form errorDecompressionBombErrorhuge pixel count; rejector raise limit knowinglypython -m PIL.report shows what the production Pillow can readdjango-deployment.com
Run PIL.report inside the production container, not on your laptop.

3. Handle formats Pillow cannot read

iPhones upload HEIC by default when the browser does not convert it. Pillow does not read HEIC natively. Either add the plugin:

pip install pillow-heif
# apps.py ready()
from pillow_heif import register_heif_opener
register_heif_opener()

or accept only formats you support and say so in the form:

from django.core.validators import FileExtensionValidator

photo = models.ImageField(upload_to="products/", validators=[FileExtensionValidator(["jpg", "jpeg", "png", "webp"])])

SVG is XML, not a raster image; ImageField rejects it by design. Use a FileField with sanitisation if you need SVG uploads, because SVG can contain scripts.

4. Protect against decompression bombs

A small file can decode to billions of pixels and exhaust memory. Pillow warns above about 89 million pixels and raises above about 179 million. Keep the protection, and set your own lower limit in validation:

from django.core.exceptions import ValidationError
from PIL import Image

def validate_image_size(f):
    img = Image.open(f)
    if img.width * img.height > 40_000_000:
        raise ValidationError("Image is too large; maximum is 40 megapixels.")
    f.seek(0)

Raising Image.MAX_IMAGE_PIXELS globally should be a deliberate choice for trusted sources only.

5. Check upload size limits

Large photos also hit size limits before Pillow sees them: Nginx returns 413 above client_max_body_size (1 MB by default), and Django streams files larger than FILE_UPLOAD_MAX_MEMORY_SIZE to a temporary directory that must be writable. See Fix Nginx 413 Request Entity Too Large.

image uploads that work in productionPillow in production requirementsbinary wheel with codecsPIL.report checked in the imageJPEG, PNG, WebP okAccepted formats stated and validatedHEIC plugin if neededPixel limit below Pillow defaultprotects worker memoryclient_max_body_size fits real photosphones send 5-15 MBMost failures are environment differences, not code bugsdjango-deployment.com
Add an upload test with a real JPEG and WebP to your CI or smoke tests.

Verification check: upload a JPEG, a PNG, and a WebP through the production form, and try an unsupported file to confirm the user sees a clear validation message rather than a 500.

Explanation

Pillow is a thin Python layer over C libraries for each image format. ImageField.to_python opens the upload with Pillow and calls verify(); anything Pillow cannot decode becomes a validation error, and missing codecs become exceptions. Because Pillow's capabilities depend on how it was built, the same code can behave differently on a laptop with a wheel and a server that compiled Pillow from source. Binary wheels make the environment predictable.

Edge cases / notes

  • Thumbnails generated with sorl-thumbnail or easy-thumbnails also depend on these codecs; failures appear later, at render time.
  • EXIF orientation. Phone photos may appear rotated; apply ImageOps.exif_transpose when processing.
  • Memory. Processing images in web requests can spike worker memory. Move resizing to Celery for large files.
  • Alpine images need musl wheels; mixing glibc builds causes import errors.

Next steps

FAQ

Why does ImageField need Pillow?

Django uses Pillow to verify that uploads are valid images and to read their dimensions.

How do I fix decoder jpeg not available?

Reinstall Pillow from the official binary wheel, or install libjpeg development headers before building from source.

Can Django accept iPhone HEIC photos?

Yes, with the pillow-heif plugin registered at startup, or by converting on the client.

Is it safe to disable the decompression bomb check?

Only for trusted input. For user uploads, keep it and set a stricter limit.

Keep going