Skip to content

Document the image processing limits - #1208

Open
ajwild wants to merge 19 commits into
image-upload-temp-directoriesfrom
image-processing-limits
Open

Document the image processing limits#1208
ajwild wants to merge 19 commits into
image-upload-temp-directoriesfrom
image-processing-limits

Conversation

@ajwild

@ajwild ajwild commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Documents the image processing limits added in these server PRs:

Based on #1207, which introduces the section it extends — review that one first.

  • Documents the new mediaLibrary.images.processing.timeout and mediaLibrary.images.processing.maxConcurrentResizes options.
  • Adds a Sizing the Processing Limits section: how the two concurrency options relate to Node.js' thread pool, why os.availableParallelism() rather than os.cpus().length is the number to size against, and recommended values per container CPU limit.
  • Describes maxConcurrentProcesses as covering an upload end to end, which is what it does as of server PR #9865.
  • Notes that uploadProcessingDirectory now holds every transformed upload rather than only large animated ones, and corrects the volume sizing guidance: the worst case is 2GB rather than 300MB for the maxFileSize: 100mb that customers commonly configure.
  • Adds a release-2026-09 feature entry covering all of the above plus the raised upload timeout, the shared job for concurrent requests for the same variant, and the one-thread-per-image change.

ajwild and others added 19 commits August 12, 2026 15:33
Covers the new processing.timeout and processing.maxConcurrentResizes options,
the raised upload timeout, and the one-thread-per-image change.
Both limits and the thread pool they share have to be set together, and none of
them can be derived from the host's core count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The legacy transform runs while the result is streamed to storage, outside the
maxConcurrentProcesses queue, so the thread pool is the only limit on it — which
makes a large thread pool a memory risk rather than a free win.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The thread pool, not the two concurrency options, is what decides how many
images are processed at once, so the sizing advice follows from both together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Link both configuration sections instead of restating how the limits interact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-frame limit and the frame count multiplied out to far more than either
suggests, which is what the new option bounds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-image figure was measured at roughly 200MB. It is about 4 bytes
per pixel of every frame, so a 200 megapixel animation takes 800MB, and
the sizing guidance was recommending combinations that exceed a
container's memory by an order of magnitude.
Still images stream, so their cost follows the variant being produced
rather than the source. Only one of the two paths decodes anything:
variants with use2025Behavior, uploads without it, never both.
maxTotalResolution bounds raster work to well under 20 seconds, so the
hour-long processing the timeout was described as preventing is no longer
reachable. SVG rendering cost is not measured by any pixel limit.
The headroom under the timeout is a function of maxTotalResolution, which
is meant to be raised. Processing time scales with it linearly, so the
timeout is what still holds at a high or unbounded limit.
The legacy transform now writes its result to uploadProcessingDirectory as well,
so the volume holds two files per in-flight upload rather than one.
Sizing the concurrency limits from the memory table forces the count to the
largest image the resolution limit allows; the budget states the limit directly.
The memory bound reads the container's limit at startup and the variant limit
follows the thread pool, so neither is a number an operator has to translate
from a container size any more. The sizing guidance shrinks to the two choices
that are left.
The memory budget took over the decode, so lowering the limit no longer relieves
memory pressure. What it still bounds is temp disk and concurrent writes to
storage.
Comment thread content/customising/server-configuration/_index.md

Image processing runs on Node.js' thread pool, which is also what every file, DNS and compression operation in the process uses. Its default size is **4 threads**. An image takes a thread once one is free and then holds it for the whole operation — several seconds for a large animated image.

Keep `UV_THREADPOOL_SIZE` a little above the container's CPU limit, which `os.availableParallelism()` reports. Do not use `os.cpus().length`: it reports the host's cores and ignores the container's CPU limit. The headroom stops file and DNS operations from queueing behind image processing, while a much larger pool mostly buys memory — for a screenful of uncached animated thumbnails, raising it from `4` to `16` took the time from 12.8s to 8.8s and the peak from 2.6GB to 8.2GB. The memory reservation is what holds that second figure down now, but the pool is still what decides whether the extra threads are worth having.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tested it, it reports the same with both commands :)

> os.availableParallelism()
2
> os.cpus().length
2

It shows the Container CPU limit

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect that Kubernetes handles things better (some kind of system level isolation) than the testing I did with local docker containers.

@JordiVM

JordiVM commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

In summary, we have to define a sensible maxResolution and maxTotalResolution that match the Memory limit of the container, and configure the storage mounted path on the container. What is the path used for that? I can't see a config in the docu.

@ajwild

ajwild commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

It's **uploadProcessingDirectory** (string, default os.tmpdir()) and **resizingDirectory** (string, default os.tmpdir()), which is most likely returning /tmp for a linux container. They can both point to the same mount point (as they do with /tmp) because we always work within a nanoid directory (e.g. ${localFolder}/${nanoid()}).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants