Skip to content

Rate limiting recipe

Implement distributed rate limiting using Aerospike’s atomic increment operations.

Use case

Limit API requests per user/IP with:

  • Distributed coordination (works across multiple app servers)
  • Atomic counters (no race conditions)
  • Automatic reset via TTL

Quick implementation: fixed window

import com.aerospike.client.sdk.Cluster;
import com.aerospike.client.sdk.DataSet;
import com.aerospike.client.sdk.RecordResult;
import com.aerospike.client.sdk.RecordStream;
import com.aerospike.client.sdk.Session;
import com.aerospike.client.sdk.policy.Behavior;
public class RateLimiter {
private final Session session;
private final DataSet limits;
private final int maxRequests;
private final int windowSeconds;
public RateLimiter(Cluster cluster, int maxRequests, int windowSeconds) {
this.session = cluster.createSession(Behavior.DEFAULT);
this.limits = DataSet.of("app", "rate_limits");
this.maxRequests = maxRequests;
this.windowSeconds = windowSeconds;
}
public boolean isAllowed(String key) {
// Atomic increment - returns new value
RecordStream stream = session.update(limits.id(key))
.bin("count").add(1)
.expireRecordAfterSeconds(windowSeconds) // Auto-reset after window
.execute();
long count = stream.getFirst()
.filter(RecordResult::isOk)
.map(RecordResult::recordOrThrow)
.map(r -> r.getLong("count"))
.orElse(1L);
return count <= maxRequests;
}
public long getRemaining(String key) {
RecordStream stream = session.query(limits.id(key)).execute();
long remaining = stream.getFirst()
.filter(RecordResult::isOk)
.map(RecordResult::recordOrThrow)
.map(r -> maxRequests - r.getLong("count"))
.orElse((long) maxRequests);
return remaining;
}
}

Usage

import com.aerospike.client.sdk.Cluster;
import com.aerospike.client.sdk.ClusterDefinition;
try (Cluster cluster = new ClusterDefinition("localhost", 3000).connect()) {
RateLimiter limiter = new RateLimiter(cluster, 100, 60); // 100 req/min
String userId = "user-123";
if (limiter.isAllowed(userId)) {
// Process request
System.out.println("Request accepted for " + userId);
} else {
throw new RuntimeException(
"Rate limit exceeded. Remaining: " + limiter.getRemaining(userId)
);
}
}

Complete example

import com.aerospike.client.sdk.AerospikeException;
import com.aerospike.client.sdk.Cluster;
import com.aerospike.client.sdk.ClusterDefinition;
import com.aerospike.client.sdk.DataSet;
import com.aerospike.client.sdk.RecordResult;
import com.aerospike.client.sdk.RecordStream;
import com.aerospike.client.sdk.ResultCode;
import com.aerospike.client.sdk.Session;
import com.aerospike.client.sdk.policy.Behavior;
public class RateLimiterExample {
static class RateLimiter {
private final Session session;
private final DataSet limits;
private final int maxRequests;
private final int windowSeconds;
RateLimiter(Cluster cluster, int maxRequests, int windowSeconds) {
this.session = cluster.createSession(Behavior.DEFAULT);
this.limits = DataSet.of("test", "rate_limits");
this.maxRequests = maxRequests;
this.windowSeconds = windowSeconds;
}
boolean isAllowed(String key) {
try {
session.upsert(limits.id(key))
.bin("count").add(1)
.expireRecordAfterSeconds(windowSeconds)
.execute();
} catch (AerospikeException e) {
if (e.getResultCode() == ResultCode.FAIL_FORBIDDEN) {
session.upsert(limits.id(key))
.bin("count").add(1)
.execute();
} else {
throw e;
}
}
return getCount(key) <= maxRequests;
}
long getRemaining(String key) {
return Math.max(maxRequests - getCount(key), 0);
}
private long getCount(String key) {
RecordStream stream = session.query(limits.id(key)).execute();
return stream.getFirst()
.filter(RecordResult::isOk)
.map(RecordResult::recordOrThrow)
.map(r -> r.getLong("count"))
.orElse(0L);
}
}
public static void main(String[] args) {
try (Cluster cluster = new ClusterDefinition("localhost", 3000).connect()) {
RateLimiter limiter = new RateLimiter(cluster, 3, 60); // 3 req/min demo
String key = "user-123";
// Reset the demo key so output is deterministic.
limiter.session.delete(limiter.limits.id(key)).execute().close();
for (int i = 1; i <= 5; i++) {
boolean allowed = limiter.isAllowed(key);
long remaining = limiter.getRemaining(key);
System.out.println(
"Request " + i + ": " + (allowed ? "ALLOWED" : "BLOCKED") +
" (remaining=" + Math.max(remaining, 0) + ")"
);
}
}
}
}

Expected output

Request accepted for user-123

Why Aerospike for rate limiting?

  1. Atomic operationsincrement() is atomic across distributed servers
  2. TTL-based reset — Windows auto-expire without background jobs
  3. Sub-millisecond latency — Doesn’t slow down your API
  4. Horizontal scaling — Works across any number of app servers

Next steps

Feedback

Was this page helpful?

What type of feedback are you giving?

What would you like us to know?

+Capture screenshot

Can we reach out to you?