Sharp Image Processing in the Strapi Upload Plugin#
Overview#
Sharp is the core image processing engine used by Strapi's upload plugin. All image manipulation — metadata extraction, resizing, optimization, thumbnail generation, and animation handling — runs through Sharp (backed by libvips). The primary implementation lives in image-manipulation.ts.
Key Source Files#
| File | Purpose |
|---|---|
packages/core/upload/server/src/services/image-manipulation.ts | All Sharp operations: metadata, resize, optimize, thumbnails, format detection |
packages/core/upload/server/src/register.ts | Plugin bootstrap: applies Sharp global settings (cache, concurrency) |
packages/core/upload/server/src/config.ts | Default config values including Sharp defaults |
packages/core/upload/server/src/types.ts | Config interface with sharp field definition |
Format Support#
Three format tiers control which operations apply :
| Constant | Formats | Used for |
|---|---|---|
FORMATS_TO_PROCESS | jpeg, png, webp, tiff, svg, gif, avif | isImage() check |
FORMATS_TO_RESIZE | jpeg, png, webp, tiff, gif | isResizableImage() check |
FORMATS_TO_OPTIMIZE | jpeg, png, webp, tiff, avif | isOptimizableImage(), quality reduction |
Note: SVG and AVIF are not resizable. GIF is resizable but not optimizable (no quality pass). The
isSupportedImagemethod was removed in Strapi 5; useisImageorisOptimizableImageinstead .
Core Operations#
Metadata & Dimensions#
getMetadata() supports both file-path and stream inputs. For path-based files it calls sharp(file.filepath).metadata(); for streams it pipes through a sharp() pipeline. getDimensions() wraps this to return { width, height }.
Optimization#
optimize() runs when either sizeOptimization or autoOrientation is enabled in Media Library settings:
- Quality reduction: Re-encodes the image at
quality: 80(whensizeOptimizationis on) orquality: 100. - Auto-rotation: Calls
transformer.rotate()to apply EXIF orientation . - Safety check: If the optimized output is larger than the original, the original file is returned unchanged .
- Both stream and filepath paths are supported;
{ animated: true }is passed to Sharp to preserve animation frames .
Thumbnail Generation#
generateThumbnail() only runs if the image exceeds the threshold. The fixed thumbnail size is 245 × 156 px, fit: 'inside' . If the image is smaller than these dimensions, no thumbnail is created.
Responsive Breakpoints#
generateResponsiveFormats() generates one resized variant per configured breakpoint, but only for dimensions smaller than the original. Default breakpoints :
| Name | Max dimension |
|---|---|
| large | 1000 px |
| medium | 750 px |
| small | 500 px |
Breakpoints can be overridden in /config/plugins via the breakpoints key. Custom sizes (e.g., xlarge: 1920, xsmall: 64) are supported . Breakpoint changes only apply to new uploads.
Each breakpoint is processed with resizeFileTo(), which calls sharp({ animated: true }).resize(options) to preserve all frames in animated GIFs and WebPs. The pageHeight field (single-frame height) is used for dimension reporting of animated images .
Faulty Image Detection#
isFaultyImage() calls sharp.stats() on the file to detect corrupted or malformed images before further processing.
Memory Management & Configuration#
Uploading large or high-megapixel images previously caused OOM crashes on memory-constrained environments (e.g., Strapi Cloud free tier with 512 MB RAM). The root cause: Sharp/libvips would decode the full source image for every responsive breakpoint concurrently, while also retaining cached pixel data between operations .
Defaults set at plugin registration :
sharp.cache(false)— disables libvips' decoded-image cache.sharp.concurrency(1)— limits the libvips thread pool to 1.- Responsive breakpoints are generated sequentially (not via
Promise.all) to cap peak memory usage.
These defaults are configurable via config/plugins.ts:
upload: {
config: {
sharp: {
cache: true, // re-enable if you have RAM headroom
concurrency: 2, // increase for multi-core, high-memory deployments
},
},
}
The Config type and default values document this shape. The concurrentUploadSize config key (default 1) provides an additional parallel upload limit .
Related Memory Issues#
A separate memory-growth issue (issue #25199) was tracked and resolved by two fixes:
- PR #26046 — Sharp cache/concurrency defaults (described above).
- PR #26678 (≥ 5.48.1) — MIME detection was reading entire files into memory and retaining a subarray view; now uses a flat 4100-byte read.
Both fixes are present in current develop. If memory still grows, take a heap snapshot with --inspect to distinguish V8-heap vs. native (libvips) retention.
Animated Image Support#
Prior to the fix in PR #26126, animated GIFs and WebPs lost all frames after upload because sharp() was called without { animated: true }. The fix:
- Passes
{ animated: true }to allsharp()constructor calls inresizeFileTo()andoptimize(). - Uses
pageHeight(height of a single frame) instead ofheight(stacked height of all frames) for dimension metadata. - A
declare module 'sharp'augmentation addspageHeighttoOutputInfotypes as a workaround until Sharp ≥ 0.34.2 .
Large Upload Handling#
For uploads exceeding defaults, two layers must be configured :
- Body middleware (
config/middlewares): increaseformidable.maxFileSizeandformLimit. - Plugin config (
config/plugins): setsizeLimit(in bytes; default 1 GB perconfig.ts). - Request timeout:
strapi.server.httpServer.requestTimeoutdefaults to 330 seconds; increase inconfig/serveror viabootstrap().
Upstream proxies (nginx client_max_body_size, load balancers) also need adjustment for large files.