Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4790601
feat(rate-limiter): initialize rate-limiter app
CSenshi Dec 29, 2025
27788b5
feat(rate-limiter): initialize rate-limiter app
CSenshi Dec 29, 2025
b91ffd4
fix(tests): ensure tests run serially to avoid Redis state interference
CSenshi Dec 29, 2025
6db6693
feat(rate-limiter): load Lua script from file for fixed window algorithm
CSenshi Dec 29, 2025
b4727ac
feat(rate-limiter): implement Sliding Window Log algorithm
CSenshi Dec 31, 2025
f1df290
feat(rate-limiter): add Sliding Window Counter algorithm implementation
CSenshi Dec 31, 2025
61863d0
refactor(rate-limiter): simplify time retrieval in lua algorithms
CSenshi Dec 31, 2025
ab6a7a0
feat(rate-limiter): implement Token Bucket algorithm
CSenshi Jan 1, 2026
79ea540
feat(rate-limiter): implement base interface for rate limiting algori…
CSenshi Jan 3, 2026
5af89e6
feat(rate-limiter): add RuleManagerService
CSenshi Jan 3, 2026
995f1fb
feat(rate-limiter): add RateLimitExceededException
CSenshi Jan 3, 2026
3da9d3e
feat(rate-limiter): add RateLimit decorator
CSenshi Jan 3, 2026
5d0290d
feat(rate-limiter): implement algorithms/rules manager service
CSenshi Jan 3, 2026
6fd8e80
feat(rate-limiter): add RateLimiterService
CSenshi Jan 3, 2026
1847dbd
feat(rate-limiter): implement ClientIdentifierService
CSenshi Jan 3, 2026
2cdeb5c
feat(rate-limiter): add RateLimitGuard and update types
CSenshi Jan 4, 2026
dff12e2
feat(rate-limiter): add RateLimitController
CSenshi Jan 4, 2026
ce1f638
feat(rate-limiter): reimplement lua scripts to be in ts files
CSenshi Jan 4, 2026
c7fbc6b
refactor(rate-limiter): update tests to not interfere with each other
CSenshi Jan 4, 2026
dc35bd1
feat(rate-limiter): add integration tests for rate limiter service
CSenshi Jan 4, 2026
602c7e9
feat(rate-limiter): add e2e tests
CSenshi Jan 4, 2026
1627b7a
feat(rate-limiter): add README
CSenshi Jan 4, 2026
b0087cf
refactor(rate-limiter): change directory structure
CSenshi Jan 4, 2026
41a046e
fix(rate-limiter): correct rate limit header values and format
CSenshi Jan 4, 2026
4518c52
feat(rate-limiter): migrate to Redis Cluster and refactor algorithms …
CSenshi Jan 4, 2026
c59c371
docs(rate-limiter): fix invalid path
CSenshi Jan 4, 2026
43022d6
fix(rate-limiter): fix lock.file
CSenshi Jan 4, 2026
038a366
chore(rate-limiter): update docker compose to use a single-node redis…
CSenshi Jan 5, 2026
50c8ca3
fix(rate-limiter): ensure testMatch includes only spec files
CSenshi Jan 7, 2026
423a55f
feat(rate-limiter): refactor Redis module initialization to use async…
CSenshi Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The goal of this project is to serve as an educational resource for developers p

- [**URL Shortener**](apps/url-shortener)
- [**Web Crawler**](apps/web-crawler)
- [**Rate Limiter**](apps/rate-limiter)

## 🛠 Planned Implementations

Expand Down
1 change: 1 addition & 0 deletions apps/rate-limiter/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REDIS_HOST=redis://localhost:6379
22 changes: 22 additions & 0 deletions apps/rate-limiter/.spec.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"jsc": {
"target": "es2017",
"parser": {
"syntax": "typescript",
"decorators": true,
"dynamicImport": true
},
"transform": {
"decoratorMetadata": true,
"legacyDecorator": true
},
"keepClassNames": true,
"externalHelpers": true,
"loose": true
},
"module": {
"type": "es6"
},
"sourceMaps": true,
"exclude": []
}
155 changes: 155 additions & 0 deletions apps/rate-limiter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# 🚦 Rate Limiter

> **Production-ready rate limiting service with multiple algorithm implementations**

## Key Components

### Algorithms

- **Fixed Window Algorithm**
[`src/rate-limiter/algorithms/fixed-window`](src/rate-limiter/algorithms/fixed-window/fixed-window.script.ts)

- **Sliding Window Log Algorithm**
[`src/rate-limiter/algorithms/sliding-window-log`](src/rate-limiter/algorithms/sliding-window-log/sliding-window-log.script.ts)

- **Sliding Window Counter Algorithm**
[`src/rate-limiter/algorithms/sliding-window-counter`](src/rate-limiter/algorithms/sliding-window-counter/sliding-window-counter.script.ts)

- **Token Bucket Algorithm**
[`src/rate-limiter/algorithms/token-bucket`](src/rate-limiter/algorithms/token-bucket/token-bucket.script.ts)

[![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![NestJS](https://img.shields.io/badge/NestJS-E0234E?style=flat&logo=nestjs&logoColor=white)](https://nestjs.com/)
[![Redis](https://img.shields.io/badge/Redis-DC382D?style=flat&logo=redis&logoColor=white)](https://redis.io/)
[![Docker](https://img.shields.io/badge/Docker-2496ED?style=flat&logo=docker&logoColor=white)](https://www.docker.com/)

## ✨ **Features**

### **Core Functionality**

- ✅ **Multiple Algorithm Support** – Fixed Window, Sliding Window Log, Sliding Window Counter, and Token Bucket
- ✅ **Flexible Rule Configuration** – Define multiple rate limit rules with different algorithms and limits
- ✅ **Client Identification** – Supports user ID, API key and IP address-based identification
- ✅ **Redis-Based Storage** – High-performance rate limit tracking using Redis Lua scripts
- ✅ **Decorator-Based API** – Easy-to-use `@RateLimit()` decorator for route protection
- ✅ **Standard Rate Limit Headers** – Returns `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers

## 🛠️ **Tech Stack**

### **Backend**

- **Framework**: [NestJS](https://nestjs.com/) - Progressive Node.js framework
- **Language**: [TypeScript](https://www.typescriptlang.org/)

### **Data Layer**

- **Storage**: [Redis](https://redis.io/) - High-performance in-memory data store
- **Scripts**: Redis Lua scripts for atomic rate limit operations

### **Development & DevOps**

- **Package Manager**: [pnpm](https://pnpm.io/) - Fast, efficient package management
- **Testing**: [Jest](https://jestjs.io/) - Comprehensive test suite
- **Logging**: Built-in NestJS logging

## 📦 **Installation & Setup**

### **Prerequisites**

- Node.js 22+
- Docker & Docker Compose (for Redis)
- pnpm

### **Quick Start**

```bash
# 1. Clone the repository (Needed only 1st time)
git clone https://github.com/CSenshi/system-craft.git
cd system-craft

# 2. Install dependencies (Needed only 1st time)
pnpm install

# 3. Copy env file (Needed only 1st time)
cp apps/rate-limiter/.env.example apps/rate-limiter/.env

# 4. Start required services (Needed only 1st time)
pnpm nx run @apps/rate-limiter:infra:up

# 5. Start the development server
pnpm nx run @apps/rate-limiter:serve
```

## 🎯 **Rate Limiting Algorithms**

### **1. Fixed Window**

**How it works:** Divides time into fixed intervals (e.g., 1-minute windows). Each window has its own counter that resets at the start of the next window. Requests increment the counter for the current window.

- **Time**: O(1) - Single Redis INCR operation
- **Space**: O(1) per client - One counter per time window
- **Accuracy**: Low - Allows boundary bursts (2N requests possible at window transitions)
- **Best For**: High-throughput scenarios where exact accuracy isn't critical

### **2. Sliding Window Log**

**How it works:** Stores a timestamp for each request in a hash. When checking limits, counts only requests within the last W seconds by removing expired entries. Most precise but requires storing all request timestamps.

- **Time**: O(1)\* - Redis `HLEN` maintains count internally
- **Space**: O(N) per client - Stores one entry per request in the window
- **Accuracy**: High - Precisely enforces "N requests in last W seconds" with no boundary bursts
- **Best For**: Low to medium traffic where accuracy is critical

### **3. Sliding Window Counter**

**How it works:** Uses weighted counters from current and previous time periods. Calculates approximate count using formula:

```
weight = (remainingTime/windowSize)
count = previousCount * weight + currentCount
```

Cloudflare-style approach that balances accuracy and efficiency.

- **Time**: O(1) - Fixed hash operations (2 periods)
- **Space**: O(1) per client - Stores counters for current and previous periods only
- **Accuracy**: Medium - Good approximation with minimal deviation from true count
- **Best For**: Production systems needing good accuracy with high performance

### **4. Token Bucket**

**How it works:** Maintains a bucket with tokens (up to capacity). Tokens refill at a constant rate (limit/windowSeconds per second). Each request consumes one token. Allows bursts up to bucket capacity while maintaining average rate.

- **Time**: O(1) - Fixed hash operations for token refill and consumption
- **Space**: O(1) per client - Stores token count and last refill timestamp
- **Accuracy**: Medium - Enforces average rate over time, not strict "N requests in last W seconds"
- **Best For**: Traffic shaping, APIs that benefit from burst allowance

### **Algorithm Comparison**

| Algorithm | Time (per client) | Space (per client) | Accuracy | Burst Handling |
| -------------------------- | ----------------- | ------------------ | -------- | ------------------ |
| **Fixed Window** | O(1) | O(1) | Low | Poor (edge bursts) |
| **Sliding Window Log** | O(1) (Amortized) | O(N) | High | None |
| **Sliding Window Counter** | O(1) | O(1) | Medium | Limited |
| **Token Bucket** | O(1) | O(1) | Medium | Excellent |

> Notes:
>
> - `Accuracy` - how precisely the algorithm enforces “X requests per time window”
> - `Burst handling` - ability to temporarily allow traffic spikes while still enforcing long-term limits

## 🧪 **Testing**

### **Run Tests**

```bash
# Unit tests
pnpm nx test @apps/rate-limiter

# Integration tests
pnpm nx test:int @apps/rate-limiter

# E2E tests
pnpm nx e2e @e2e/rate-limiter
```
12 changes: 12 additions & 0 deletions apps/rate-limiter/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
redis-single-node-cluster:
image: docker.io/bitnamilegacy/redis-cluster:8.0
environment:
- 'ALLOW_EMPTY_PASSWORD=yes'
- 'REDIS_CLUSTER_REPLICAS=0'
- 'REDIS_NODES=127.0.0.1 127.0.0.1 127.0.0.1'
- 'REDIS_CLUSTER_CREATOR=yes'
- 'REDIS_CLUSTER_DYNAMIC_IPS=no'
- 'REDIS_CLUSTER_ANNOUNCE_IP=127.0.0.1'
ports:
- '6379:6379'
3 changes: 3 additions & 0 deletions apps/rate-limiter/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import baseConfig from '../../eslint.config.mjs';

export default [...baseConfig];
22 changes: 22 additions & 0 deletions apps/rate-limiter/jest.config.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* eslint-disable */
const { readFileSync } = require('fs');

// Reading the SWC compilation config for the spec files
const swcJestConfig = JSON.parse(
readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8'),
);

// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves
swcJestConfig.swcrc = false;

module.exports = {
displayName: '@apps/rate-limiter',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['@swc/jest', swcJestConfig],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: 'test-output/jest/coverage',
testMatch: ['**/*.spec.ts', '!**/*.int.spec.ts'],
};
21 changes: 21 additions & 0 deletions apps/rate-limiter/jest.int.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { readFileSync } from 'fs';

// Reading the SWC compilation config for the spec files
const swcJestConfig = JSON.parse(
readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8'),
);

// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves
swcJestConfig.swcrc = false;

export default {
displayName: '@apps/rate-limiter',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['@swc/jest', swcJestConfig],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: 'test-output/jest/coverage',
testMatch: ['**/*.int.spec.ts'],
};
113 changes: 113 additions & 0 deletions apps/rate-limiter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{
"name": "@apps/rate-limiter",
"version": "0.0.1",
"private": true,
"nx": {
"targets": {
"build": {
"executor": "nx:run-commands",
"options": {
"command": "webpack-cli build",
"args": [
"--node-env=production"
],
"cwd": "apps/rate-limiter"
},
"configurations": {
"development": {
"args": [
"--node-env=development"
]
}
}
},
"prune-lockfile": {
"dependsOn": [
"build"
],
"cache": true,
"executor": "@nx/js:prune-lockfile",
"outputs": [
"{workspaceRoot}/apps/rate-limiter/dist/package.json",
"{workspaceRoot}/apps/rate-limiter/dist/pnpm-lock.yaml"
],
"options": {
"buildTarget": "build"
}
},
"copy-workspace-modules": {
"dependsOn": [
"build"
],
"cache": true,
"outputs": [
"{workspaceRoot}/apps/rate-limiter/dist/workspace_modules"
],
"executor": "@nx/js:copy-workspace-modules",
"options": {
"buildTarget": "build"
}
},
"prune": {
"dependsOn": [
"prune-lockfile",
"copy-workspace-modules"
],
"executor": "nx:noop"
},
"serve": {
"continuous": true,
"executor": "@nx/js:node",
"defaultConfiguration": "development",
"dependsOn": [
"build"
],
"options": {
"buildTarget": "@apps/rate-limiter:build",
"runBuildTargetDependencies": false
},
"configurations": {
"development": {
"buildTarget": "@apps/rate-limiter:build:development"
},
"production": {
"buildTarget": "@apps/rate-limiter:build:production"
}
}
},
"test": {
"options": {
"passWithNoTests": true
}
},
"test:int": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "apps/rate-limiter/jest.int.config.ts",
"passWithNoTests": true
}
},
"infra:up": {
"executor": "nx:run-commands",
"options": {
"command": "docker compose -f docker-compose.yml up -d",
"cwd": "apps/rate-limiter"
}
},
"infra:down": {
"executor": "nx:run-commands",
"options": {
"command": "docker compose -f docker-compose.yml down",
"cwd": "apps/rate-limiter"
}
}
}
},
"dependencies": {
"@nestjs-redis/kit": "0.13.2",
"redis": "^5.10.0"
},
"devDependencies": {
"@nestjs/testing": "^11.0.0"
}
}
7 changes: 7 additions & 0 deletions apps/rate-limiter/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { RateLimiterModule } from './rate-limiter/rate-limiter.module';

@Module({
imports: [RateLimiterModule],
})
export class AppModule {}
20 changes: 20 additions & 0 deletions apps/rate-limiter/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* This is not a production server yet!
* This is only a minimal backend to get started.
*/
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
const port = process.env.PORT || 3000;
await app.listen(port);
Logger.log(
`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`,
);
}

bootstrap();
2 changes: 2 additions & 0 deletions apps/rate-limiter/src/rate-limiter/algorithms/base/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './rate-limit-algorithm.interface';
export * from './redis-script.helper';
Loading