node-ip-rate-limiter
v1.2.0
Published
Adaptive IP rate limiter with hierarchical pattern tracking (p2/p3 patterns) for in-memory rate limiting
Maintainers
Readme
node-ip-rate-limiter
An adaptive IP rate limiter with hierarchical pattern tracking (p2/p3 patterns) for in-memory rate limiting. This package automatically detects patterns in IP addresses and applies rate limiting at different levels (single IP, /24 subnet, /16 network).
Features
- Adaptive Rate Limiting: Automatically detects and groups IPs by patterns
- Hierarchical Pattern Tracking:
- Single IP: Individual IP address tracking
- P3 Pattern (/24 subnet): Groups IPs sharing the first 3 octets (e.g.,
72.145.152.*) - P2 Pattern (/16 network): Groups IPs sharing the first 2 octets (e.g.,
72.145.*.*)
- Real Request Counting: Every request is counted per IP, per /24 and per /16; blocking always uses these request counts
- Automatic Grouping: The /24 and /16 quotas switch on when several IPs or subnets from the same range are active
- In-Memory Storage: Fast, efficient in-memory storage with automatic expiration
- Zero Dependencies: No external dependencies required
Installation
npm install node-ip-rate-limiterQuick Start
const { createRateLimiter } = require('node-ip-rate-limiter');
const limiter = createRateLimiter({
moduleName: 'my-api',
single: {
maxCount: 10, // Max requests per IP
expiryTime: 60000 // 60 seconds in milliseconds
},
p3Series: {
maxCount: 50, // Max requests per /24 subnet
expiryTime: 60000 // 60 seconds
},
p2Series: {
maxCount: 100, // Max requests per /16 network
expiryTime: 60000 // 60 seconds
}
});
// Check an IP address
const result = limiter.check('72.145.152.42');
if (result.isDuplicate) {
console.log('Rate limit exceeded!');
console.log(result.reason);
} else {
console.log('Request allowed');
console.log(`Current level: ${result.level}`);
console.log(`Count: ${result.count}`);
}Multi-module Example (same Node process)
const { createRateLimiter, ipRateLimiting } = require('node-ip-rate-limiter');
const authLimiter = createRateLimiter({
moduleName: 'auth',
single: { maxCount: 5, expiryTime: 60000 },
p3Series: { maxCount: 20, expiryTime: 60000 },
p2Series: { maxCount: 50, expiryTime: 60000 }
});
const apiLimiter = createRateLimiter({
moduleName: 'api',
single: { maxCount: 10, expiryTime: 60000 },
p3Series: { maxCount: 30, expiryTime: 60000 },
p2Series: { maxCount: 80, expiryTime: 60000 }
});
// Option 1: use the per-module instance
authLimiter.check('192.168.1.10');
// Option 2: use the registry via moduleName
ipRateLimiting('192.168.1.10', 'api');How It Works
Pattern Detection
The rate limiter tracks IP addresses at three levels:
- Single IP (
72.145.152.42): Individual IP tracking - P3 Pattern (
72.145.152.*): First 3 octets - /24 subnet - P2 Pattern (
72.145.*.*): First 2 octets - /16 network
Request Counting
Every request increments three request counters, each with its own expiry window:
- the IP counter (
single.expiryTime) - the /24 counter (
p3Series.expiryTime) - the /16 counter (
p2Series.expiryTime)
Which Limits Apply
- Per-IP limit (
single.maxCount): always applies. - /24 limit (
p3Series.maxCount): applies while 2+ distinct IPs from that /24 are active. - /16 limit (
p2Series.maxCount): applies while 2+ distinct /24 subnets from that /16 are active.
A request is blocked when any applicable counter is over its limit. level is the broadest active tier (or the tier that blocked), and count is that tier's real request count.
Example (single = 5, p3 = 10, p2 = 15):
72.145.152.42 → level single, count 1 (IP counter)
72.145.152.43 → level p3, count 2 (/24 counter: 2 requests)
72.145.83.96 → level p2, count 3 (/16 counter: 3 requests)
...16th request anywhere in 72.145.*.* → blocked at p2, count 16A range stops being grouped once its members have been inactive for the expiry window.
Changelog
1.2.0
- Fixed: the /16 tier blocked on the number of distinct /24 subnets instead of the number of requests, so a distributed flood inside a few subnets was never blocked.
- Fixed: the first IP seen in a subnet stayed at the per-IP tier and its counter was deleted whenever a neighbour was evaluated, so it never reached its limit.
- Fixed: the /16 pattern count could expire independently and read 0.
- The per-IP limit now also applies inside grouped subnets and networks.
countis now always a real request count.- Removed the full-store scans on every grouped request (about 0.75 ms per check under load in 1.1.0).
API Reference
createRateLimiter(config)
Creates and returns an isolated limiter instance for a single moduleName (safe to use multiple modules in one process).
Returns:
moduleName(string)check(ip)(function) ->{ isDuplicate, level, reason, count }
Example:
const { createRateLimiter } = require('node-ip-rate-limiter');
const limiter = createRateLimiter({
moduleName: 'api-server',
single: { maxCount: 100, expiryTime: 60000 },
p3Series: { maxCount: 500, expiryTime: 60000 },
p2Series: { maxCount: 1000, expiryTime: 60000 }
});
const result = limiter.check('72.145.152.42');initRateLimiter(config) (Deprecated)
Initializes the single global default limiter.
Deprecated: unsafe for multi-module usage (each call replaces the global default). Prefer createRateLimiter().
Parameters:
config(Object): Configuration objectmoduleName(string): Unique name for this rate limiter instancesingle(Object): Configuration for single IP rate limitingmaxCount(number): Maximum number of requests allowedexpiryTime(number): Time in milliseconds before the limit resets
p3Series(Object): Configuration for /24 subnet rate limitingmaxCount(number): Maximum number of requests allowed per subnetexpiryTime(number): Time in milliseconds before the limit resets
p2Series(Object): Configuration for /16 network rate limitingmaxCount(number): Maximum number of requests allowed per networkexpiryTime(number): Time in milliseconds before the limit resets
Example:
initRateLimiter({
moduleName: 'api-server',
single: { maxCount: 100, expiryTime: 60000 },
p3Series: { maxCount: 500, expiryTime: 60000 },
p2Series: { maxCount: 1000, expiryTime: 60000 }
});ipRateLimiting(ip, moduleName?)
Checks if an IP address should be rate limited.
Parameters:
ip(string): Client IP address (IPv4 or IPv6)moduleName(string, optional): Which module's configuration to use (as registered bycreateRateLimiter()/initRateLimiter())
Returns:
Object: Rate limiting resultisDuplicate(boolean):trueif rate limit exceeded,falseotherwiselevel(string): Current rate limiting level ("single","p3", or"p2")reason(string): Human-readable explanation of the resultcount(number): Number of requests counted atlevelin the current window
Example:
const result = ipRateLimiting('192.168.1.100', 'api-server');
// {
// isDuplicate: false,
// level: 'single',
// reason: 'Request allowed under the per-IP rate limit...',
// count: 1
// }Usage Examples
Express.js Middleware
const express = require('express');
const { createRateLimiter } = require('node-ip-rate-limiter');
const app = express();
// Initialize isolated rate limiter
const limiter = createRateLimiter({
moduleName: 'express-api',
single: { maxCount: 100, expiryTime: 60000 },
p3Series: { maxCount: 500, expiryTime: 60000 },
p2Series: { maxCount: 1000, expiryTime: 60000 }
});
// Rate limiting middleware
function rateLimitMiddleware(req, res, next) {
const clientIP = req.ip || req.connection.remoteAddress;
const result = limiter.check(clientIP);
if (result.isDuplicate) {
return res.status(429).json({
error: 'Rate limit exceeded',
reason: result.reason,
retryAfter: 60
});
}
// Add rate limit headers
res.setHeader('X-RateLimit-Level', result.level);
res.setHeader('X-RateLimit-Count', result.count);
next();
}
app.use(rateLimitMiddleware);
app.get('/api/data', (req, res) => {
res.json({ message: 'Success' });
});
app.listen(3000);Batch Processing
const { createRateLimiter } = require('node-ip-rate-limiter');
const fs = require('fs');
// Initialize
const limiter = createRateLimiter({
moduleName: 'batch-processor',
single: { maxCount: 10, expiryTime: 60000 },
p3Series: { maxCount: 50, expiryTime: 60000 },
p2Series: { maxCount: 100, expiryTime: 60000 }
});
// Process IPs from file
const ips = JSON.parse(fs.readFileSync('ip_addresses.json', 'utf8'));
const results = [];
for (const ip of ips) {
const result = limiter.check(ip);
results.push({ ip, ...result });
if (result.isDuplicate) {
console.log(`Blocked: ${ip} - ${result.reason}`);
}
}
console.log(`Processed ${results.length} IPs`);Response Examples
Single IP (First Request)
{
isDuplicate: false,
level: 'single',
reason: 'Request allowed under the per-IP rate limit. The client is currently treated as a single IP.',
count: 1
}P3 Pattern Detected
{
isDuplicate: false,
level: 'p3',
reason: 'Request allowed under the /24 subnet rate limit. Multiple IPs were detected in the same subnet and are sharing a common quota.',
count: 2
}P2 Pattern Detected
{
isDuplicate: false,
level: 'p2',
reason: 'Request allowed under the /16 network rate limit. Traffic from a wider network is being grouped and rate-limited together.',
count: 3
}Rate Limit Exceeded
{
isDuplicate: true,
level: 'p3',
reason: 'Request blocked because the /24 subnet rate limit has been exceeded. Multiple IPs from the same subnet are sharing this quota.',
count: 6
}Important Notes
- In-Memory Only: This package uses in-memory storage. Data is lost on process restart.
- Single Process: Designed for single-process applications. For multi-process setups, consider using Redis or a shared storage solution.
- Automatic Cleanup: Expired entries are automatically cleaned up every 10 minutes.
- IPv4 + IPv6: Supports both IPv4 and IPv6. IPv6 is rate-limited by the first /64 prefix (no /48-/32 promotion yet).
- Multi-module (in one process): Supported via
createRateLimiter()/ipRateLimiting(ip, moduleName).initRateLimiter()swaps a single global default and is not safe for multi-module usage.
License
ISC
Author
Mirabel Technologies
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
For issues, questions, or contributions, please visit the npm package page.
