A manual compression press, the machine that squeezes harder and harder for progressively less material gain

Every compression press has a point past which more force buys almost nothing. zlib has one too, and on real gallery PNGs it sits at level 3.

PNG Level 9 Costs Six Times The Time For Three Percent Of The Bytes

Four real 1024 by 1536 gallery files, re-encoded at every zlib level with the clock running. The default is not the problem. The people cranking it to 9 are.

Published August 27, 2026 · RealAIGirls · About a 6 minute read

Share on X Share on Facebook Share on Reddit

Somewhere in every image pipeline there is a line that says compress_level=9, and nobody remembers putting it there. It looks free. It is lossless, after all, so the only thing it can possibly cost is time, and time is cheap until you are re-encoding a gallery.

So four real gallery PNGs went through every zlib level this morning with a timer attached. All four are 1024 by 1536, all four are photographic AI portraits, and all four are the kind of file this site actually serves.

The Files

Nothing synthetic, nothing cropped to make a point. These are four PNGs pulled straight off the gallery, opened, converted to RGB and re-encoded. Every output was byte compared back to the source pixels, so every number below is lossless.

FileDimensionsModeOn disk
elise.png1024 x 1536RGB2,712,717
ashleybank2.png1024 x 1536RGB2,815,970
bestever.png1024 x 1536RGB2,101,903
sienna5.png1024 x 1536RGB2,599,726
Total10,230,316

One thing worth noting before the sweep, because it is a common assumption that turned out to be wrong here: not one of these four carries an alpha channel. They are already RGB. Stripping alpha is the first advice everyone gives about PNG size and on this gallery it saves exactly zero bytes, because there is nothing to strip.

The Sweep

Six encodes per file. Levels 0, 1, 3, 6 and 9, plus Pillow's optimize=True, which forces level 9 and additionally searches the filter space. Totals across all four files, with encode time measured on the same machine back to back.

SettingTotal bytesShare of originalEncode time, per image
compress_level=018,887,020184.6%172 ms
compress_level=115,238,725149.0%317 ms
compress_level=310,345,066101.1%349 ms
compress_level=6 (default)10,278,313100.5%513 ms
compress_level=99,984,79097.6%2,299 ms
optimize=True9,783,77895.6%2,515 ms
Level 3 to level 9 is 6.6 times the encode time for 3.5 percent of the bytes. That is the entire finding and everything below is detail.

Level 0 Is A Nice Sanity Check

At level 0 all four files came out at exactly 4,721,755 bytes. Identical, to the byte, on four different images. That is not a bug, it is arithmetic: 1024 times 1536 times 3 channels is 4,718,592 bytes of raw pixel data, plus one filter byte per row for 1,536 more, plus the header and chunk structure. Level 0 stores rather than compresses, so the image content is irrelevant and only the dimensions matter.

It is also 84.6 percent larger than the original files, which is a useful reminder of how much work zlib is doing before anyone starts tuning it.

Where The Curve Actually Bends

The interesting jump is not at the top of the range. It is between 1 and 3.

StepBytes savedExtra time per imageBytes per extra millisecond
0 to 13,648,295145 ms6,290
1 to 34,893,65932 ms38,232
3 to 666,753164 ms102
6 to 9293,5231,786 ms41
9 to optimize201,012216 ms233

Level 1 to level 3 returns roughly 38,000 bytes for every extra millisecond of encode time. Level 6 to level 9 returns 41. That is a factor of nine hundred, and it is the reason a default of 6 is a perfectly sensible default and a hardcoded 9 usually is not.

The one genuine surprise sits in the last row. Jumping from plain level 9 to optimize=True costs only another 216 milliseconds, because the file is already being deflated at maximum, and it returns 201,012 bytes. Per millisecond that is more than five times better than the 6 to 9 step it sits on top of. If you are already paying for level 9, you are leaving bytes on the table by not passing optimize=True.

What This Means For A Gallery

Three practical readings, and none of them require rewriting a pipeline.

If encoding happens once and serving happens forever, use optimize=True and stop thinking about it. 2.5 seconds per image is nothing against a file that will be requested for years, and 95.6 percent of the original beats 100.5 percent every time.

If encoding is in a request path, use level 3. It lands within 0.6 percent of the default at two thirds of the time, and within 3.5 percent of the best result at one seventh of it.

Never leave level 0 or 1 in a pipeline by accident. Both produce files larger than the source. Level 1 in particular looks like a reasonable low setting and ships 49 percent more bytes than the original for more time than level 3 costs.

Reproducing This

Open each file with Pillow, convert to RGB, then save to a BytesIO at each level with time.perf_counter() around the call and read .tell() for the byte count. Writing to memory rather than disk keeps filesystem caching out of the timings. Run the levels in the same order for every file and do the whole sweep in one process so the interpreter is warm throughout.

Two caveats belong on the record. Four images is a small sample and all four are photographic portraits at one resolution, so a gallery of flat colour or line art would compress very differently and the curve would bend somewhere else. And these timings are single threaded on one machine, so the absolute milliseconds will not match yours. The ratios are what transfer.