Blog/tutorials

# How to ratelimit public pages

Learn how to use Unkey to ratelimit public pages, like newsletter sign up forms or login attempts.

Written by

Lê Sỹ Cường

Published on

Dec 11, 2025

Consider a typical authentication form. Without protection, someone could:

- Submit thousands of fake email addresses, polluting your database
- Attempt to guess valid email addresses through enumeration
- Overload your server with excessive requests
- Abuse authentication endpoints by trying random email/password combinations

Traditional rate limiting based on IP addresses has limitations. Users can bypass IP-based restrictions using VPNs, proxy servers, or botnets. This is where device fingerprinting shines.

## Device Fingerprinting: A Better Approach

Device fingerprinting creates a unique identifier for each device based on browser characteristics, hardware information, and other attributes. Unlike IP addresses, fingerprints are much harder to spoof and provide a more reliable way to identify individual devices.

The thumbmark.js library generates comprehensive fingerprints using:

- Browser characteristics (user agent, platform, language)
- Screen resolution and color depth
- Hardware information (CPU cores, memory)
- Canvas and WebGL rendering signatures
- Audio context fingerprinting
- Installed fonts and plugins

This creates a robust identifier that persists across sessions and IP changes.

## Implementing Rate Limiting with Unkey

Let's walk through a complete implementation using Next.js, Unkey for rate limiting, and thumbmark.js for device fingerprinting. You can see the full example in the unkey-fingerprint repository.

### Step 1: Setting Up Unkey

First, create an Unkey account and generate a root key with rate limiting permissions:

    1# Add to your .env.local
    2UNKEY_ROOT_KEY=your_unkey_root_key_here

### Step 2: Client-Side Fingerprinting

Install the required dependencies:

    1npm install @thumbmarkjs/thumbmarkjs @unkey/ratelimit

Create a React component that generates the device fingerprint:

    1"use client";
    2
    3import { useState, useEffect } from "react";
    4import { getThumbmark } from "@thumbmarkjs/thumbmarkjs";
    5import type { thumbmarkResponse } from "@/lib/fingerprint-validation";
    6
    7export default function WaitlistPage() {
    8  const [fingerprintData, setFingerprintData] =
    9    useState<thumbmarkResponse | null>(null);
    10
    11  useEffect(() => {
    12    // Generate device fingerprint when component mounts
    13    const generateFingerprint = async () => {
    14      try {
    15        const result = await getThumbmark();
    16        setFingerprintData(result);
    17      } catch (error) {
    18        console.error("Error generating fingerprint:", error);
    19      }
    20    };
    21
    22    generateFingerprint();
    23  }, []);
    24
    25  const handleSubmit = async (e: React.FormEvent) => {
    26    e.preventDefault();
    27
    28    if (!fingerprintData) {
    29      // Handle case where fingerprint isn't ready
    30      return;
    31    }
    32
    33    const response = await fetch("/api/waitlist", {
    34      method: "POST",
    35      headers: { "Content-Type": "application/json" },
    36      body: JSON.stringify({
    37        email,
    38        fingerprintData,
    39      }),
    40    });
    41
    42    // Handle response...
    43  };
    44
    45  // Rest of your component...
    46}

### Step 3: Server-Side Validation

Create a robust validation system for fingerprint data:

    1// lib/fingerprint-validation.ts
    2export interface ValidationResult {
    3  isValid: boolean;
    4  errors: string[];
    5}
    6
    7export function validateFingerprint(
    8  fingerprintData: unknown,
    9): ValidationResult {
    10  const errors: string[] = [];
    11
    12  if (!fingerprintData || typeof fingerprintData !== "object") {
    13    errors.push("Invalid fingerprint data format");
    14    return { isValid: false, errors };
    15  }
    16
    17  const data = fingerprintData as Record<string, unknown>;
    18
    19  // Check required fields
    20  if (!data.components || !data.thumbmark) {
    21    errors.push("Missing required fingerprint components");
    22    return { isValid: false, errors };
    23  }
    24
    25  // Validate structure and realism
    26  const components = data.components as Record<string, any>;
    27
    28  if (!validateFingerprintStructure(components)) {
    29    errors.push("Invalid fingerprint structure");
    30  }
    31
    32  if (!validateFingerprintRealism(components)) {
    33    errors.push("Fingerprint appears to be fake or unrealistic");
    34  }
    35
    36  return {
    37    isValid: errors.length === 0,
    38    errors,
    39  };
    40}

### Step 4: API Endpoint with Rate Limiting

Create the API endpoint that combines fingerprint validation with Unkey rate limiting:

    1// app/api/waitlist/route.ts
    2import { NextRequest, NextResponse } from "next/server";
    3import { validateFingerprint } from "@/lib/fingerprint-validation";
    4import { Ratelimit } from "@unkey/ratelimit";
    5
    6const limiter = new Ratelimit({
    7  rootKey: process.env.UNKEY_ROOT_KEY!,
    8  duration: 3600000, // 1 hour
    9  limit: 3, // 3 requests per hour
    10  async: false,
    11  namespace: "waitlist",
    12});
    13
    14export async function POST(request: NextRequest) {
    15  try {
    16    const body = await request.json();
    17    const { email, fingerprintData } = body;
    18
    19    // Validate required fields
    20    if (!email || !fingerprintData) {
    21      return NextResponse.json(
    22        {
    23          error: "Email and device fingerprint are required",
    24          success: false,
    25        },
    26        { status: 400 },
    27      );
    28    }
    29
    30    // Validate email format
    31    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    32    if (!emailRegex.test(email)) {
    33      return NextResponse.json(
    34        {
    35          error: "Invalid email format",
    36          success: false,
    37        },
    38        { status: 400 },
    39      );
    40    }
    41
    42    // Validate fingerprint data
    43    const validation = validateFingerprint(fingerprintData);
    44    if (!validation.isValid) {
    45      return NextResponse.json(
    46        {
    47          error: "Invalid or suspicious device fingerprint",
    48          success: false,
    49        },
    50        { status: 400 },
    51      );
    52    }
    53
    54    // Check rate limit using Unkey with the validated fingerprint
    55    const { success, limit, remaining, reset } = await limiter.limit(
    56      fingerprintData.thumbmark,
    57    );
    58
    59    if (!success) {
    60      const resetTime = new Date(Date.now() + reset);
    61      return NextResponse.json(
    62        {
    63          error: "Rate limit exceeded. Please try again later.",
    64          success: false,
    65          rateLimited: true,
    66          resetTime: resetTime.toISOString(),
    67          remaining,
    68          limit,
    69        },
    70        { status: 429 },
    71      );
    72    }
    73
    74    // Process the request (e.g., save to database)
    75    console.log("New signup:", {
    76      email,
    77      fingerprint: fingerprintData.thumbmark,
    78    });
    79
    80    return NextResponse.json({
    81      success: true,
    82      message: "Successfully added to waitlist!",
    83      remaining,
    84      limit,
    85    });
    86  } catch (error) {
    87    return NextResponse.json(
    88      {
    89        error: "Internal server error",
    90        success: false,
    91      },
    92      { status: 500 },
    93    );
    94  }
    95}

## Security Best Practices

When implementing fingerprint-based rate limiting, follow these security practices:

### 1. Comprehensive Fingerprint Validation

Never trust client-provided fingerprint data. Validate:

- Structure: Ensure all required components are present
- Data Types: Verify each component has the correct type
- Realism: Check for obviously fake values (unreasonable screen resolutions, invalid user agents)
- Hash Integrity: Validate that the fingerprint hash matches the components

### 2. Fallback Mechanisms

Implement proper fallback handling for when Unkey is unavailable:

    1const fallback = (identifier: string) => ({
    2  success: false,
    3  limit: 0,
    4  reset: 0,
    5  remaining: 0,
    6});
    7
    8const limiter = new Ratelimit({
    9  // ... other config
    10  timeout: {
    11    ms: 3000,
    12    fallback,
    13  },
    14  onError: (err, identifier) => {
    15    console.error(`${identifier} - ${err.message}`);
    16    return fallback(identifier);
    17  },
    18});

### 3. User Experience

Provide clear feedback about rate limits when a user exceeds their limit, in this repo example we just show a simple message:

    1{
    2  rateLimitInfo && (
    3    <div className="mt-6 p-4 bg-blue-50 rounded-lg">
    4      <div className="flex items-center space-x-2 text-sm text-blue-600">
    5        <Clock className="h-4 w-4" />
    6        <span>
    7          {rateLimitInfo.remaining} of {rateLimitInfo.limit} requests remaining
    8          this hour
    9        </span>
    10      </div>
    11    </div>
    12  );
    13}

## Advanced Considerations

### Handling Legitimate Users

Sometimes legitimate users might trigger rate limits (family sharing devices, public computers). Consider:

- Providing a way for users to request limit increases
- Implementing progressive rate limiting (stricter limits for new users)
- Adding CAPTCHA challenges for suspicious activity

### Privacy Implications

Device fingerprinting raises privacy concerns. Be transparent about:

- What data you're collecting
- How you're using it
- How users can opt out if needed

### Monitoring and Analytics

Track rate limiting effectiveness:

    1// Log rate limit violations for monitoring
    2if (!success) {
    3  console.warn(`Rate limit exceeded: ${fingerprintData.thumbmark}`);
    4  // Send to monitoring service
    5}

## Conclusion

Device fingerprinting combined with Unkey's rate limiting provides a robust solution for protecting public endpoints. This approach is more reliable than IP-based restrictions and offers better protection against automated abuse.

The complete example demonstrates how to implement this pattern securely, with proper validation, error handling, and user experience considerations. By following these practices, you can protect your public endpoints while maintaining a smooth experience for legitimate users.

For the full implementation, check out the unkey-fingerprint repository on GitHub.

Written by

Lê Sỹ Cường

Published on

Dec 11, 2025

Contents

- Device Fingerprinting: A Better Approach
- Implementing Rate Limiting with Unkey
- Step 1: Setting Up Unkey
- Add to your .env.local
- Step 2: Client-Side Fingerprinting
- Step 3: Server-Side Validation
- Step 4: API Endpoint with Rate Limiting
- Security Best Practices
- 1. Comprehensive Fingerprint Validation
- 2. Fallback Mechanisms
- 3. User Experience
- Advanced Considerations
- Handling Legitimate Users
- Privacy Implications
- Monitoring and Analytics
- Conclusion

Suggested

Vncod — Hành trình định nghĩa lại tiêu chuẩn vận hành Game

Mar 26, 2026

Building a flow editor

Dec 02, 2025

Why we're leaving serverless

Aug 01, 2025

## Sẵn sàng bứt phá doanh thu.
Bắt đầu ngay hôm nay.

Liên hệ với chúng tôi
Server Game

Hỗ trợ cài đặt trọn gói. Cam kết vận hành ổn định 24/7.

We value your privacy
This site uses cookies to improve your browsing experience, analyze site traffic, and show personalized content.