Rendering

An edit can be drawn in two places: on the immich-edit server, and in the browser you edit from. Each has its own GPU, and they do different jobs. Knowing which one does what tells you where a graphics card helps.

What runs where

Work Runs on
Downloading and decoding the original from Immich Server
Source settings: noise reduction, capture sharpening, lens correction, retouch, camera profile, crop and rotation Server
Every other Develop and mask adjustment in the editor preview, including 1:1 tiles Browser with WebGPU, otherwise server
Histogram and scopes Wherever the preview was drawn
The original in split view Server
Exports, batch jobs and edited thumbnails Server
AI mask models Server
AI Denoise Server

With browser previews, the server prepares the photo once: it decodes the original, applies the source settings, and sends the result to the browser. The browser applies everything else locally, so dragging exposure, white balance, tone, color, mask or effect sliders sends nothing to the server. Changing a source setting asks the server for a new copy, and the preview keeps using the old one until it arrives.

Zoomed past the fit size, the browser asks for the visible region only. A GPU server adds the whole-photo measurements that Dehaze, Texture, Clarity and Shadows need, even for photos too large for the GPU, so the zoomed view matches the server’s render. Changing Dehaze, Lens Blur, Exposure, Texture, Clarity or Shadows can fetch a new copy of the region, and the view keeps drawing the old one until it arrives. A server without a GPU sends a wider region instead.

The server

The server renders on a GPU through Vulkan on Linux or Metal on macOS, and falls back to a built-in CPU renderer. IMMICH_EDIT_RENDERER chooses between them; see configuration.

A photo larger than the GPU’s texture or buffer limits, or a render larger than its memory budget, renders its sensor stage (demosaic, lens corrections, noise reduction and capture sharpening) in tiles. When the rest of the render still does not fit, it renders the output in tiles too. Tiles share one whole-photo measurement for Dehaze, Texture, Clarity and Shadows, so the stitched result matches a single-pass render exactly. A render that no tile size fits goes to the CPU, within its memory budget, such as one whose healing or clone strokes span most of the photo. The next render uses the GPU again. Only a lost GPU device takes the GPU out of use until the server rebuilds it.

Inside Docker, the server can only use a GPU that is passed through to the container. See GPU passthrough.

Without a GPU, the server does not drop straight to its CPU renderer. The image ships Mesa, whose llvmpipe driver presents itself as a Vulkan device and runs the GPU code on CPU cores. Settings > Diagnostics then shows GPU type as software rasterizer. This is expected on a host with no graphics card. On a 6-core arm64 container, llvmpipe rendered a 6000x4000 frame in 0.54x the built-in CPU renderer’s time at 1024 px and 0.72x at 2048 px, and matched it at 4096 px. Real graphics hardware is much faster than either.

A server GPU still matters with browser previews on. It speeds up:

  • Opening a photo and changing a source setting
  • Exports, batch jobs and edited thumbnails
  • Previews for browsers that render on the server
  • AI masks

On a CPU-only server, browser previews keep slider feedback fast. The waits move to opening a photo, changing a source setting, and exporting.

GPU memory

Server renders share one GPU memory budget, 90% of the memory the GPU reports. A GPU that shares system memory, such as Apple silicon or an integrated GPU, is also capped at the host budget, and its renders count against that budget in full. Set GPU_MEMORY_BUDGET_MB to override it, for example when other programs use the same GPU. A GPU that reports no memory size has no budget unless you set one.

A render waits until it fits next to the renders already running, and a render larger than the whole budget renders in tiles, or on the CPU when no tile size fits. If the GPU runs out of memory anyway, the server lowers the budget until it restarts and retries the render in tiles.

Settings > Diagnostics shows the GPU memory, the budgets and the texture cache size.

CPU memory

CPU renders, and the system memory that GPU renders use for uploads, readback and encoding, share one host budget: the server’s RAM, or its container limit, minus RAW_FRAME_CACHE_MB and QUALITY_FRAME_CACHE_MB. A render waits until it fits, and the server frees cached render stages first. The CPU renderer cannot tile, so a render or decoded photo larger than the whole budget fails with This photo is too large for the server's memory.

The browser

The editor draws previews itself when the browser supports WebGPU and treats the page as secure. Browsers only offer WebGPU to secure pages: HTTPS, or localhost on the same machine. Opening immich-edit at http://<server-address>:3000 from another computer is not secure in that sense, so it uses server previews. Everything still works; slider moves just wait on the server. The next section lists ways around it.

The browser renderer uses the GPU of the computer you edit from, not the server’s. It loads when the editor opens a photo. Each browser keeps its own choice under Settings > This device > Preview rendering:

  • Fastest, the default, times both renderers and uses whichever is clearly quicker.
  • Browser always draws previews locally, even when the server is faster.
  • Server always asks the server and never loads the browser renderer.

Fastest compares time per megapixel. The browser renderer times a short benchmark when it loads and then every preview it draws. Server previews are timed from the moment the server starts rendering until the browser has decoded the image, so queueing behind other requests does not count. The browser wins only when it takes at most 80% of the server’s time; a closer race goes to the server, which needs no source download. It also stays on the server when the browser’s GPU cannot hold a preview as large as the window needs. The choice is made when a photo opens and holds until the next one, so the renderer never switches mid-edit. Each browser remembers its measurements: the first photo after a reload uses the last answer, and previews come from the server only until both speeds are known.

When the browser renderer fails, the editor switches to server previews for the rest of the session. Switching to another setting and back retries. Settings > Diagnostics shows the setting, the decision and its reason, both measured speeds, and the browser’s GPU adapter and limits; see troubleshooting.

Exports never use the browser. The server renders them from the original at full resolution.

Browser previews on a local network

This is a browser rule, not an immich-edit setting, so the fix is on the browser or network side. Pick whichever suits your setup. Each one also changes the address you open, so if you sign in with OAuth, register that address at the identity provider.

Chrome or Edge: allow your server’s address. Takes a minute and needs nothing on the server.

  1. Open chrome://flags/#unsafely-treat-insecure-origin-as-secure, or edge://flags/#unsafely-treat-insecure-origin-as-secure in Edge.
  2. Enter the exact address you use, such as http://192.168.1.20:3000, and set the flag to Enabled.
  3. Relaunch the browser.

Repeat it on each computer and browser you edit from. The flag only covers the addresses you list, so only add your own server. Safari has no equivalent.

An SSH tunnel: use localhost. Works in every browser, for one computer at a time:

ssh -N -L 3000:localhost:3000 <user>@<server-address>

Then open http://localhost:3000 on that computer.

HTTPS on your network. Works in every browser and on every device:

  • Tailscale Serve gives the server an HTTPS address with a trusted certificate inside your tailnet: run tailscale serve --bg 3000 on the host.
  • A reverse proxy such as Caddy issues its own certificate for a LAN address. Browsers warn about it until you install Caddy’s root certificate on your computers; Chrome and Edge run browser previews once you accept the warning.

    https://192.168.1.20 {
      tls internal
      reverse_proxy 127.0.0.1:3000
    }
    

Settings > Diagnostics shows WebGPU needs HTTPS or localhost as the browser renderer’s reason while none of these is in place.