DEV Community

pgodace
pgodace

Posted on

Your product photos get rejected for file size, not resolution

Marketplaces reject product photos far more often for file size than for dimensions, and almost nobody documents the ceiling next to the pixel size. I compiled the published upload rules for 18 marketplaces into one file, and the volume constraints turned out to be the most hostile part of it.

Here is what those numbers actually look like, and a way to stop guessing which quality setting fits.

Only 13 of 51 rules publish a cap — and they disagree by 640×

Across the 18 platforms I checked, there are 51 separate rules (main image, zoom, detail, banner, video cover…). 38 of them state no file-size ceiling at all. The 13 that do use 7 different numbers:

Platform Slot Pixels Ceiling
Pinduoduo order thumbnail 200×200 32 KB
Pinduoduo home banner (white bg) 750×352 100 KB
WeChat Store main 600×600 1024 KB
Pinduoduo main 800×800 2048 KB
Kuaishou main (white bg) 800×800 2048 KB
Temu carousel (white bg) 800×800 2048 KB
Shopee main 800×800 2048 KB
Pinduoduo SKU 480×480 3072 KB
Douyin main 600×600 5120 KB
Douyin main (white bg) 800×800 5120 KB
Douyin detail 750×1000 5120 KB
Shopify main 2048×2048 20480 KB
Shopify main (max) 5000×5000 20480 KB

32 KB → 20480 KB is a 640× spread. The photo that sails into Shopify as a 5000×5000 PNG will be bounced by Pinduoduo's 200×200 thumbnail slot unless it is under 32 KB.

Two combinations are genuinely nasty:

  • Pinduoduo's 750×352 home banner: white background and ≤ 100 KB. A wide white-background JPEG that small is a compression exercise, not a crop.
  • The 200×200 thumbnail at 32 KB: I measured this one instead of guessing, and it is the opposite of what I expected — see the numbers below. The cap is not tight for simple images; it is tight for busy ones, and a real product photo is busy.

I measured it instead of guessing

I took a 3000×2250 test image, resized it to the constrained sizes above, and encoded it as JPEG at six quality settings in Chrome:

Output size q .95 q .90 q .80 q .70 q .50 q .30
200×200 8.4 KB 5.9 KB 4.3 KB 3.8 KB 3.0 KB 2.4 KB
750×352 29.0 KB 18.4 KB 12.8 KB 10.4 KB 6.6 KB 5.4 KB
800×800 56.7 KB 33.6 KB 20.9 KB 15.5 KB 11.7 KB 9.5 KB
750×1000 64.3 KB 38.5 KB 24.0 KB 18.2 KB 13.5 KB 11.0 KB

Read the caveat before you reuse these numbers: my test image is synthetic colour gradients, which compress much better than a photograph. Fabric weave, wood grain, food texture, printed text — the things product photos are made of — cost several times more bytes at the same size and quality. So treat this table as a floor, not an estimate for your catalogue.

That reframes the 32 KB slot honestly: at 200×200 even quality 0.95 produced 8.4 KB here, so the ceiling is not absurd on its own. What makes it dangerous is that the same quality setting on a real, textured photo can be an order of magnitude larger — and you only find out at upload time.

Why "export at quality 80" doesn't solve this

File size is roughly pixels × bits-per-pixel. Resizing changes the first term by the square; quality changes the second by 3–6× across the whole usable range (measured above: 56.7 KB → 9.5 KB from q .95 to q .30 at 800×800). So:

  • Going 4000×3000 → 800×800 cuts pixel count by ~19×. That is the big lever.
  • Going quality 0.95 → 0.50 saves roughly 3–5×, and starts to be visible on product edges and fine text.

If you pick one quality for everything, you are choosing the second lever only, and you will either ship 3 MB files that fail the 100 KB slot, or crush every image to satisfy the single tightest ceiling you happen to have.

What actually fits: downscale first, then search the quality

The honest algorithm is two steps, and the second one is a search, not a guess:

  1. Resize so the image is exactly the required pixel size (never rely on the file size to imply dimensions).
  2. If a ceiling exists, binary-search the encoder quality until the encoded bytes fit.

That is what my own pipeline does, and it is not clever — it is just measurement. The search runs between quality 0.30 and 0.95 with at most 8 encode attempts, taking the highest quality that fits:

// canvas -> Blob, then binary search on quality for a byte budget
async function encodeWithBudget(encode, maxBytes) {
  let lo = 0.30, hi = 0.95, best = null;
  for (let i = 0; i < 8; i++) {
    const q = (lo + hi) / 2;
    const blob = await encode(q);           // encode(q) -> Promise<Blob>
    if (blob.size <= maxBytes) { best = { blob, q }; lo = q; }
    else { hi = q; }
    if (hi - lo < 0.02) break;
  }
  return best;                              // null => even q=0.30 doesn't fit
}
Enter fullscreen mode Exit fullscreen mode

Two properties matter more than the exact numbers:

  • It returns null instead of lying. If quality 0.30 still exceeds the ceiling, you must shrink the image or change the format — silently shipping an oversized file just moves the rejection downstream.
  • It never re-encodes more than ~8 times per image. With 51 outputs per photo you can feel an unbounded search loop on a phone.

For reference, an audit run of 15 outputs from my tool (200×200 up to 1000×2000, white-background and transparent slots mixed) produced JPEGs between 15 KB and 44 KB. That is under every ceiling in the table except the 32 KB thumbnail — and 32 KB sits inside that range, so whether an output passes depends on the image, which is the whole point.

A five-second check on your own folder

Before blaming the platform, see which files are over which ceiling. macOS / Linux:

find ./exports -iname '*.jpg' -size +2048k -exec ls -lh {} \;
Enter fullscreen mode Exit fullscreen mode

Windows PowerShell:

Get-ChildItem .\exports\*.jpg | Where-Object Length -gt 2MB | Select-Object Name, Length
Enter fullscreen mode Exit fullscreen mode

And if you want to know what a given pixel size can reach at all, measure the encoder instead of trusting a blog post:

// returns the smallest quality that fits, or null
const probe = async (img, w, h, maxBytes) => {
  const c = new OffscreenCanvas(w, h);
  c.getContext('2d').drawImage(img, 0, 0, w, h);
  return encodeWithBudget(async (q) => c.convertToBlob({ type: 'image/jpeg', quality: q }), maxBytes);
};
Enter fullscreen mode Exit fullscreen mode

The part nobody documents

The 38 rules without a published ceiling are not permission to upload 40 MB. In practice they mean "the platform will re-encode your file and may reject it if it is absurd". I kept them as maxKB: null in the dataset rather than inventing a number, because a made-up limit is worse than a missing one.

Same for the white-background slots: 16 of the 51 rules require a white background, and "white" means the pixels, not the intent. In my own test images the "white" sweep of a photo-style shot measured 246–252 on all three channels — visibly white on screen, and not white to a checker that wants 255. If your pipeline only fills the letterbox padding with white, you will fail these slots even though the image "looks white".

Where the data lives

The full set — 18 platforms, 51 rules, a maxKB (or explicit null) and a sourceUrl on every rule:

25 of the 51 rules trace to the platform's own documentation, 26 only to third-party write-ups — each rule says which. If a ceiling has changed, send me the URL and I will fix it.

I also build the tool that produced the measurements in this post: it resizes one photo into every marketplace size in the browser, nothing uploaded (tugui.top). Five files per batch free, $4/month or $12/year. The dataset is useful without it.

Top comments (0)