Learn how QR codes work internally, the different data types they support, error correction levels, size optimization, and how to generate them programmatically in various languages.
The restaurant menu that changed my mind about QR codes
I used to think QR codes were a gimmick. Then COVID happened, and suddenly we were building QR menu systems for restaurant clients at Šikulovi s.r.o.. One code replaced printed menus, enabled contactless ordering, and tracked which dishes were getting attention. I went from skeptic to enthusiast in about two weeks.
QR codes are just two-dimensional barcodes - black and white squares encoding data. But the engineering behind them is clever. They can be read from any angle, work even when partially damaged, and pack a surprising amount of data into a small space. Once you understand how they work, you can make them work better.
Anatomy of a QR code (the parts that matter)
You don't need to understand every module in a QR code, but knowing the structure helps when codes won't scan or you're trying to optimize size:
- Finder patterns: Those three big squares in the corners - they tell scanners "this is a QR code, this way up"
- Alignment patterns: Smaller squares for distortion correction (larger codes have more)
- Timing patterns: The alternating black/white lines that define the grid
- Format info: Error correction level and mask pattern
- Data area: Everything else - your actual content plus error correction bytes
- Quiet zone: The white border. Without it, scanners get confused. Minimum 4 modules.
How much can you actually fit?
QR codes come in 40 versions (sizes). Version 1 is 21x21 modules, version 40 is 177x177. Bigger = more data, but also harder to scan. Here's the practical reality:
- Version 1 (21x21): ~25 characters - enough for a short URL
- Version 10 (57x57): ~174 characters - most URLs fit here
- Version 20 (97x97): ~858 characters - vCards, longer content
- Version 40 (177x177): ~4,296 characters - theoretical max, rarely practical
- Numeric-only: Up to 7,089 digits (more efficient encoding)
- Binary data: Up to 2,953 bytes
- My advice: Keep it under 500 characters. Libraries auto-select the smallest version that works.
Error correction: the magic that makes QR codes resilient
This is the coolest part of QR codes. They use Reed-Solomon error correction, so they work even when damaged. Higher levels mean bigger codes but better reliability. Here's how I choose:
- L (7%): Smallest codes. Use for screens, short URLs, pristine conditions
- M (15%): Default for most cases. Good balance of size and resilience
- Q (25%): For printed stuff that might get worn or folded
- H (30%): Outdoor signage, harsh environments, or when you want to add a logo
- My rule: L for digital, M for clean prints, H for anything else
- Fun fact: You can cover up to 30% of a QR code with a logo if you use H level
The encoding trick that saves space
This blew my mind when I learned it. QR codes use different encoding modes, and the mode affects size dramatically:
- Numeric mode: Just digits 0-9. Super efficient - 3.3 bits per character
- Alphanumeric mode: 0-9, A-Z (uppercase only!), space, and some symbols. 5.5 bits
- Byte mode: Anything else, including lowercase. 8 bits per byte
- The trick: "HTTP://EXAMPLE.COM" encodes smaller than "https://example.com"
- Why? Uppercase URL uses alphanumeric mode. Lowercase forces byte mode.
- Yes, URLs are case-insensitive for the domain part. Use it.
- I started using uppercase URLs for printed codes at Šikulovi s.r.o. - noticeably smaller codes
URL optimization (my actual workflow)
URLs are 90% of what I generate QR codes for. Here's what I've learned to do:
- URL shorteners: bit.ly turns 200 characters into 20. Huge difference.
- HTTPS is worth the extra characters - don't sacrifice security
- Strip unnecessary query params - every character counts
- Uppercase the domain: HTTPS://EXAMPLE.COM - smaller code, same destination
- App deep links: myapp://action bypasses the browser entirely
- Test shortened URLs BEFORE printing - a dead short link is a dead QR code
- UTM params for tracking, but keep them minimal
WiFi QR codes: the guest network savior
We put these in every client's office and reception area at Šikulovi s.r.o.. Visitors scan, connect, done. No more spelling out passwords. The format is specific:
- Format: WIFI:T:WPA;S:NetworkName;P:Password;;
- T = auth type: WPA, WPA2, WEP, or nopass for open networks
- S = SSID (network name)
- P = password (omit for open networks)
- H:true if the network is hidden (optional)
- Escape special characters with backslash: \; \: \\ etc.
- Example: WIFI:T:WPA;S:GuestWifi;P:welcome2024;;
- Works on Android natively, iOS 11+. Basically everyone.
vCard QR codes for business cards
vCards encode contact info that saves directly to the phone. Great for business cards - scan instead of type. But keep it minimal or your QR code becomes a dense mess:
- BEGIN:VCARD and END:VCARD wrap everything
- VERSION:3.0 - stick with 3.0 for compatibility
- N: Last;First;Middle;Prefix;Suffix (structured name)
- FN: Full display name
- TEL;TYPE=CELL: Phone number
- EMAIL: Email address
- ORG: Company name
- URL: Website
- My advice: Name, phone, email, maybe company. Skip the rest. Smaller code = easier scan.
Other useful data types
Beyond URLs and WiFi, QR codes can trigger all sorts of actions. I've used most of these at some point:
- Email: mailto:[email protected]?subject=Hello&body=Message - opens email compose
- Phone call: tel:+1234567890 - one tap to dial
- SMS: sms:+1234567890?body=Hello - pre-filled text message
- Location: geo:40.7128,-74.0060 - opens maps at coordinates
- Calendar event: vCalendar format - adds event to calendar
- Bitcoin: bitcoin:address?amount=0.001 - crypto payments
- Plain text: Any content without a scheme - just displays text
- App Store: Platform-specific links to apps
My size optimization checklist
Every time I generate a QR code for print at Šikulovi s.r.o., I run through this mental checklist:
- Uppercase the URL domain: HTTP://EXAMPLE.COM uses alphanumeric mode
- Short link: bit.ly or your own redirect service
- Strip www and trailing slashes
- Match error correction to use: L for screens, M for print, H for outdoor
- Avoid special characters when possible
- For changing content, use a redirect URL you control
- Numeric-only? Lucky you - most efficient encoding
JavaScript: what I use
The 'qrcode' npm package handles 95% of my use cases. Simple, works everywhere, good options:
- qrcode (npm): My go-to. Works in Node.js and browser.
- Basic: QRCode.toDataURL("content", options)
- Options: errorCorrectionLevel, width, margin, color (foreground/background)
- Output: Base64 data URL, canvas, or file
- qr-code-styling: When you need logos, gradients, fancy stuff
- Server-side: Generate PNG/SVG for downloads
- Canvas: Real-time preview as user types
Python: equally straightforward
Python's qrcode library is similarly simple. I use it for batch generation and server-side stuff:
- Install: pip install qrcode[pil]
- One-liner: qrcode.make("content").save("qr.png")
- More control: QRCode(version=1, error_correction=..., box_size=10)
- Error correction: qrcode.constants.ERROR_CORRECT_L/M/Q/H
- SVG output: qrcode.image.svg.SvgImage as image_factory
- Pillow integration: Add logos, modify colors
- Flask/Django: Generate on-the-fly and stream to response
API-based generation (when you can't install libraries)
Sometimes you just need a QR code URL in an img tag. APIs work, but know the tradeoffs:
- Google Charts (deprecated but alive): chart.googleapis.com/chart?cht=qr&chs=200x200&chl=content
- QRServer: api.qrserver.com/v1/create-qr-code/?size=200x200&data=content
- Parameters usually: size, data, format (png/svg), ecc (error correction)
- Rate limits: Free APIs have them. Hit them during a launch and you're stuck.
- Privacy: Your data goes in the URL. It gets logged. Don't put sensitive stuff.
- High volume? Self-host or use libraries.
- Cache aggressively to reduce API calls
Quick reference for other languages
Every language has a QR library. Here's what I've used or seen recommended:
- Go: github.com/skip2/go-qrcode - simple and effective
- PHP: endroid/qr-code or bacon/bacon-qr-code
- Java: ZXing (com.google.zxing) - the classic
- C#: QRCoder NuGet package
- Ruby: rqrcode gem
- Rust: qrcode crate
- Swift: CoreImage CIFilter - built into iOS, no dependencies
Printing: where most QR codes fail
I've seen beautifully designed QR codes that nobody could scan. Print is unforgiving. Here's what actually matters:
- Size: 2cm minimum for close scanning. For distance, divide scanning distance by 10.
- Resolution: 300 DPI for print, 72 DPI is fine for screens
- Contrast: Dark on light. Always. Inverted codes are risky.
- Quiet zone: That white border? It's not optional. 4 modules minimum.
- Material: Glossy/reflective surfaces cause glare. Matte is safer.
- Test before print run: Multiple phones, multiple apps, actual print conditions
- The test that matters: Can your mom scan it?
Customization without breaking scannability
Clients always want branded QR codes. That's fine, but there are rules:
- Contrast is king: Dark modules need to be way darker than light ones
- Dark on light: Inverted (light on dark) works sometimes. Test heavily.
- No gradients on modules: Each square needs to be solid
- Don't touch the finder patterns: Those three corner squares are sacred
- Logos: H error correction, center placement, under 30% of code area
- Custom codes fail more: Test on way more devices than you think
- Accessibility: Some color combinations are invisible to color-blind users
Static vs dynamic: choose wisely
This distinction matters more than most people realize. We've had clients print 10,000 brochures with static codes pointing to dead URLs. Don't be them.
- Static: Data encoded directly. Works forever. Works offline. No tracking.
- Dynamic: Points to a redirect you control. Can change destination. Trackable.
- Static for: Business cards, permanent labels, anything that needs to work offline
- Dynamic for: Marketing campaigns, product packaging, anything that might change
- Dynamic dependency: Your redirect service must stay alive
- My rule at Šikulovi s.r.o.: Printed = dynamic (unless the URL is set in stone)
- Added benefit: Dynamic lets you A/B test landing pages without reprinting
Security: QR codes as attack vectors
QR codes have a dark side. Anyone can generate one pointing anywhere. I've seen phishing attacks using QR codes stuck over legitimate ones on parking meters. Be aware:
- Anyone can create a malicious QR code - users should preview URLs before visiting
- Physical replacement attacks: Stickers over legitimate codes. Happens more than you'd think.
- Never encode passwords or tokens directly - QR codes are not secure storage
- Short URLs hide destinations - legitimate use, but also phishing enabler
- WiFi codes expose credentials - use guest networks for public codes
- If scanning programmatically: Validate URLs, whitelist domains, sanitize input
- High security? Look into signed QR codes (more complexity, more safety)
My testing checklist
Before any QR code goes to print at Šikulovi s.r.o., we run through this:
- Multiple phones: iPhone, Android, at least two of each
- Multiple apps: Native camera, Google Lens, a dedicated scanner app
- Real conditions: Office lighting, outdoor light, dim restaurant
- Distance: Can you scan from where users will actually be?
- Action verification: URL opens? WiFi connects? vCard saves?
- Print test: Actual printed output, not just screen. Paper matters.
- Error correction test: Cover part of the code. Still work?
- Dynamic codes: Check the redirect monthly. URLs die.
What I learned from those restaurant menus
Those restaurant QR menus we built at Šikulovi s.r.o. during COVID? They taught me that QR codes are more engineering than design. The pretty ones that marketing wanted often failed. The ugly black-and-white ones that I wanted always worked.
The formula is simple: Keep data minimal. Use appropriate error correction. Test obsessively. Dynamic for print, static for permanent. And never, ever skip that white border. Everything else is optimization.
FAQ
What is the maximum data capacity of a QR code?
QR codes can store up to 7,089 numeric characters, 4,296 alphanumeric characters, or 2,953 bytes of binary data. However, more data means larger codes that are harder to scan. For practical use, keep content under 500 characters.
How do I make a QR code smaller?
Use URL shorteners, remove unnecessary query parameters, use uppercase URLs (enables more efficient alphanumeric encoding), choose lower error correction (L or M for screens), and avoid special characters that force byte mode encoding.
Can I add a logo to a QR code?
Yes, but use high error correction (H level) and keep the logo under 30% of the QR code area. Place the logo in the center, which has the least critical data. Always test extensively—logos can prevent scanning if too large.
Why does my QR code not scan?
Common issues: insufficient contrast between colors, code is too small, quiet zone (white border) is missing or too small, code is damaged or dirty, or the content is too long resulting in a dense pattern. Try increasing size and using black on white.
Do QR codes expire?
Static QR codes (where data is encoded directly) never expire. Dynamic QR codes that redirect through a service can expire if the service stops working. For permanent use, prefer static codes or use redirect services you control.
What error correction level should I use?
Use L (7%) for digital displays with short content. Use M (15%) for most printed materials. Use Q (25%) for materials that may get worn. Use H (30%) for outdoor signage, when adding logos, or harsh environments.
Can QR codes be read from any angle?
Yes, QR codes can be read from any rotation angle. The three finder patterns (large squares in corners) allow scanners to determine orientation automatically. However, avoid reflective surfaces and extreme angles that may cause glare.
Are QR codes secure?
QR codes themselves are not secure—anyone can read their contents. Never encode sensitive data like passwords or API keys directly. For sensitive applications, encode encrypted data or use QR codes only for non-sensitive identifiers.