Skip to content
The Cloud Guy
ATC-007 AWS Guide

A QR-code redirector with click analytics, for pennies a month

Fast redirects with analytics that never slow the user down. The serverless AWS design, an even cheaper edge version, and the Azure equivalent.

Published
Reading time
5 min
On this page
  1. Two paths, one request
  2. The details that matter
  3. What it costs
  4. What I’d change
  5. The even cheaper version: do it at the edge
  6. The same on Azure

A small SaaS business I work with prints QR codes on material it gives to its business partners. Each code points to a short URL like go.example.com/partner-name, and each one has to do two things: send the person to the right landing page, and record that the scan happened.

The temptation is to do both in one place. Look up the destination, write the analytics row, then redirect. It works, and it makes every scan wait for a database write. This design splits the job in two, and that split is the lesson.

Two paths, one request

Phone scans QR API Gateway custom domain, TLS Redirect function no VPC, small, fast DynamoDB async 302 to landing page default URL if unknown the user waits for this only Click tracker in the VPC RDS Proxy MySQL: clicks nobody waits for this
Two paths from one request. Only the left one is on the clock.

The fast path is all the user waits for. API Gateway receives the request on the custom domain and calls a small redirect function. The function looks up the path in DynamoDB and returns a 302 to the landing page. The design target for the whole redirect is 100–300 ms.

The slow path is for analytics, and nobody waits for it. Before returning, the redirect function fires an asynchronous invocation of a second function, the click tracker, passing the path, timestamp, user agent and referrer. The tracker runs inside the VPC, connects through RDS Proxy to the MySQL database the business already had, and inserts one row per scan.

Splitting it this way buys three things:

  • Speed. The redirect never waits for a database. The original design put the difference at roughly 250 ms against 800 ms.
  • Resilience. If the database is slow or down, people still get to the landing page. You lose a few analytics rows, not a customer.
  • Simpler networking. Only the click tracker needs to be in the VPC to reach the database. The redirect function stays outside it, so it reaches DynamoDB and Lambda directly, with no NAT gateway or VPC endpoints to pay for.

The details that matter

The mappings live in DynamoDB, not in code. Each item has a path, a destination URL and an active flag. Adding a QR code or retiring one is a table edit, not a deployment, and with a few dozen items on-demand billing costs next to nothing.

Never show an error. An unknown path, an inactive mapping or an unexpected failure all redirect to a default URL, usually the company’s home page. Someone who scanned a code on paper can’t do anything with an error message.

Make every scan count. The response is a 302 with Cache-Control: no-store. A permanent 301, or a cacheable 302, lets the browser skip the redirector next time, and that scan disappears from your analytics.

Validate the path. Paths longer than 255 characters, or with traversal sequences, get a 400 before they reach the database.

Await the asynchronous call. It sounds contradictory, but it isn’t. An Event invocation returns as soon as Lambda has accepted the event, in a few milliseconds. If you don’t await it, the function can return and be frozen before the request has actually left, and the click is silently lost.

Treat clicks as at-least-once. Lambda retries a failed asynchronous invocation up to two more times, and AWS’s documentation warns that an event can occasionally arrive more than once even without an error. Give each click an idempotency key, such as the API Gateway request ID, and make it unique in the table. Then a duplicate is a no-op rather than an inflated count.

Drop what you don’t need. The first version looked up each visitor’s location from their IP address. That library made the function package much bigger for data nobody was using yet, so it was removed. You can always derive coarse location later from the stored data.

What it costs

At list prices, before any free tier, each scan costs roughly:

Item Per million scans
API Gateway REST API $3.50
Redirect function (512 MB, ~100 ms) ~$1.03
Click tracker (512 MB, ~500 ms) ~$4.37
DynamoDB reads (eventually consistent) ~$0.06
Total, before logs about $9

At 10,000 scans a month, that’s under ten cents. Two things will cost more than that if you’re careless:

  • Logging. If the function logs every incoming event in full, CloudWatch ingestion can cost more than the redirector itself. Log the path and the outcome, not the whole request.
  • The database. This design reuses an existing MySQL database and its RDS Proxy. If you’d have to create them just for this, use DynamoDB for the clicks too.

What I’d change

  • HTTP API instead of REST API. At $1.00 per million requests against $3.50, it’s the single biggest saving. A redirector doesn’t need any REST API-only features.
  • Arm functions. The same code on Graviton costs about 20% less per second of compute.
  • A dead-letter destination for the click tracker, so failed clicks land somewhere you can see them instead of vanishing after the retries.
  • Less personal data. An IP address together with a user agent is personal data in many jurisdictions. Store a truncated IP, or just a country, unless you have a real use for the full address, and set a retention period on both the table and the logs.

The even cheaper version: do it at the edge

If you can live with analytics that arrive in batches rather than in real time, you don’t need API Gateway or Lambda at all.

  • Put the mappings in a CloudFront KeyValueStore.
  • Answer each request with a CloudFront Function that reads the store and returns the 302 at the edge location nearest the visitor.
  • Get the analytics from CloudFront’s access logs, delivered to S3 and queried with Athena when you need a report.

CloudFront Functions cost $0.10 per million invocations, and KeyValueStore reads $0.03 per million. CloudFront’s always-free allowance includes 2 million function invocations and 2 million store reads a month, so a small business’s QR codes would likely cost nothing at all. The trade-off is that CloudFront Functions can’t make network calls, so there’s no live click counter, only logs. One more catch: CloudFront’s flat-rate plans don’t include KeyValueStore or access logs, so this version stays on pay-as-you-go pricing.

The same on Azure

Azure has a direct equivalent for each piece, with one improvement worth copying back to AWS.

  • Azure Functions with an HTTP trigger does the redirect, reading the mapping from Table Storage (or Cosmos DB).
  • Instead of invoking a second function directly, the redirect function writes the click to a Storage queue through an output binding. A queue-triggered function then writes it to Azure SQL. The queue gives you retries and a poison queue for clicks that keep failing, which is the dead-letter behavior I’d add on AWS.
  • For a handful of fixed codes, Azure Front Door’s rules engine can return redirects with no code at all, and its logs can carry the analytics. Front Door Standard carries a $35 monthly base fee, though, so at small scale the Functions version is cheaper.

The design choice is identical on both clouds. Keep the user’s path short, and push everything else onto a path nobody waits for.


Prices are US list prices as of October 2026. Sources: API Gateway pricing, Lambda pricing, DynamoDB on-demand pricing, CloudFront pay-as-you-go pricing, Lambda asynchronous error handling.

  • aws
  • serverless
  • lambda
  • dynamodb
  • cloudfront
  • azure-functions
  • cost

Related

Have a project in mind?

Let's see if I can help.

Send a message