How to Build a Simple Java Rate Limiter from Scratch

This article explains the concept and benefits of rate limiting, reviews popular Java libraries, and walks through a custom implementation using maps, locks, and atomic counters, complete with full source code and a test script demonstrating a 2‑requests‑per‑2‑seconds policy.

FunTester
FunTester
FunTester
How to Build a Simple Java Rate Limiter from Scratch

Rate Limiting Overview

Rate limiting controls the number of requests a service processes within a defined time window. It protects system stability, mitigates abuse (e.g., DoS or brute‑force attacks), limits costly resource consumption, and improves response latency for legitimate users.

Motivation for a Custom Limiter

When the use case does not require distributed coordination, complex algorithms, or advanced metrics, a lightweight in‑process limiter avoids the overhead of third‑party libraries. The implementation below provides a self‑contained N/M limiter suitable for single‑node Java or Groovy services.

Design Overview

Configuration management : a Map<String, LimitConfig> stores, per key, the maximum request count ( maxTimes) and the window length in seconds ( duration). Adding a configuration creates a default entry if the key is unseen.

State management : three maps track runtime data for each key: Map<String, Integer> lastTime – timestamp of the last request that started a window. Map<String, AtomicInteger> requestTimes – current request count within the active window. Map<String, ReentrantLock> allLock – per‑key lock to serialize state updates.

Thread safety : a global ReentrantLock writeLock protects configuration mutations. Each key’s ReentrantLock guards the check‑and‑update sequence for that key, while AtomicInteger provides lock‑free increment semantics.

Implementation

import com.funtester.frame.SourceCode
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock

/**
 * Rate‑limiting utility supporting N/M limits
 */
class RateLimit {
    /** total limit configuration */
    Map<String, LimitConfig> config = [:]
    /** last request timestamp */
    Map<String, Integer> lastTime = [:]
    /** request count */
    Map<String, AtomicInteger> requestTimes = [:]
    /** lock for each key */
    Map<String, ReentrantLock> allLock = [:]
    /** global write lock */
    ReentrantLock writeLock = new ReentrantLock()

    /**
     * Determine whether a request should be blocked.
     * @param key the rate‑limit identifier
     * @return true if the request must be limited
     */
    boolean isLimit(String key) {
        if (!config.containsKey(key)) {
            addConfig(key, 2, 2) // default 2 requests per 2 seconds
            return isLimit(key)   // recurse after initialization
        }
        def mark = SourceCode.getMark()
        if (mark - lastTime[key] >= config[key].duration) { // new window
            if (allLock[key].tryLock(1, TimeUnit.SECONDS)) {
                try {
                    if (mark - lastTime[key] >= config[key].duration) {
                        lastTime[key] = mark
                        requestTimes[key] = new AtomicInteger(1)
                        return false
                    }
                } finally {
                    allLock[key].unlock()
                }
            }
        }
        if (requestTimes[key].get() >= config[key].maxTimes) {
            return true // exceeded max count
        }
        requestTimes[key].getAndIncrement()
        return false
    }

    /**
     * Add a new limit configuration.
     * @param key identifier
     * @param maxTimes maximum requests per window
     * @param duration window length in seconds
     */
    void addConfig(String key, int maxTimes, int duration) {
        if (writeLock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                if (!config.containsKey(key)) {
                    config[key] = new LimitConfig(maxTimes: maxTimes, duration: duration)
                    allLock[key] = new ReentrantLock()
                    lastTime[key] = SourceCode.getMark()
                    requestTimes[key] = new AtomicInteger(0)
                }
            } finally {
                writeLock.unlock()
            }
        }
    }

    /** limit configuration holder */
    static class LimitConfig {
        int maxTimes   // maximum requests
        int duration   // window duration in seconds
    }
}

Test Script

import com.funtester.httpclient.FunHttp
import com.funtester.utils.RateLimit

class Routine extends FunHttp {
    static void main(String[] args) {
        def limit = new RateLimit()
        limit.addConfig("test", 1, 1) // 1 request per second
        1000.times {
            sleep(0.1)
            fun {
                def limited = limit.isLimit("test")
                if (!limited) {
                    output("未限流")
                }
            }
        }
    }
}

The console output shows that only the allowed request passes each second, confirming that the default 2‑requests‑per‑2‑seconds configuration (or the custom 1‑request‑per‑1‑second rule) works as intended.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend Developmentconcurrencyrate limitingToken Bucket
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.