QR code tracking: what scan analytics measure, and what they don't
Every provider sells you a dashboard. Almost none of them will tell you how precise the numbers behind it are. Here are ours, field by field.
You can track scans only on a dynamic QR code, because tracking needs a server in the middle to record each visit. A static code sends the phone straight to your link with nothing watching, so there is nothing to count. If scan data matters to you, that decision is made before you ever print.
What follows is not a feature list. It is what a redirect can honestly measure, with our own implementation as the worked example, because "advanced analytics" is how this category avoids the question.
Tracking happens at the redirect, not in the image
A dynamic code encodes a short URL you control. A phone scans it, asks that short URL where to go, and the server answers with an HTTP 302 and the real destination. That single request is the entire measurement surface. Everything a scan report can ever tell you has to be readable from one HTTP request: when it arrived, which address it came from, and whatever the browser volunteered about itself in its User-Agent header. No cookie, no second visit, no script running on the scanner's phone.
That sets a limit worth knowing early. A 302 is cacheable when the response says it is, so if the redirect can be cached, a repeat scan may be answered from a cache and never reach the server at all. It is then invisible, permanently. Our redirect answers with Cache-Control: private, no-store for exactly this reason: a faster redirect that quietly undercounts is not a better redirect.
What each metric means
- Total scans. One row per request that reached the redirect. It is a request count, not a people count, and nothing is filtered out of it.
- Unique devices. The number of distinct address and
User-Agentpairs. It is a proxy and it errs in both directions: a cafe full of phones on one network can collapse toward a single device, and one phone that moves from wifi to cellular can split into two. - Country. Resolved from the request address by Cloudflare, which hands us a two-letter country code. Country is the whole resolution. We do not store a city, because we never receive one.
- Device class. The
User-Agentstring matched into five coarse buckets: mobile, desktop, tablet, bot, unknown. Deliberately crude, because a real scan filed under the wrong label corrupts the report worse than a vague label does. - Time of day. Twenty-four hour buckets, in UTC. Not the scanner's local time and not yours.
- Scans over time. Thirty daily buckets, also UTC, with empty days present as zero so a gap reads as a gap instead of disappearing.
Check a code before you trust anyone's numbers
You can establish whether a QR code is trackable at all in about thirty seconds, with nothing printed and no account anywhere. Run this against your current provider before you compare dashboards.
- Decode the code to read the URL inside it. Our QR code URL checker does it in your browser.
- If that URL is your final destination, the code is static. Stop here: nobody can count its scans, including whoever sold it to you.
- If it is a short URL on the provider's domain, ask the URL what it does:
curl -sI https://tgo.sh/EXAMPLE. A trackable code answers302with aLocationheader pointing at the real destination. - Read the
Cache-Controlline in that same response.no-storemeans every scan reaches the server and gets counted. A longmax-agemeans repeat scans can be served from a cache and go uncounted.
Step four is the one nobody runs, and it is the one that decides whether the totals in a dashboard are counts or estimates.
What scan data cannot tell you
This is the section most competitors leave out, so it is worth being blunt.
- A scan is not a person. The same phone scanning twice counts twice. Unique devices is a floor on your audience, never a headcount.
- Link previews get counted. Paste a short link into Slack, WhatsApp or iMessage and the app fetches it to build the preview card. That is a genuine request to the redirect, so it is a genuine row. We file the ones that identify themselves into the bot bucket, but they still count toward the total, because silently deleting traffic we guessed about would be the worse error.
- A region is a country. "Approximate region" in most marketing copy means a country code. Ours does too. Treat any city-level claim in this category with suspicion, and ask which header it came from.
- The clock is UTC. A dinner rush in California lands in the 01:00 and 02:00 buckets. Convert before you draw a conclusion about human behaviour.
- The trail stops at the redirect. We know a phone asked where to go. Whether it arrived, read the menu, or bought anything happens on your site, where your own analytics live. Tag the destination with UTM parameters if you need to join the two.
Looking for the codes you scanned yourself?
"Scan history" means two different things and they have nothing to do with each other. If you want the history of scans on a code you own, that is everything above. If you are trying to find a QR code you scanned with your own phone last week, that is a phone question rather than a QR code question, and we answered it separately in where to find QR codes you've scanned.
Retention is the number to compare
Tracking comparisons are usually feature checklists, which is convenient for whoever wrote them. The number that decides whether you can answer a question in six months is how long the provider keeps the data, and it is generally printed nowhere near the word "analytics". The table below is what we found when we went looking.
Bitly is the honest comparison here because it is the strongest product in the category, and there are real reasons to choose it: a UTM builder, campaign grouping, integrations with HubSpot and Salesforce, and a bulk API that we do not offer. If you manage hundreds of links across a marketing team, that is worth paying for. If you printed one code on a menu and want to know whether anyone scanned it, the retention row is the whole story.
Every TangoQR code records scans from the moment it exists, on every plan, and keeps them for as long as the code exists. The free plan shows the full breakdowns for a code's first 50 scans and the running total forever, so upgrading later reveals history that was being collected the whole time rather than starting a new clock.
What one scan records, and how precise it is
| Field | Where it comes from | Precision |
|---|---|---|
| Time | The server clock when the redirect is hit | To the second, stored and bucketed in UTC |
| Country | Cloudflare's CF-IPCountry request header | Country only, two letters. No city is received or stored |
| User agent | The browser's own User-Agent header | Self-reported, truncated at 1024 characters |
| Address | The connecting IP address | Stored to deduplicate devices, never shown in a report |
| Total scans | One row per redirect request | An exact request count, including repeats and previews |
| Unique devices | Distinct address and user-agent pairs | A proxy. Shared networks collapse it, roaming splits it |
| Device class | User-Agent matched into five buckets | mobile, desktop, tablet, bot, unknown |
| Time of day | The hour read off the scan timestamp | 24 buckets in UTC, not the scanner's local time |
Source: TangoQR implementation: the scan_events table, RedirectsController#log_scan, and Analytics::CodeReport in this codebase
An example report for one code over 30 days
| Line | Example value | How to read it |
|---|---|---|
| Total scans | 412 | Requests that reached the redirect, repeats and previews included |
| Unique devices | 287 | Distinct address and user-agent pairs. A floor on people, not a count |
| Last scan | 3 hours ago | The freshness signal. Silence here is the useful alarm |
| Top country | US, 301 scans | Country resolution. The remaining 111 are spread or unknown |
| Busiest hour | 18:00 UTC | Convert to local time before concluding anything about behaviour |
| Device mix | 88% mobile, 6% bot | The bot share is your link-preview and crawler noise |
| Scans over time | 30 daily buckets | Shape beats total: a launch spike and a steady trickle differ |
Source: Illustrative. The lines and their meanings match the real per-code report; the values are invented so the shape is readable
How long QR scan analytics are kept, verified 17 August 2026
| Plan | Price | How long scan data is kept |
|---|---|---|
| TangoQR Free | $0 | For the life of the code. Full breakdowns for a code's first 50 scans, total scans always |
| TangoQR Pro | $10/mo | For the life of the code. Full breakdowns on every code, no scan limit |
| Bitly Free | $0 | No click or scan analytics at all. Scans are collected, none are shown |
| Bitly Core | $10/mo | 30 days |
| Bitly Growth | $29/mo | 120 days |
| Bitly Premium | $199/mo | 1 year |
Source: Bitly's own pricing page and its Free Plan post, both checked 17 August 2026
Common questions
- Can you track a static QR code?
-
No, and no provider can. A static code has the destination encoded in the pattern, so the phone goes straight there and no server you can see is involved. There is nothing to instrument after the fact. This is why the static or dynamic decision has to happen before the print run, not after.
- Do I need to add anything to my website to track scans?
-
No. Measurement happens at the redirect, one hop before the browser reaches your site, so it works even if your destination is a PDF or a page you do not control. The flip side is that your own analytics will not label that traffic as a QR scan on its own. Add UTM parameters to the destination if you want the two reports to line up.
- Does QR code tracking identify who scanned my code?
-
No. A scan record is a timestamp, a two-letter country code, a self-reported browser string, and an address used only to deduplicate devices. There is no name, no phone number, and no cross-site tracking. It is enough to tell you that scans are arriving from these countries on these devices at these hours, and not enough to tell you anything about a person.
- Can I start tracking a code I already printed?
-
Only if it was dynamic to begin with. If it points through a redirect, its scans have been recorded all along and are waiting for you. If it was static, the printed pattern points straight at your destination and cannot be retrofitted. Check which kind you have with the QR code analytics checker before you assume either way.
- Why is my scan count higher than the number of people I watched scan it?
-
Three reasons, usually all at once. Repeat scans from the same phone each count. Link previews from chat apps and crawlers fetch the redirect without a human involved. And a person who scans, loses the page, and scans again counts twice. Compare the total against unique devices to get a sense of the gap, and read the bot share in the device mix.
- How many scans does a free TangoQR code track?
-
All of them, forever. Collection is never limited or throttled by plan. What the free plan limits is the detailed view: the country, device and time-of-day breakdowns stay clear for a code's first 50 scans, and after that the running total keeps showing while the breakdowns move behind an upgrade. The data underneath is still being recorded, so upgrading shows you the history rather than starting a new one.
Sources
- RFC 9110, HTTP Semantics: 302 Found and the caching of redirect responses
- Cloudflare: adding the visitor's country to an HTTP request with CF-IPCountry
- Bitly: what the free plan includes, and data history by plan (checked 17 August 2026)
- Bitly pricing, scan data retention by tier (checked 17 August 2026)