@periodic/titanium
v1.0.3
Published
Production-ready Redis-backed rate limiting middleware for Express with TypeScript support, fail-safe design, and flexible configuration
Downloads
30
Maintainers
Readme
🛡️ Periodic Titanium
Production-ready IORedis-backed rate limiting middleware for Express with TypeScript support, fail-safe design, and flexible configuration.
🎯 Why Titanium?
Building a robust API requires protecting your endpoints from abuse, but most rate limiting solutions come with significant tradeoffs:
- In-memory limiters don't scale across multiple servers
- Generic packages force you into opinionated implementations
- Complex solutions add unnecessary overhead for simple use cases
PERIODIC Titanium provides the perfect middle ground:
✅ IORedis-backed for distributed rate limiting across multiple instances
✅ Framework-agnostic core with clean Express adapter
✅ TypeScript-first with complete type safety
✅ Fail-safe design that never breaks your application
✅ Zero dependencies except Express and IORedis (peer dependencies)
✅ Flexible configuration for user-based, IP-based, or custom identification
✅ Production-tested with atomic Redis operations to prevent race conditions
📦 Installation
npm install @periodic/titanium ioredis expressPeer Dependencies:
express^4.0.0 || ^5.0.0ioredis^5.0.0
🚀 Quick Start
Basic Usage (IP-based)
import express from 'express';
import Redis from 'ioredis';
import { rateLimit } from '@periodic/titanium';
const app = express();
// Create Redis client (auto-connects, no need to call connect())
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
// Apply rate limiting to all routes
app.use(rateLimit({
redis,
limit: 100, // 100 requests
window: 60, // per 60 seconds
keyPrefix: 'api' // Redis key prefix
}));
app.get('/api/data', (req, res) => {
res.json({ message: 'Success!' });
});
app.listen(3000);User-based Rate Limiting (JWT)
import { rateLimit } from '@periodic/titanium';
// Rate limit based on authenticated user ID
app.post('/api/resource',
authMiddleware, // Your JWT auth middleware
rateLimit({
redis,
limit: 10,
window: 60,
keyPrefix: 'create-resource',
// Extract user ID from JWT token
identifier: (req) => req.user?.id?.toString() || null
}),
createResourceHandler
);🌥️ Using with Redis Cloud
This package works seamlessly with any hosted Redis service including Redis Cloud, AWS ElastiCache, Azure Cache, Upstash, and more.
Quick Setup
import Redis from 'ioredis';
import { rateLimit } from '@periodic/titanium';
// Connect to Redis Cloud (or any hosted Redis)
const redis = new Redis(process.env.REDIS_URL);
app.use(rateLimit({
redis,
limit: 100,
window: 3600,
keyPrefix: 'api'
}));Connection URL Format
redis://username:password@hostname:portExample:
redis://default:[email protected]:12345Popular Redis Providers
const redis = new Redis({
host: 'redis-xxxxx.c1.us-east-1-2.ec2.cloud.redislabs.com',
port: 12345,
password: 'your-password',
tls: {}, // Required
});Get connection URL:
- Go to Redis Cloud Dashboard
- Select your database
- Copy "Public endpoint"
const redis = new Redis({
host: 'your-cluster.xxxxx.cache.amazonaws.com',
port: 6379,
password: 'your-auth-token', // Optional
});const redis = new Redis({
host: 'your-cache.redis.cache.windows.net',
port: 6380,
password: 'your-access-key',
tls: {}, // Required
});const redis = new Redis({
host: 'your-region.upstash.io',
port: 6379,
password: 'your-token',
tls: {}, // Required
});Environment Variables (Recommended)
# .env
REDIS_URL=redis://default:[email protected]:12345import 'dotenv/config';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);See full guide: Using with Redis Cloud
🔐 Security Best Practices
- ✅ Never hardcode credentials - Use environment variables
- ✅ Use TLS/SSL - Most cloud providers require it
- ✅ Add to .gitignore - Never commit
.envfiles - ✅ Rotate credentials - Change passwords regularly
- ✅ Use IP whitelisting - Restrict access to known IPs
🚀 Production-Ready Example
import express from 'express';
import Redis from 'ioredis';
import { rateLimit } from '@periodic/titanium';
import 'dotenv/config';
const app = express();
// Create Redis client with error handling
const redis = new Redis(process.env.REDIS_URL, {
retryStrategy: (times) => Math.min(times * 50, 2000),
maxRetriesPerRequest: 3,
});
// Event handlers
redis.on('ready', () => console.log('✅ Redis connected'));
redis.on('error', (err) => console.error('❌ Redis error:', err.message));
// Apply rate limiting
app.use('/api', rateLimit({
redis,
limit: 1000,
window: 3600,
keyPrefix: 'api',
failStrategy: 'open', // Allow requests if Redis is down
}));
// Graceful shutdown
process.on('SIGTERM', async () => {
await redis.quit();
process.exit(0);
});
app.listen(3000);🐛 Troubleshooting Redis Cloud
Connection Refused / Timeout
- Check Redis Cloud dashboard - is database running?
- Verify connection URL format
- Check IP whitelist in Redis Cloud settings
- Ensure public endpoint is enabled
Authentication Error
// Make sure URL includes username and password
redis://default:yourpassword@host:portTLS/SSL Required
const redis = new Redis(process.env.REDIS_URL, {
tls: {}, // Enable TLS
});Full troubleshooting guide: Redis Cloud Usage Guide
🎛️ Configuration Options
Core Options
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| redis | Redis | ✅ Yes | - | IORedis client instance |
| limit | number | ✅ Yes | - | Maximum requests allowed in time window |
| window | number | ✅ Yes | - | Time window in seconds |
| keyPrefix | string | ✅ Yes | - | Redis key prefix (identifies rate limit type) |
| algorithm | 'fixed-window' | No | 'fixed-window' | Rate limiting algorithm |
| logger | Logger | No | console | Custom logger instance |
Express-Specific Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| identifier | (req) => string \| null | IP-based | Custom function to extract client identifier |
| message | string | 'Too many requests...' | Error message when limit exceeded |
| skip | (req) => boolean | - | Function to skip rate limiting conditionally |
| failStrategy | 'open' \| 'closed' | 'open' | Behavior when Redis is unavailable |
| standardHeaders | boolean | true | Include standard rate limit headers |
📚 Common Patterns
1. Different Limits per Route
// Strict limit for authentication
app.post('/api/login',
rateLimit({
redis,
limit: 5,
window: 300, // 5 requests per 5 minutes
keyPrefix: 'login',
message: 'Too many login attempts. Try again in 5 minutes.'
}),
loginHandler
);
// Moderate limit for API mutations
app.post('/api/posts',
authMiddleware,
rateLimit({
redis,
limit: 20,
window: 60, // 20 requests per minute
keyPrefix: 'create-post',
identifier: (req) => req.user?.id?.toString() || null
}),
createPostHandler
);
// Lenient limit for reads
app.get('/api/posts',
rateLimit({
redis,
limit: 100,
window: 60, // 100 requests per minute
keyPrefix: 'list-posts'
}),
listPostsHandler
);2. API Key-based Rate Limiting
app.use('/api', rateLimit({
redis,
limit: 1000,
window: 3600, // 1000 requests per hour
keyPrefix: 'api-key',
identifier: (req) => {
const apiKey = req.headers['x-api-key'] as string;
return apiKey || null; // Falls back to IP if no API key
}
}));3. Tiered Rate Limits (Free vs Premium)
app.use('/api', authMiddleware, (req, res, next) => {
const limiter = req.user?.isPremium
? rateLimit({
redis,
limit: 10000,
window: 3600,
keyPrefix: 'premium'
})
: rateLimit({
redis,
limit: 100,
window: 3600,
keyPrefix: 'free'
});
return limiter(req, res, next);
});4. Skip Rate Limiting for Admins
app.use('/api/admin', rateLimit({
redis,
limit: 100,
window: 60,
keyPrefix: 'admin',
skip: (req) => req.user?.role === 'admin' // Admins bypass rate limit
}));5. Cascading Rate Limits (Global + Route-specific)
// Global safety net
app.use('/api', rateLimit({
redis,
limit: 1000,
window: 3600,
keyPrefix: 'global'
}));
// Stricter limit on expensive operations
app.post('/api/ai/generate', rateLimit({
redis,
limit: 3,
window: 3600,
keyPrefix: 'ai-generation'
}), generateHandler);🔒 Fail Strategies
Fail-Open (Default, Recommended)
When Redis is unavailable, allow requests through.
rateLimit({
redis,
limit: 100,
window: 60,
keyPrefix: 'api',
failStrategy: 'open' // Default
});Best for: High-availability applications where downtime is unacceptable.
Fail-Closed (Strict Security)
When Redis is unavailable, block all requests.
rateLimit({
redis,
limit: 100,
window: 60,
keyPrefix: 'api',
failStrategy: 'closed'
});Best for: Security-critical endpoints where rate limiting must be enforced.
🏗️ Rate Limiting Algorithm
Fixed Window (Current Implementation)
The package uses a fixed window algorithm:
- Time is divided into fixed windows (e.g., 0-60s, 60-120s, etc.)
- Each window has an independent counter
- Counter resets at window boundaries
Implementation:
SET key 0 EX window NX // Initialize window if not exists
INCR key // Increment counter atomicallyCharacteristics:
- ✅ Simple and efficient
- ✅ Predictable behavior
- ✅ Low memory usage (one key per identifier)
- ⚠️ Potential for bursts at window boundaries (e.g., 100 requests at 59s, 100 more at 61s)
Why not sliding window?
- Sliding window requires storing individual request timestamps (higher memory)
- Fixed window is sufficient for 99% of use cases
- Can be mitigated with shorter windows if needed
📊 Response Headers
When standardHeaders: true (default), the middleware adds these headers:
Success Response
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1640995200Rate Limit Exceeded (429)
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
Retry-After: 45
{
"error": "Too many requests. Please try again later.",
"retryAfter": 45,
"limit": 100,
"remaining": 0,
"reset": 1640995200
}🧪 Advanced Usage
Manual Rate Limiting (Custom Frameworks)
import { createRateLimiter } from '@periodic/titanium';
const limiter = createRateLimiter({
redis,
limit: 100,
window: 60,
keyPrefix: 'api'
});
// In your custom middleware/framework
async function customHandler(req, res) {
const identifier = getUserId(req) || getIp(req);
const result = await limiter.limit(identifier);
if (!result.allowed) {
return res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: result.ttl
});
}
// Process request...
}Utility Methods
import { createRateLimiter } from '@periodic/titanium';
const limiter = createRateLimiter({ /* config */ });
// Check current status
const status = await limiter.getStatus('user-123');
console.log(status);
// { current: 45, ttl: 30 } or null
// Reset rate limit (useful for testing or admin tools)
await limiter.reset('user-123');🎨 Custom Logger Integration
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()]
});
app.use(rateLimit({
redis,
limit: 100,
window: 60,
keyPrefix: 'api',
logger: {
info: (...args) => logger.info(args.join(' ')),
warn: (...args) => logger.warn(args.join(' ')),
error: (...args) => logger.error(args.join(' '))
}
}));🔍 Monitoring & Debugging
Log Levels
INFO: Rate limit checks and warnings (80% threshold)
Rate limit check: identifier=ratelimit:api:192.168.1.1, count=45/100, allowed=true
Rate limit warning for identifier: ratelimit:api:192.168.1.1 - 18 remainingWARN: Rate limits exceeded, Redis failures
Rate limit exceeded for identifier: user-123 (create-post)
Redis unavailable with fail-open strategy - allowing requestERROR: Unexpected errors
Rate limiter error: Error: Connection refusedRecommended Metrics to Track
- Rate limit hits per endpoint (counter)
- Redis availability (gauge)
- 95th percentile remaining requests (histogram)
- Fail-open events (counter)
🛠️ Production Recommendations
Redis Configuration
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL, {
retryStrategy: (times) => {
if (times > 10) return null;
return Math.min(times * 100, 3000); // Exponential backoff
},
maxRetriesPerRequest: 3,
enableReadyCheck: true,
});
redis.on('error', (err) => console.error('Redis error:', err));
redis.on('reconnecting', () => console.log('Redis reconnecting...'));
redis.on('ready', () => console.log('Redis connected'));Rate Limit Guidelines
| Endpoint Type | Recommended Limit | Window | Reasoning | |---------------|-------------------|--------|-----------| | Authentication | 5-10 | 300s (5 min) | Prevent brute force | | Expensive AI/ML | 3-5 | 3600s (1 hour) | High compute cost | | Write Operations | 10-50 | 60s | Prevent spam | | Read Operations | 100-1000 | 60s | Allow browsing | | Global API | 1000-5000 | 3600s | Safety net |
Environment Variables
# .env
REDIS_URL=redis://localhost:6379
# Production (Redis Cloud)
REDIS_URL=redis://default:[email protected]:12345
# AWS ElastiCache
REDIS_URL=redis://your-cluster.cache.amazonaws.com:6379⚠️ Important Considerations
Redis Persistence
Rate limiting does not require Redis persistence (RDB/AOF). If Redis restarts, rate limits reset—which is acceptable for most applications.
If you need guaranteed limits across restarts, enable Redis persistence:
redis-server --appendonly yesHorizontal Scaling
This package works seamlessly across multiple app instances because:
- All state is stored in Redis
- Redis operations are atomic (INCR, SET NX)
- No coordination between instances needed
IPv6 Handling
The package automatically normalizes IPv6-mapped IPv4 addresses:
::ffff:192.168.1.1 → 192.168.1.1🚫 Explicit Non-Goals
This package intentionally does not include:
❌ Sliding window log algorithm (use fixed window with shorter intervals instead)
❌ Token bucket or leaky bucket algorithms (may be added in v2.x)
❌ Built-in Redis clustering logic (use Redis Cluster directly)
❌ Distributed tracing or metrics export (integrate with your APM)
❌ Framework auto-detection (explicit configuration only)
❌ In-memory fallback (defeats the purpose of distributed rate limiting)
If you need these features, consider:
- express-rate-limit for in-memory limiting
- rate-limiter-flexible for advanced algorithms
- Building a custom solution on top of the core
RateLimiterclass
🧩 Architecture
@periodic/titanium/
├── src/
│ ├── core/
│ │ ├── limiter.ts # Framework-agnostic rate limiter
│ │ └── types.ts # TypeScript interfaces
│ ├── adapters/
│ │ └── express.ts # Express middleware adapter
│ ├── utils/
│ │ └── ip.ts # IP extraction utilities
│ └── index.ts # Public API exportsDesign Philosophy:
- Core is pure TypeScript, no framework dependencies
- Adapters connect core to specific frameworks (Express, Fastify, etc.)
- Utils provide reusable helper functions
This allows you to:
- Use the core
RateLimiterdirectly in non-Express apps - Build custom adapters for other frameworks
- Test components independently
📖 API Reference
rateLimit(options)
Creates Express middleware for rate limiting.
Returns: (req, res, next) => Promise<void>
createRateLimiter(options)
Creates standalone rate limiter instance.
Returns: RateLimiter
RateLimiter Methods
limit(identifier: string): Promise<RateLimitResult>reset(identifier: string): Promise<boolean>getStatus(identifier: string): Promise<{ current, ttl } | null>
🤝 Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
MIT License - see LICENSE file for details.
🙏 Acknowledgments
- Inspired by express-rate-limit
- Built with production lessons from scaling APIs to millions of requests
📞 Support
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
- 📧 Email: [email protected]
Built with ❤️ for production-grade Node.js applications
