What you'll learn
Quick Answer
A QR code stores data as a grid of black-and-white modules using one of several fixed encoding modes, then deliberately adds redundant Reed-Solomon error-correction codewords on top of the raw data. That built-in redundancy is exactly why a QR code with a logo stamped in the middle, or a torn corner, can still scan — you choose how much damage tolerance to buy (up to roughly 30%), and the decoder reconstructs whatever got destroyed.
What's Actually Encoded in the Grid
Each black or white square in a QR code is called a module. Before any of that data goes in, the encoder picks the most efficient of several fixed encoding modes for your input: numeric mode packs three digits into just 10 bits when the data is digits-only, alphanumeric mode handles a restricted character set more efficiently than raw bytes, and byte mode falls back to one full byte per character for anything else (like a URL with lowercase letters and punctuation).
Not everything in the grid is data. Three large squares sit in three of the four corners — the finder patterns — with a distinctive fixed proportion that lets a scanner instantly locate the code and work out its rotation from almost any angle. Timing patterns are alternating black-and-white strips connecting the finder patterns, letting the scanner count modules accurately even across a slightly warped or angled photo. A blank quiet zone border has to surround the whole code so a scanner can tell where it starts. And a small block of format information tells the decoder which error-correction level and which mask pattern (covered below) were used, so it knows how to read everything else correctly.
Versions: Why Some QR Codes Are Bigger Than Others
The QR spec defines 40 sizes, called versions. Version 1 is a 21x21 module grid; each version up adds 4 modules per side, all the way to version 40 at 177x177. More input data — or a higher error-correction level, since that eats into the same fixed codeword budget — forces a bigger version.
Rather than take that on faith, I generated real QR codes with Python's qrcode library (which implements the ISO/IEC 18004 spec) and read back the actual version chosen at each error-correction level for identical input:
import qrcode
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, ERROR_CORRECT_H
levels = {"L": ERROR_CORRECT_L, "M": ERROR_CORRECT_M, "Q": ERROR_CORRECT_Q, "H": ERROR_CORRECT_H}
data = "https://priodemy.com"
for name, level in levels.items():
qr = qrcode.QRCode(error_correction=level)
qr.add_data(data)
qr.make(fit=True)
print(f"EC={name}: version={qr.version}, size={qr.modules_count}x{qr.modules_count}")Real output:
EC=L: version=2, size=25x25
EC=M: version=2, size=25x25
EC=Q: version=2, size=25x25
EC=H: version=3, size=29x29The exact same 20-character string needed a version 2 grid (25x25 modules) at error-correction levels L, M, and Q, but jumped to version 3 (29x29) at level H. Nothing else about the input changed — only how much redundancy was requested. Feeding a much longer string through the same test showed the same relationship holding at a bigger scale:
Data: 'https://priodemy.com/blog/how-qr-codes-work-a-fairly-long-slug-example-string-for-testing'
EC=L: version=5, size=37x37
EC=M: version=6, size=41x41
EC=Q: version=8, size=49x49
EC=H: version=9, size=53x53
Reed-Solomon: How the Code Survives Damage
QR codes use Reed-Solomon error-correcting codes — the same general family used to recover data from scratched CDs and DVDs, and in satellite transmission. After the raw data is encoded into codewords, extra error-correction codewords are computed and appended. Reed-Solomon works over these codeword blocks and can both detect and correct errors, up to a bound that depends directly on how many correction codewords were spent — which is the whole reason “error-correction level” is really just a choice of how much redundancy to buy.
Checked against the format's own documentation (from DENSO WAVE, the QR code's original creator, referencing ISO/IEC 18004) rather than recited from memory, the four levels tolerate approximately:
- L (Low): ~7% of the code can be damaged and still decode
- M (Medium): ~15%
- Q (Quartile): ~25%
- H (High): ~30%
The version jump measured above is the literal, physical cost of that redundancy: choosing H for the identical “https://priodemy.com” string forced the grid from 25x25 up to 29x29 — more modules had to be spent on error-correction codewords, so the encoder needed a bigger grid to still fit the actual data. This is also exactly why brands can stamp a logo in the middle of a QR code and have it still scan: the logo physically destroys every module underneath it, but as long as the destroyed area stays within the percentage the chosen error-correction level can absorb, Reed-Solomon reconstructs those codewords entirely from the ones that survived.
Masking: Fixing Patterns That Confuse Scanners
Once data and error-correction codewords are placed into the grid, the raw bit pattern can accidentally form shapes that look like the finder patterns, or long runs of the same color that are genuinely hard to scan reliably at an angle. To fix this, the QR spec defines 8 fixed XOR mask patterns that can be applied over the data region — purely a visual transformation, it doesn't touch the underlying encoding. The encoder tries all 8, scores each one with a defined penalty rule (fewest large same-color blocks, fewest patterns that resemble a finder pattern, most balanced black-to-white ratio), and keeps whichever mask scores best. The chosen mask's ID gets written into the format information block, so a scanner can XOR it right back off before reading the real data underneath.
How a Phone Camera Actually Decodes One
The full read pipeline, in order: the camera captures the image; scanning software searches for the three finder patterns, whose distinctive proportions are deliberately unlike anything that occurs naturally in photos; their positions let the software compute the code's rotation and correct for perspective skew; the timing pattern lets it map out the module grid precisely even on an imperfect photo; it reads the format information to learn which mask and error-correction level were used; it XORs the mask back off; it extracts the raw codewords and runs Reed-Solomon decoding, which verifies integrity and repairs any damaged codewords up to the chosen level's limit; and only then does it decode the corrected bitstream back into the original mode-specific data — a URL, plain text, or whatever was encoded.
What I Actually Generated and Checked
To keep every number in this article honest: the version and module-count figures above came from actually running Python's qrcode library against real strings at all four error-correction levels, not from memory or a table I half-remembered. The error-correction percentages (7% / 15% / 25% / 30%) were cross-checked via web search against material tracing back to DENSO WAVE, the format's original creator, rather than asserted outright. The one thing not independently re-derived from scratch here is the Reed-Solomon math itself — that's a well-established, decades-old coding-theory result the QR spec builds on rather than something a blog post needs to re-prove.
