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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
