Troubleshooting
Fix the common payload-images failures fast — 404s, missing admin panels, over-fetching, off-center crops, and stale pages.
Each failure below leads with what you see, then the cause, then the fix. Scan the first column for your symptom.
| Symptom | Cause | Fix |
|---|---|---|
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.