DEV Community

bowen Tse
bowen Tse

Posted on Originally published at toolkitloop.blogspot.com

Why Dynamic QR Codes Break: A Static-Code Fix for Devs

Disclosure first, because this is a tool post: I built the QR code generator at ToolkitLoop, and this article explains a failure mode I keep seeing in support threads — a printed QR code that "suddenly expired." It almost never expired. Something behind it died, and you can prove which thing in about a minute.

The mental model most people get wrong

A QR code is not a link. It is a payload, and what the payload contains decides everything about how it can fail.

A static QR code's payload is the content: https://example.com/menu, a Wi-Fi credential block, a vCard. The scanner reads it, and there is nothing else to go wrong except the destination itself.

A dynamic QR code's payload is a pointer: something like https://short.example/abc123. The scanner reads the pointer, the browser asks that short domain where to go, and only then does the visitor reach your page. That extra hop is the whole product — it is what lets you change the destination without reprinting — and it is also a single point of failure you do not own.

So when someone says "my free QR code stopped working," the first question is never "what is wrong with the squares?" It is "was the payload a pointer, and is the pointer still alive?"

A one-minute diagnosis you can actually run

Suppose a printed menu code now lands on an error page. Do not regenerate it yet. Work through this:

  1. Scan it and read the URL the scanner shows before tapping. If it is a short domain you do not control, you are in dynamic-code territory.
  2. Open that URL in a desktop browser and watch the address bar. Does it stay put, bounce through two or three hops, or land on a parking page? Each hop is another owner who can break your code.
  3. Open the final destination directly. If the final page is gone, the QR code is innocent — fix the page.
  4. If the final page loads but the short link does not, the redirect record is dead: deleted, downgraded, or orphaned when an account lapsed.

That distinction decides the fix. A dead page is fixed on your site. A dead redirect is fixed in the redirect service — or not at all, if you have lost access to it. A code that scans on one phone but not another is a third thing entirely: a print problem, covered below.

Tracing the redirect chain in code

If you have the short URL, you can inspect its hop chain instead of guessing. This snippet follows redirects manually and logs each step. It works best for same-origin links or hosts that allow cross-origin requests; many short-link hosts block browser fetches with CORS, in which case use your browser's Network tab or curl -I on the command line instead — the idea is the same, only the tool changes.

async function traceRedirect(startUrl, maxHops = 5) {
  let url = startUrl;
  for (let hop = 0; hop < maxHops; hop++) {
    const response = await fetch(url, { redirect: "manual" });
    const next = response.headers.get("location");
    console.log(`Hop ${hop}: ${response.status} ${url}`);
    if (!next) {
      console.log("Chain ends here. This is the final destination.");
      return url;
    }
    url = new URL(next, url).href;
  }
  console.log("Stopped after max hops — check for a redirect loop.");
  return url;
}

// Example: traceRedirect("https://short.example/abc123");
Enter fullscreen mode Exit fullscreen mode

Read the output like a detective. A chain that ends in a 404 means the destination moved. A chain that ends at a domain parking page means the redirect domain or record is gone. A chain that loops means someone wired two redirects to point at each other. A chain you cannot fetch from the browser at all is a reminder that the redirect hop is infrastructure you do not control — which is exactly the risk this article is about.

One honest caveat about that snippet: redirect: "manual" in a browser returns an opaque response for cross-origin redirects, and the location header is only readable when the host permits it. Do not treat a blocked fetch as proof the link is dead. Treat it as proof you need a different tool — Network tab, curl, or a server-side check — before you conclude anything.

The print failures that look like link failures

Not every dead code is a dead link. Some codes fail before the network is even involved:

  • Size. A code printed too small leaves no margin for shaky hands or weak cameras. The ToolkitLoop generator suggests about 25 mm or larger for print; posters and packaging need more, because viewing distance grows with the surface.
  • Contrast and polarity. Readers expect dark modules on a light background. Light-grey on white, colour-on-colour, or reversed white-on-black codes look designed and scan badly on at least some phones.
  • Logo coverage. A centre logo is safe when it stays small and the error correction level is high. Error correction levels run L, M, Q, and H, from least to most redundant data. A big logo on level L eats modules the reader cannot reconstruct; the same logo on level H often survives.
  • Quiet zone and distortion. The blank border around a code is part of the code. Crop it, stretch the code to fit a banner, or wrap it around a curved bottle, and even a perfect link will struggle.

The practical test is boring and effective: print the final file, scan it on two different phones, at the real distance, in realistic light. A code that only works on your own phone in perfect light is not done.

Why static is the default I ship

This is where the disclosure and the engineering meet. The generator I built at ToolkitLoop creates static codes only: the content is encoded in the image, generation happens locally in your browser, there is no redirect layer, and the downloaded file carries no watermark. You can try it here: https://toolkitloop.com/qr-code-generator

That choice trades away one feature — you cannot change the destination after printing — in exchange for removing an entire class of failure. There is no redirect account to lapse, no short domain to park, and no subscription keeping your printed posters alive. What can still break is the destination itself: if you delete the menu page the code points to, the code will faithfully deliver visitors to a 404. That is a website problem with a website fix, not a QR problem.

Static is not the right answer for everything. If you genuinely need to swap a campaign destination after printing, a dynamic code — ideally on a domain you control — is the honest tool for that job. But if the destination is stable — a homepage, a permanent menu, contact details, Wi-Fi credentials — paying for redirect machinery you will forget to maintain is how "free QR codes" end up with an expiry story.

A checklist before your next print run

  • Is the payload the final address, or a pointer you will have to maintain?
  • Does the final address load, right now, in a private browser window?
  • Is the code at least about 25 mm for handheld print, larger for distance viewing?
  • Is the contrast dark-on-light, with the quiet zone intact and no stretching?
  • Is any centre logo small, with error correction set high enough to cover it?
  • Have two different phones scanned the final file at the real distance?
  • Did you download SVG for print, reserving PNG for tools that specifically need a raster file?

If you can answer yes to all of those, the code you print is about as durable as a printed link can be. If you cannot, the failing item on that list is your fix — and it is almost never "make the squares prettier."

Now the hook I promised: how do you trace a suspicious redirect chain in your own stack — browser Network tab, curl -I, or a server-side check? And have you ever found a chain with a hop you did not know existed? I am curious which tool caught it, because that hidden hop is usually the one that breaks first.

Top comments (0)