Rate limiting a public demo without fingerprinting anyone

python dev.to

We put two models behind a public demo with no signup and no card. That creates an immediate problem: how do you stop one visitor from consuming the whole thing, without tracking anybody?

The usual answer is device fingerprinting. We decided against it, and this is what we built instead.

The constraint

A demo with no account has no identity to meter against. You need to tell callers apart well enough to enforce a limit, and that is genuinely in tension with not tracking people.

Canvas and WebGL fingerprinting solve it decisively. They also build a persistent identifier for someone who never agreed to one, work across sites, and survive clearing storage. Under GDPR and ePrivacy that is a consent obligation, not a legitimate-interest freebie.

We were not willing to make that trade for a demo.

What we actually do

Two components, combined and hashed:

def caller_hash(request, session_token):
    ip = (headers.get("cf-connecting-ip")
          or (headers.get("x-forwarded-for") or "").split(",")[0].strip()
          or request.client.host)
    return hashlib.sha256(f"{ip}|{session_token}".encode()).hexdigest()[:32]
Enter fullscreen mode Exit fullscreen mode

The session token is generated by the browser per visit, held in memory only, and gone when the tab closes. It is not a device identifier and nothing reads anything from the device.

The result is truncated to 32 characters and stored in Redis with a 24-hour TTL. The raw IP is never written to disk. What we keep is a one-way hash that expires by itself.

Verified from a clean browser: zero cookies, zero localStorage, zero sessionStorage.

The bug this design creates

Here is the part worth reading if you are building something similar.

We set the Sentinel limit to one company lookup per visitor per day. The original code incremented the counter on request, which is the obvious implementation and is fine at twenty lookups per day.

At one lookup per day it is a disaster. Mistype a company name, get no result, and your entire daily allowance is gone. The demo is unusable and the visitor leaves thinking the product is broken.

So the quota is reserved and then refunded:

reservation = reserve(bucket, caller, limit)   # atomic INCR
try:
    result = do_the_work()
    if not result:
        reservation.refund()                   # nothing produced, nothing charged
        return empty_response()
except Exception:
    reservation.refund()
    raise
reservation.commit()
Enter fullscreen mode Exit fullscreen mode

A company name that matches nothing, a query under three characters, a headline the model cannot resolve, an inference service that is down — none of them cost the visitor anything.

Reserve-then-refund rather than check-then-increment, specifically because Redis INCR is atomic. A read-then-write check would let two concurrent requests both pass.

Two ceilings, two messages

Per visitor, and global across all visitors. They fail differently because they mean different things to whoever is reading:

  • Per visitor: "Daily demo limit reached for this device. Come back tomorrow."
  • Global: "Demo capacity reached for now. Try again shortly."

Telling someone to come back tomorrow when the real problem is that you are saturated for ten minutes is a small lie that costs you the visitor.

The global ceiling resolves per request from a Redis key first, then an environment variable, then a default. Raising it during a traffic spike takes no restart and no deploy:

redis-cli SET demo:config:daily_ceiling 8000
Enter fullscreen mode Exit fullscreen mode

Was the trade worth it?

Fingerprinting would give us stronger abuse control. Someone determined can clear their session token and get a fresh allowance, and the IP hash is the only thing standing in the way.

We accept that. The demo is a static snapshot and a rate-limited inference call — the blast radius of abuse is small, and the cost of fingerprinting is paid by every honest visitor, permanently, so that we can inconvenience a few dishonest ones.

The privacy page says plainly that we could stop abuse more effectively by fingerprinting and have chosen not to. If you are going to make that choice, say so where people can read it.


Try it at dashboard.aiondashboard.site/demo — one company lookup and three event analyses per visitor per day, and only requests that return a result are counted.

Source: dev.to

arrow_back Back to Tutorials