NoWaterProgramming

QR Codes 101: How They Actually Work

A walkthrough of QR internals against the spec: the module grid and its reserved regions, encoding modes, Reed-Solomon over GF(256), interleaved blocks, mask selection, and what the standard deliberately does not protect.

9 min read
Share:

A QR code is not an image of data. It is a fixed-size grid of black and white modules where every position carries a defined meaning: some cells are structural, some are metadata, and the rest hold an interleaved payload protected by polynomial error correction. Once you stop treating the pattern as decoration and start reading it as a binary matrix with a strict layout spec, the whole thing becomes legible. This walkthrough goes down to that level.

Everything here comes from ISO/IEC 18004, the QR code symbology specification, currently in its 2024 edition. The standard is paywalled; the structure it defines is not secret, and this is a description of it, not a substitute for it.

The Module Grid and Its Reserved Regions

A QR symbol comes in 40 versions. Version 1 is 21x21 modules; each step up adds 4 modules per side, so version 40 is 177x177. Before any user data is placed, the grid reserves several function patterns.

The map below is a version 1 symbol with every module coloured by the role the spec gives it, rather than by the bits of any particular payload. Version 1 is the clearest one to draw because it has no alignment patterns (those start at version 2) and no version information blocks (those start at version 7).

Module map of a version 1 QR symbol, 21 by 21, with the three finder patterns, their separators, the timing patterns on row 6 and column 6, both copies of the format information, the dark module, and the remaining data and error correction region

The three large finder patterns in the corners (a 7x7 nested square) let a scanner locate orientation and the three reference points regardless of rotation. Each is fenced off by a one-module separator so it cannot run into the data region. Alignment patterns, smaller 5x5 squares, appear from version 2 onward to correct for perspective distortion when the code is photographed at an angle. Timing patterns, the alternating runs along row 6 and column 6, give the decoder a clock to count module coordinates.

Two strips next to the finders hold format information: the error correction level (L, M, Q, H) and the mask pattern index. This 15-bit field is itself protected by a BCH code and written twice, because if you cannot read the format bits you cannot read anything else. There is also a single module that is always dark, at row 4 x version + 9, column 8, which carries no information at all and exists purely so that region cannot be all-light. Larger symbols carry separate version information blocks.

Notice how much of a version 1 symbol is not payload. Counting the map above: of 441 modules, 147 are finder patterns, 45 are separators, 30 are format information, 10 are timing and 1 is the dark module. That is 233 structural modules, 52.8 percent of the symbol, before a single bit of your URL is placed.

The 208 that remain are exactly 26 bytes, which is a useful sanity check: 26 total codewords is the documented capacity of a version 1 symbol, split between data and error correction according to the level you pick. Get that number out of your own layout code and you know the reserved regions are in the right places.

Encoding Modes and Bit Packing

The payload starts with a 4-bit mode indicator. The four common modes are numeric (10 bits per 3 digits), alphanumeric (11 bits per 2 characters from a 45-symbol set), byte (8 bits per character, usually UTF-8 or Latin-1), and kanji.

Choosing the tightest mode matters and is the single largest lever on symbol size. Encoding 12345 as numeric costs far fewer bits than as byte, and that difference decides which version you can fit into. The 45-character alphanumeric set is uppercase only, plus digits, space and a handful of punctuation, which is exactly why URLs in QR codes are so often written in uppercase: HTTPS://EXAMPLE.COM/PATH encodes alphanumeric, the lowercase version does not and falls back to byte mode.

After the mode indicator comes a character count whose bit-width depends on version and mode, then the encoded data, a terminator, and padding bytes (the alternating 0xEC and 0x11 pad pattern) filling the capacity of the chosen version and error level.

Reed-Solomon Error Correction

This is the part that lets a scuffed or partially obscured code still resolve. QR uses Reed-Solomon codes over the Galois field GF(256), with arithmetic defined modulo the primitive polynomial x^8 + x^4 + x^3 + x^2 + 1. The data codewords are treated as coefficients of a message polynomial, which is divided by a generator polynomial whose degree equals the number of error correction codewords. The remainder of that division becomes the EC codewords appended to the data.

The naive rule is that a decoder corrects up to t byte errors where 2t equals the number of EC codewords, and handles erasures (errors at known positions) more cheaply. The spec is more careful than that. Clause 8.5.1 gives the capacity as e + 2t <= d - p, where e is erasures, d is the number of error correction codewords and p is a number of misdecode protection codewords held back deliberately. Table 13 spells out why: the correction capability is set below half the error correction codewords on purpose, to reduce the chance that a badly damaged symbol decodes into a valid but different message. Recovering the wrong URL silently is worse than failing to scan.

The four levels correspond to roughly 7, 15, 25 and 30 percent of codewords being error correction. Treat those as the nominal ratio of the codeword split, not as a promise about how much of a torn label survives.

For larger versions the codewords are split into blocks, each with its own Reed-Solomon computation, then interleaved so a localised smudge damages a few codewords across many blocks rather than wiping out one block entirely.

Masking and Why It Exists

Raw module placement can produce large blank areas or sequences that resemble the finder marks, both of which confuse scanners. So the encoder XORs the data region against one of eight predefined mask patterns, each a formula over the row and column index such as (row + col) mod 2 == 0.

The encoder evaluates all eight against four penalty rules: long same-colour runs, 2x2 blocks of one colour, finder-like 1:1:3:1:1 sequences, and overall light-dark imbalance. The mask with the lowest total penalty wins, and its index is written into the format bits so the decoder can reverse the XOR.

This is why two QR codes carrying the same URL can look completely different if they were produced by different encoders: mask choice is scored, not specified, and encoders differ in how faithfully they implement the scoring.

Reading One by Hand

Put it together and decoding is mechanical: locate the finders, read and BCH-correct the format bits, undo the mask, walk the zigzag module placement order up and down the columns, regroup into codewords, de-interleave the blocks, run Reed-Solomon to repair errors, strip the mode and length headers, then decode the payload bytes. Every byte has a place, and every place is in the spec. That is why a standard this old still resolves reliably on a cracked phone screen in bad light.

What the Spec Does Not Give You

Reading the structure carefully also makes clear what is missing from it, and the gaps matter more than the mechanics for anyone putting QR codes in production.

  • There is no authentication and no integrity check on the meaning. Reed-Solomon protects the symbol against damage. It does nothing about a symbol that was correct when printed and has since had a sticker placed over it. A QR code cannot tell a scanner who made it, which is the whole basis of QR phishing.
  • A scanner shows a URL, not a destination. Shortened and redirecting links resolve after the scan, so what the user reviews before tapping is frequently not where they end up.
  • High error correction is not free. Level H spends about 30 percent of the codeword budget on redundancy, so the same payload needs a larger version and, at a fixed printed size, smaller modules. Past a point you have made the code harder to scan in the name of damage tolerance. Logo overlays sit right on this trade-off.
  • The famous logo trick relies on the error correction budget you were going to need anyway. Covering the centre works because the decoder repairs it. Doing it on a dirty or curved surface spends the same budget twice.
  • Micro QR and rMQR are different symbologies, not small QR codes. Same family, different layout rules, and not every reader supports them.
  • Byte mode has no declared character encoding. The spec's default is ISO-8859-1; UTF-8 is signalled with an ECI header that plenty of encoders omit and plenty of decoders ignore. Non-ASCII payloads are where cross-reader inconsistency actually shows up.

Sources

Checked 2026-08-20.

  • ISO/IEC 18004:2024, QR code bar code symbology specification - the normative source for everything above: version sizes and the 4-modules-per-version step, finder, separator, alignment and timing patterns, the format information field and its BCH protection, the dark module position, mode indicators and character count widths, the GF(256) primitive polynomial, the eight mask patterns and four penalty rules, and block interleaving. Clause 8.5.1 and Table 13 specifically give the e + 2t <= d - p capacity rule and the note that correction capability is held below half the error correction codewords to reduce misdecodes.
  • The diagram is generated from the layout rules above by scripts/generate-qr-grid-diagram.mjs in this site's repository, not traced from a rendered symbol.

Related Posts

7 min read
Compress, merge, split, linearize and OCR are five different operations on the same file format. What each one touches in the object graph, why some are fast and some are not, and the three that can quietly corrupt a document.
By NoWaterProgramming Team