Limiting work per request

Copy Markdown View Source

Before exposing ImagePipe to real traffic, limit the work one request can cause: how large an original may be, how long origins and clients may take, and how much memory it uses.

This guide assumes ImagePipe is running in your app (see Getting started with Phoenix) or as image_pipe_server (see Getting started with the server).

Limiting originals

Three limits reject an original before it is processed, with 413:

  • max_body_bytes, 10 MB by default, limits the size of the file.
  • max_input_pixels, 40 million by default, limits its decoded size. For an animation it counts the frames needed to reach the requested one.
  • max_input_frames, 1,000 by default, limits the frames or pages a file may declare.

Lower them to the largest originals you expect to serve:

Plug

{ImagePipe,
 name: MyApp.Images,
 max_body_bytes: 5_000_000,
 max_input_pixels: 25_000_000,
 sources: [...]}

image_pipe_server

[processing]
max_body_bytes = 5000000
max_input_pixels = 25000000

max_result_width, max_result_height, and max_result_pixels scale the output down to fit before it is encoded.

Limiting intermediate images

Set max_intermediate_pixels to bound the size of decoded frames that are buffered in memory, downsampled, or delivered. The default is 100 million pixels. Larger frames fail with 422, including before the output size is reduced to its configured maximum.

Plug

{ImagePipe,
 name: MyApp.Images,
 max_intermediate_pixels: 50_000_000,
 sources: [...]}

image_pipe_server

[processing]
max_intermediate_pixels = 50000000

A resize or padding step may create a larger image when a later crop reads only a small part of it. The crop keeps the coordinates of the requested image. A later downsampling step reads across that larger image and is subject to the limit.

Timeouts for origins

HTTP and S3 sources wait at most 5 seconds for a connection (connect_timeout), 5 seconds for each part of the response (receive_timeout), and 30 seconds for the whole download (fetch_timeout). All three are set per source:

Plug

sources: [
  web: [
    adapter: ImagePipe.Source.HTTP,
    match: [scheme: ["https"]],
    options: [
      allowed_hosts: ["assets.example.com"],
      receive_timeout: 3_000,
      fetch_timeout: 10_000
    ]
  ]
]

image_pipe_server

[sources.web]
adapter = "http"
match = { scheme = ["https"] }
allowed_hosts = ["assets.example.com"]
receive_timeout = 3000
fetch_timeout = 10000

A download that runs past fetch_timeout fails, even when the origin keeps sending a little data within each receive_timeout. Other requests waiting for the same original give up after waiting fetch_timeout plus one second, with 504. Give the proxy or load balancer in front a total request timeout as well.

Slow clients

ImagePipe streams each image as it is encoded. A client that reads slowly keeps that request's processing slot and memory until it finishes, and the processing pool's deadline counts that time too.

Put a proxy or CDN in front that reads the whole response from ImagePipe and sends it to the client itself, such as nginx with response buffering or the CDN described in serving images through a CDN. Set processing_timeout well above the time your largest images take to make and send to that proxy.

A response cut short by the deadline ends the [:deliver] span with result: :processing_error. Failures during streaming explains what the client receives.

Memory

ImagePipe reads most images in one pass, without holding the whole decoded image in memory. Some operations need the whole image at once: trimming, rotating by an arbitrary angle, smart and object-detection cropping, and rotating a photo whose EXIF orientation says it is turned. These copy the decoded image into memory, so a 40-megapixel original can take over a hundred megabytes while it is processed.

Allow for that across the requests that run at once. A processing pool caps how many that is, and max_input_pixels caps the decoded original, and max_intermediate_pixels caps frames read during processing.

libvips also keeps its recent operations and their images in an operation cache, 100 operations by default. Each request decodes its original again, so the cache rarely gets a hit and mostly keeps decoded images in memory. In a run of 160 resizes of four 24-megapixel JPEGs, resident memory after garbage collection was 480–730 MB with the cache and 310–340 MB without it, at the same speed.

Plug

Turn the cache off when your app starts:

# lib/my_app/application.ex, in start/2
Vix.Vips.cache_set_max(0)

Vix.Vips.cache_get_max() then returns 0. The setting applies to all libvips work in the VM, including your app's own use of Image or Vix.

image_pipe_server

The server turns the cache off when it starts.

Next steps