QR codes
Generating QR codes on the server: formats, error correction, and what breaks at scale
Generate QR codes in Node and Python, choose PNG vs SVG deliberately, understand error-correction levels and the quiet zone, and avoid the logo-overlay and caching mistakes that make codes fail to scan.
The RoutexAPI TeamUpdated August 11, 20266 min read
A QR code looks like the most solved problem on the internet — one library call, a square of pixels, done. And for a single code it is. The trouble starts when the code has to survive being printed small, overlaid with a logo, generated a hundred thousand times a day, or embedded in an email that some clients refuse to render. Every one of those is a decision, not a default, and getting them wrong produces the one failure mode that matters: a code that does not scan. This guide covers generation in Node and Python and the four decisions that actually determine whether your codes work in the wild.
Generating a code
Node
The qrcode package is pure JavaScript, renders to PNG buffers, SVG strings, or data URIs, and has no native dependencies.
import QRCode from "qrcode";
// PNG buffer — for saving to disk or object storage
const png = await QRCode.toBuffer("https://example.com/x", {
errorCorrectionLevel: "M",
margin: 4, // quiet zone in modules — see below
width: 512, // output pixel size
});
// SVG string — for the web, scales without blurring
const svg = await QRCode.toString("https://example.com/x", {
type: "svg",
errorCorrectionLevel: "M",
margin: 4,
});
Python
qrcode (Pillow-backed) is the common choice; segno is a pure-Python alternative with no imaging dependency and first-class SVG.
import segno
qr = segno.make("https://example.com/x", error="m")
qr.save("out.png", scale=8, border=4) # border = quiet zone in modules
qr.save("out.svg", scale=8, border=4) # crisp at any size
That is the entire "generate a QR code" story. Now the decisions.
Decision 1: error correction is a size-vs-durability trade
QR codes carry Reed–Solomon error correction, and you choose how much. More correction means the code still scans when part of it is dirty, blurred, or covered — but it also means more data modules, so either a denser (harder to scan small) code or a physically larger one for the same content.
| Level | Recovers up to | Use when |
|---|---|---|
| L (low) | ~7% | Clean digital display, short data, you want the least-dense code |
| M (medium) | ~15% | The sane default for most uses |
| Q (quartile) | ~25% | Printed, or a small logo in the centre |
| H (high) | ~30% | Harsh print environments, or a larger logo overlay |
The instinct is "always use H, more is better." It is not — H inflates module count, and a denser code printed at the same physical size has smaller modules that cheap cameras struggle to resolve. Pick the lowest level that survives your environment. M for screens, Q or H only when you are printing or covering part of the code.
Decision 2: the quiet zone is not optional
A QR code needs a margin of empty space around it — the quiet zone, four modules wide by spec — for scanners to locate it. This is the single most common reason a technically-correct code fails to scan: someone crops it tight or lays it directly against dark content, and the scanner cannot find the edges. In qrcode it is margin, in segno and Python qrcode it is border. Keep it at 4. Do not "reclaim the whitespace" to make the code look tidier; you are removing the part that makes it work.
Decision 3: PNG or SVG — choose on purpose
- SVG is resolution-independent: one file scans crisply on a phone screen and on a billboard, and it is tiny for the web. Prefer it for any on-screen or scalable use. The catch: many email clients strip or refuse to render inline SVG, so SVG is a poor choice for an emailed code.
- PNG (raster) is universally supported, including in email, and is what you want when you need a fixed-size image or a print asset at a known DPI. The catch: it is resolution-bound — generate it at the size you will actually display, because upscaling a small PNG blurs the module edges and breaks scanning.
A rule that holds up: SVG for the web, PNG for email and print. If you are putting a code in a transactional email, generate a PNG at a generous fixed size and attach or host it — do not inline an SVG and hope.
Decision 4: logos and colours, where good intentions break codes
Two "make it on-brand" requests reliably produce codes that do not scan:
- Centre logo. You can overlay a logo because error correction lets the scanner reconstruct the covered modules — but only up to that level's budget. A logo covering more than ~15–20% of the code, or placed over a finder pattern (the three corner squares), pushes past what
Q/Hcan recover. Keep the logo small, keep it centred, keep it clear of the corners, and raise the correction level toQorHto buy back the covered area. Then actually test it with a phone — not just your phone, a cheap one. - Low contrast / inverted colours. Scanners expect dark modules on a light background. A dark-on-dark "sleek" code, or pale grey on white, drops below the contrast threshold and fails. Brand colours are fine as long as the dark/light relationship and contrast hold; a light module on a dark background (inverted) breaks many scanners outright.
The meta-point: every one of these is a change you can see is prettier and cannot see is broken, because it still scans on your clean phone in good light. Validate with a real decode step, not eyeballs.
What breaks at scale
One code is free. A hundred thousand a day is a system, and three things surface:
- CPU and blocking. QR generation is Reed–Solomon encoding plus rasterising — non-trivial CPU, especially for large PNGs. Generating them inline on your request threads will start adding latency under load. Move bulk generation to a worker or a queue, and cap the output size.
- Cache, do not regenerate. A QR code is a pure function of its content and options. The same URL at the same size and correction level is byte-for-byte identical every time. Key a cache on
hash(content + options)and you turn repeated generation into a lookup. Most "our QR endpoint is slow" problems are really "we regenerate the same fifty codes on every page load." - Dynamic vs static. A code that encodes a long URL directly is static — change the destination and every printed code is dead. If the destination might change, encode a short redirect URL you control (
/r/abc123) and change where it points server-side. That also gives you scan analytics for free. This is a URL-shortener decision wearing a QR hat, and it is far cheaper to make before you print ten thousand flyers than after.
Self-hosting
For generation, self-hosting is the sensible default and it is light: qrcode or segno is a pure dependency, there is no binary and no licensing cliff, and the whole thing is a function. Add a content-plus-options cache, push bulk jobs to a worker, and you have a robust in-house QR service. The parts people underestimate are not the encoding — they are the operational shell around it: the cache, the worker, size limits, and a real decode-based test in CI so a well-meaning logo tweak cannot ship a code that does not scan.
If you also need the dynamic layer — trackable short URLs behind the codes, scan analytics, per-campaign rotation — that is a redirect service with its own storage and dashboard, not a bigger call to the QR library. That is the natural point to decide between building that layer and pointing your codes at a managed endpoint that already runs the generation, the caching, and the redirect analytics behind one key.
Try it on RoutexAPI
Skip the setup
Would rather not run and maintain this yourself? These APIs on RoutexAPI do the same job behind one key.
