Everyone repeats the same fact about QR codes: you can destroy about 30% of one and it still scans. It is the reason people put logos in the middle of them, and it is the reason a scuffed parking sign still works.
I wanted to see it happen, so I generated codes at all four error-correction levels, damaged them in controlled ways, and fed each damaged image to a real decoder two hundred times per data point.
The folklore is true in one specific sense and badly misleading in every other. Where you damage a QR code matters far more than how much.
A sticker over 15% of it: fine
First, the kind of damage people imagine: a solid patch, like a label stuck on a box or a ring left by a coffee cup. I covered a square of the data area and tried to decode it, moving the patch to a random position each time.
| EC level | 5% covered | 10% | 15% | 20% | 25% |
|---|---|---|---|---|---|
| L | 47% | 2% | 0% | 0% | 0% |
| M | 97% | 43% | 0% | 0% | 0% |
| Q | 100% | 99% | 35% | 0% | 0% |
| H | 100% | 100% | 99% | 29% | 0% |
Those are the share of two hundred attempts that decoded correctly. At level H you can cover 15% of the symbol with a sticker and it still scans essentially every time.
That is the picture the folklore is describing, and it holds up.
Scattered dirt over 3% of it: dead
Now the same total amount of damage, spread out. Instead of one patch, I flipped individual modules at random across the whole symbol — the pattern you would get from dust, print noise, or a bad camera.
| EC level | 1% speckled | 2% | 3% | 5% |
|---|---|---|---|---|
| L | 65% | 13% | 1% | 0% |
| M | 74% | 46% | 7% | 0% |
| Q | 61% | 38% | 15% | 0% |
| H | 44% | 29% | 10% | 0% |
Three per cent of scattered damage is worse than fifteen per cent in a patch. Every level is finished by five per cent.
The reason is that the error correction does not work on pixels, it works on codewords of eight modules each. A solid patch ruins a small number of codewords completely. The same number of scattered modules ruins up to eight times as many codewords, because each stray speck lands in a different one. The decoder is not counting damaged area, it is counting damaged codewords, and scattering is the most efficient way to spend your damage budget.
The part that genuinely surprised me
Look at that speckle table again, at the 1% column. Level H — the strongest error correction available — did worse than level M. 44% against 74%.
That is not an error. Level H needs more room for its redundancy, so for the same payload it produced a bigger symbol: 33×33 modules against M's 25×25. One per cent of a bigger code is more modules, and since scattered modules are what destroy codewords, the stronger code ate more damage at the same percentage.
Stated as a percentage, more error correction made it more fragile. Stated in absolute terms, H still corrects more. Both are true, and the percentage version is the one people quote.
The 0.7% that kills it instantly
Everything above damaged the data area. Now the structure.
A QR code is not uniform. Roughly a quarter of the modules in the symbol I tested — 260 of 1,089 — are fixed furniture: the three big squares in the corners, the dotted lines running between them, the strips that describe the format. I found them empirically, by generating three hundred codes with different payloads at the same size and marking every module that never changed.
None of that furniture is protected by error correction. It is what the decoder uses to find the code in the first place.
| what I erased | share of modules | result |
|---|---|---|
| one finder pattern (a corner square) | 4.5% | dead |
| the format strip beside a finder | 0.8% | dead |
| half of one timing row | 0.7% | dead |
Seven tenths of one per cent. A single thin line of modules, and the code is unreadable — while the same code shrugs off a sticker twenty times that size.
That timing row is the dotted line running between two corner squares. It is how the decoder works out the grid spacing, so without it there is nothing to measure the rest of the symbol against. No amount of redundancy in the data helps, because the data is never reached.
What I got wrong
I ran the whole first round with an error-correction level I was not actually using.
My level L and level M results came out identical — not close, identical to the decimal. Two hundred randomised trials producing exactly 50.5% for both is not a coincidence, it is a message. Comparing the two matrices directly: zero differing modules.
The library I used boosts the error-correction level automatically when a stronger one fits the same symbol size, which is sensible behaviour and is on by default. So asking for level L had quietly given me level M, and my weakest data point was never measured. Every number above comes from a rerun with boost_error=False, and with that off, L and M separate exactly as they should: 47% against 97% at the same damage.
If you are putting QR codes on things
Use level H when the code will live in the world rather than on a screen — it is the difference between surviving a 15% sticker and dying at 5%.
Do not scatter your logo. A single solid shape in the middle is close to the best case; the same area sprinkled around the symbol is close to the worst.
Protect the corners and the two dotted lines between them above everything else. That is where a fraction of a per cent of damage is fatal, and it is exactly the part people crop, round off, or print over.
There is also a practical debugging order that falls out of all this. When a code in the wild stops scanning, the instinct is to assume it has got too dirty or too worn, and to reprint it bigger. The numbers say that is usually the wrong first guess, because dirt has to be remarkably evenly distributed to beat the error correction in a level-H symbol, whereas a corner that has been trimmed by a label machine, or a sign whose bottom edge has been clipped by a mounting bracket, will take out a finder pattern or a timing row and kill the thing outright while looking almost undamaged to a person.
So check the edges and the corners before you check the surface. And if you are the one placing the code, give it a quiet margin on all four sides and keep anything decorative away from those three squares, because that is the region where less than one per cent of damage is the difference between a code that works for years and one that never worked at all.
Code, images and the raw numbers are at github.com/DimitrovK/qr-damage if you want to rerun it.



Top comments (0)