Umbraco.Community.Imaging.ImageSharp
1.2.0
dotnet add package Umbraco.Community.Imaging.ImageSharp --version 1.2.0
NuGet\Install-Package Umbraco.Community.Imaging.ImageSharp -Version 1.2.0
<PackageReference Include="Umbraco.Community.Imaging.ImageSharp" Version="1.2.0" />
<PackageVersion Include="Umbraco.Community.Imaging.ImageSharp" Version="1.2.0" />
<PackageReference Include="Umbraco.Community.Imaging.ImageSharp" />
paket add Umbraco.Community.Imaging.ImageSharp --version 1.2.0
#r "nuget: Umbraco.Community.Imaging.ImageSharp, 1.2.0"
#:package Umbraco.Community.Imaging.ImageSharp@1.2.0
#addin nuget:?package=Umbraco.Community.Imaging.ImageSharp&version=1.2.0
#tool nuget:?package=Umbraco.Community.Imaging.ImageSharp&version=1.2.0
Umbraco.Community.Imaging.ImageSharp
Stops Umbraco sites being OOM-killed in memory-limited containers while browsing the media section.
This is a backport of the fix proposed for the CMS in umbraco/Umbraco-CMS#23557 (issue #23556, also proposed for v13 and v18), for sites that need it before it ships in the product. Remove this package once you are on an Umbraco version that includes the fix — see Migrating off this package.
The problem
If you run Umbraco in a Linux container with a memory limit, browsing the media section grows
memory until the container is killed — docker inspect shows OOMKilled=true and exit code
137. Windows hosting does not behave the same way, and setting DOTNET_GCHeapHardLimit and
friends makes no difference, which makes it look like a leak.
It is not a leak. ImageSharpMiddleware decodes the source image at full resolution before any
processor runs, and it only de-duplicates concurrent requests for the same URL. A media grid
renders a screenful of distinct thumbnails, so every source decodes in parallel with no ceiling.
Peak memory is:
concurrent requests × decoded source size
which is unbounded. Measured with VmHWM (peak RSS), one request, no concurrency: a single
300×300 thumbnail of a 4000×3000 JPEG costs +60 MB. Sixteen concurrent thumbnail requests is
~960 MB, so a 512 MB container dies in seconds.
Two further things stop the memory coming back promptly, both unmanaged and therefore outside
anything DOTNET_GC* controls:
- ImageSharp's pool is sized at
GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / 8and trims only on a gen2 collection, at most 50% per 60 s unless memory load exceeds 90%. Measured: 176 MB still held at idle in a 2 GB container, surviving a forced gen2 collection. - The pool blocks come from
Marshal.AllocHGlobal(=malloc).malloc_trim(0)released a further 20–45 MB that GC alone did not — glibc retention, which is why Windows differs.
You may also see docker stats reporting far more memory than the process actually uses. Cgroup
accounting includes page cache from MediaCache writes — measured 1016 MB cgroup against 441 MB
process RSS. The kernel reclaims that under pressure, so it is not what kills the container, but it
does make monitoring overstate the problem.
What this package does
It adds two limits, both derived from the memory available to the process (which honours the container limit):
| Setting | Purpose | Derived default |
|---|---|---|
MaximumPoolSizeMegabytes |
Caps the unmanaged buffer pool ImageSharp retains between requests | available / 32, clamped to 16–64 MB |
MaximumConcurrentProcessing |
Caps how many images are processed at once | (available / 2) / 64 MB, capped at processor count |
The concurrency cap is enforced by a middleware registered ahead of UseImageSharp() in
Umbraco's pre-pipeline. Requests over the limit wait; they are not rejected. Only requests whose
path has a file extension and whose query carries a registered ImageSharp processor command are
gated, so an API call that happens to carry a width is unaffected.
On a host with no container limit the derived concurrency lands at the processor count, so non-containerised sites are effectively unthrottled.
Results
Same load that produced exit 137 — 512 MB limit, 16 concurrent distinct crop/resize requests against 4000×3000 JPEG sources, every request a cache miss:
| without the package | with the package | |
|---|---|---|
| outcome | OOMKilled, exit 137 | survives |
| peak RSS | >512 MB | 285 MB |
| idle RSS after two bursts | — | 189 MB |
| derived settings | — | poolMB=16 maxConcurrent=3 (available 384 MB, 28 CPUs) |
Behaviour by concurrency without the package, 512 MB limit:
| concurrency | result |
|---|---|
| 1 | stable, 167 MB RSS |
| 8 | survives, peak 453 MB |
| 16 | OOMKilled, exit 137, within 5 s |
Steady state at a 2 GB limit, concurrency 8, idle after two bursts of load:
| idle RSS | pooled unmanaged | |
|---|---|---|
| without | 409 MB | 176 MB (44 handles × 4 MB) |
| with | 251 MB | 16–32 MB |
Cache hits are unaffected: 200 warm-cache requests complete in 3.5 s through a semaphore of 3, because a cache hit takes milliseconds and never holds the gate.
Installation
Pick the package that matches the imaging package your site already uses:
# Default setup (Umbraco.Cms.Imaging.ImageSharp — ImageSharp 3.x)
dotnet add package Umbraco.Community.Imaging.ImageSharp
# Only if your site uses Umbraco.Cms.Imaging.ImageSharp2 (ImageSharp 2.x)
dotnet add package Umbraco.Community.Imaging.ImageSharp2
That is all — the package composes itself. There is nothing to add to Program.cs.
Install one or the other, never both. They carry the same types in the same namespace and depend
on mutually exclusive ImageSharp majors, so referencing both fails at restore. That mirrors
Umbraco.Cms.Imaging.ImageSharp and Umbraco.Cms.Imaging.ImageSharp2 themselves.
Configuration, behaviour and defaults are identical across the two — only the ImageSharp major differs, so you can switch packages without touching config.
ImageSharp 2
Umbraco.Community.Imaging.ImageSharp2 exists because ImageSharp 2.x has the same problem. Verified
with the same harness, 512 MB limit, 16 concurrent distinct crops:
| ImageSharp 2 | ImageSharp 3 | |
|---|---|---|
| without the package | OOMKilled, exit 137 | OOMKilled, exit 137 |
| with the package | survives, peak 271 MB | survives, peak 285 MB |
Both packages are built from one set of shared sources, compiled once per ImageSharp major, and run the same test suite against each — so they cannot drift apart.
Supported versions
| Umbraco | Target framework | Dependency range |
|---|---|---|
| 13 (LTS) | net8.0 |
[13.0.0, 14.0.0) |
| 17 | net10.0 |
[17.0.0, 19.0.0) |
| 18 | net10.0 |
[17.0.0, 19.0.0) |
All three ship SixLabors.ImageSharp 3.x and register imaging the same way, so one implementation covers every target.
Umbraco 14, 15 and 16 are not supported. They fall on the net8.0 asset but resolve an Umbraco
major outside its dependency range, so restore fails loudly rather than installing something
untested.
Configuration
Everything is optional. Defaults are derived at startup and suit most sites.
{
"Umbraco": {
"CMS": {
"Imaging": {
"Memory": {
"Enabled": true,
"MaximumPoolSizeMegabytes": 0,
"MaximumConcurrentProcessing": 0
}
}
}
}
}
| Setting | Default | Meaning |
|---|---|---|
Enabled |
true |
Set to false to leave ImageSharp entirely at its own defaults |
MaximumPoolSizeMegabytes |
0 |
0 derives from available memory; any positive value is used as-is |
MaximumConcurrentProcessing |
0 |
0 derives from available memory and processor count; any positive value is used as-is |
These are deliberately the same configuration keys proposed for the CMS, so nothing has to change when you migrate off the package.
Tuning
- Still being OOM-killed? Your sources are probably larger than the 64 MB per image the
derivation assumes. Set
MaximumConcurrentProcessingexplicitly to a lower number, or raise the container's memory limit. - Thumbnails feel slow on first load? The gate is only reached on cache misses. Raise
MaximumConcurrentProcessing, and check peak memory afterwards withdocker stats. - Memory still high at rest? Lower
MaximumPoolSizeMegabytes(16 is the derived floor). Below that, allocations bypass the pool entirely — correct, just a little slower.
Migrating off this package
Once you are on an Umbraco version that applies these limits itself:
- Remove the package reference.
- Leave the
Umbraco:CMS:Imaging:Memoryconfiguration exactly where it is — the keys are the same.
Leaving the package installed on such a version is harmless but redundant: you would get two throttles nested inside each other, the inner of which never blocks.
How this was verified
The measurements above come from a reproduction harness: a minimal ASP.NET Core app configured
identically to AddUmbracoImageSharp() (ClearProviders(), WebRootImageProvider, Umbraco's
CropWebProcessor, ConfigureImageSharpMiddlewareOptions, PhysicalFileSystemCache), running on
mcr.microsoft.com/dotnet/aspnet:10.0-noble under cgroup v2 with ImageSharp 3.1.12 and
ImageSharp.Web 3.2.0. Peak memory is read from VmHWM rather than sampled, so there is no sampling
race, and the split between managed heap, ImageSharp's unmanaged pool and C-allocator retention is
measured separately.
The package itself was then installed into a clean Umbraco 17.4.2 site and the throttle confirmed to sit in front of the imaging middleware, by timing four concurrent distinct crops against a single one:
MaximumConcurrentProcessing |
four concurrent crops vs. one | |
|---|---|---|
1 |
×3.57, ×3.84 | serialised, as configured |
8 |
×1.40, ×1.11 | parallel, as configured |
Had the middleware been registered after UseImageSharp() rather than before it, both rows would
read the same.
To reproduce the original problem against a real site:
- Run Umbraco in Docker with
--memory=512mon an Ubuntu-basedaspnetimage. - Upload a few dozen 4000×3000 JPEGs to the media section.
- Browse the media section page by page, so each page requests a screenful of distinct thumbnails that are not yet in the media cache.
- Watch
docker stats.
Releasing
Publishing uses NuGet trusted publishing, so no API key is stored in this repository. The workflow mints a GitHub OIDC token, and nuget.org exchanges it for a short-lived key.
One-time setup:
- On nuget.org, under the package owner's account, add a trusted publishing policy for this
repository (owner
AaronSadlerUK, repositoryUmbraco.Community.Imaging.ImageSharp, workflowrelease.yml). - In this repository's settings, add a variable (not a secret) named
NUGET_USERset to the nuget.org account name that owns the policy.
To release:
git tag v1.0.0
git push origin v1.0.0
The tag drives the package version — v1.2.3 publishes 1.2.3. .github/workflows/release.yml
builds, tests, packs and pushes; workflow_dispatch runs everything except the push, so it can be
used as a dry run.
Licence
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Umbraco.Cms.Imaging.ImageSharp (>= 17.0.0 && < 19.0.0)
-
net8.0
- Umbraco.Cms.Imaging.ImageSharp (>= 13.0.0 && < 14.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.