Payload Plugins
Pluginspayload-images

Troubleshooting

Fix the common payload-images failures fast — 404s, missing admin panels, over-fetching, off-center crops, and stale pages.

For AI / LLMs: View Markdown

Each failure below leads with what you see, then the cause, then the fix. Scan the first column for your symptom.

SymptomCauseFix
An <img> src points at /api/img/… but returns 404, or images never optimize.Sharp is being bundled — its native binary breaks when bundled.Add serverExternalPackages: ['sharp'] to next.config.ts (Turbopack and webpack both need it).
src/srcset are absent from a read.An explicit select left them out — the virtual URL fields are always registered, but a select still decides what comes back.Add them to the select, or use the ready-made RESPONSIVE_IMAGE_SELECT. See Rendering images.
The focal-point picker or the Presets & variants panel is missing or blank in the admin.Admin components register by string path; the import map is stale.Run pnpm payload generate:importmap and restart the dev server. (collections.images.options.focalUI defaults on, so the components are registered.)
The browser downloads a huge image into a small slot (over-fetching).sizes left at its 100vw default.Set sizes to how big the image actually renders, e.g. sizes="(max-width: 768px) 100vw, 33vw". See Rendering images.
The crop is off-center or the wrong shape.The CSS box disagrees with the crop — usually an aspectRatio passed to the component that differs from the one the read declared.Declare it once, on the read (imageFor(id).aspectRatio('16:9')) and let the doc spread carry it. Only pass aspectRatio to the component when you deliberately want a different box; for the focal point itself, see Focal point & crop.
Edited an image but a page still shows the old one.The page is cached. Image URLs self-bust; the page cache doesn't.Revalidate that route. See Caching → Revalidation.
A bulk upload reports errors, and re-uploading one of those files now fails at the storage provider — that filename is unusable no matter what.clientUploads puts the file in the store before the document is validated, and the admin's bulk-upload screen submits without validating first. A rejected save (a missing alt, most often) leaves an object no document owns, and the provider won't write that name twice.Turn on the adapter's random-suffix option for the client-upload collections — e.g. vercelBlobStorage({ clientUploads: true, addRandomSuffix: true, … }). Every attempt then writes a new name, so a rejected save costs a stray object instead of the filename. Leave it off for server-side collections, which upload after validation and keep clean names. See the callout below.
Boot fails with [payload-images] collections.images: field(s) … are already defined by the plugin.A field in your collections.images.overrides.fields has the same name as one the plugin injects (alt, variants, placeholder, palette, …) — overrides append, so the name would collide.Rename or remove your field. See Collections → Renaming a collection.
The prewarm plan shows fewer variants than expected, or the status endpoint / admin panel reports the plan is capped.The strategy derives more targets than maxVariantsPerImage (default 32) allows — the plan is truncated: lower-priority units (quieter learned profiles first) lose budget first, and a unit that doesn't fit keeps an even spread of its widths.Raise options.prewarm.maxVariantsPerImage, or narrow the strategy (widths: { every: 2 }, fewer seeds, learned: false). See Prewarming → What gets warmed.
Uploads succeed but prewarm jobs pile up and never run.strategy.autoRun: false turns off all three built-in runners (the cron, the post-upload kick, the image-traffic drain) — or the queue only holds a big CLI-enqueued backlog on a site with too little traffic to drain it.Leave autoRun on (the kick and drain cover serverless too), or run the jobs yourself: a cron hitting /api/payload-jobs/run?queue=images-prewarm (e.g. a Vercel Cron), or payload images:prewarm --now as a build step. See Prewarming → When it runs.

With clientUploads, storage is written before the document is validated. The browser sends the file straight to the provider and only then posts the document, so nothing server-side — no hook of this plugin's or Payload's — runs early enough to stop a rejected save from orphaning the object. A random suffix is what keeps that orphan from costing you the filename; server-side uploads don't need it, because they're written from an afterChange hook once the document is already saved.

Don't gate clientUploads on NODE_ENV. Running server uploads in dev and client uploads in production means the two paths differ in exactly the way that matters, and a failure like this one can't reproduce locally. Turn it on in both, and let dev exercise what production does.

Image URLs self-bust; your page cache doesn't. A cached page keeps serving the old URL until you revalidate that route — exactly as it would for any Payload content. Content-addressed URLs refresh themselves on file replace or focal move; the page serving them does not.