DEV Community

Wesley Israel
Wesley Israel

Posted on

Implementing a Leaky Bucket Rate Limiting System in Node.js:

๐Ÿ“‹ Introduction

Recently, I faced a fascinating technical challenge: implementing a complete rate limiting system using the Leaky Bucket strategy in Node.js. The project involved creating an HTTP server with authentication, GraphQL for PIX queries, and a multi-tenant system with granular token control.

In this article, I'll share my complete implementation journey, from initial architecture to final testing, including all technical decisions and challenges faced.

๐ŸŽฏ The Challenge

The goal was to build a system that:

  • โœ… Node.js HTTP server with Koa.js and TypeScript
  • โœ… Multi-tenancy strategy (each user with their own bucket)
  • โœ… Bearer Token authentication (JWT)
  • โœ… GraphQL mutation for PIX query
  • โœ… Leaky Bucket strategy for token control
  • โœ… Complete tests with Jest
  • โœ… Postman documentation
  • โœ… Load testing with k6

๐Ÿ—๏ธ System Architecture

Project Structure

leaky-bucket/ โ”œโ”€โ”€ src/ โ”‚ โ”œโ”€โ”€ middleware/ # Authentication and rate limiting middlewares โ”‚ โ”œโ”€โ”€ services/ # Business logic (auth, leaky bucket, PIX) โ”‚ โ”œโ”€โ”€ graphql/ # GraphQL schema and resolvers โ”‚ โ”œโ”€โ”€ models/ # Data models โ”‚ โ”œโ”€โ”€ utils/ # Utilities (JWT, logger, metrics) โ”‚ โ””โ”€โ”€ server.ts # Main server โ”œโ”€โ”€ tests/ # Jest and load tests โ”œโ”€โ”€ docs/ # Documentation and Postman โ””โ”€โ”€ scripts/ # Automation scripts 
Enter fullscreen mode Exit fullscreen mode

Data Flow

Client โ†’ Auth Middleware โ†’ Leaky Bucket โ†’ GraphQL Resolver โ†’ PIX Service 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”ง Detailed Implementation

1. Initial Configuration

I started by configuring the environment with TypeScript and necessary dependencies:

{ "dependencies": { "koa": "^2.14.2", "koa-router": "^12.0.0", "graphql": "^16.8.1", "apollo-server-koa": "^3.12.1", "jsonwebtoken": "^9.0.2", "bcryptjs": "^2.4.3" } } 
Enter fullscreen mode Exit fullscreen mode

2. Multi-Tenancy Strategy

I implemented a system where each user has their own token bucket:

// src/models/user.ts interface User { id: string; email: string; name: string; password: string; tokens: number; maxTokens: number; lastRefill: Date; } class UserRepository { private users: Map<string, User> = new Map(); createUser(email: string, password: string, name: string): User { const user: User = { id: generateId(), email, name, password: hashPassword(password), tokens: 10, // Initial tokens maxTokens: 10, lastRefill: new Date(), }; this.users.set(user.id, user); return user; } } 
Enter fullscreen mode Exit fullscreen mode

3. JWT Authentication

The authentication middleware validates tokens and extracts user information:

// src/middleware/auth.ts export const authMiddleware = async (ctx: Context, next: Next) => { const authHeader = ctx.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { throw new UnauthorizedError('Authentication token required'); } const token = authHeader.substring(7); const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload; ctx.state.user = decoded; await next(); }; 
Enter fullscreen mode Exit fullscreen mode

4. Leaky Bucket Strategy

The Leaky Bucket implementation was the heart of the system:

// src/services/leakyBucketService.ts export class LeakyBucketService { private readonly LEAK_RATE = 1; // tokens per second private readonly REFILL_INTERVAL = 1000; // 1 second async consumeToken(userId: string): Promise<boolean> { const user = await this.userRepository.findById(userId); if (!user) throw new Error('User not found'); // Calculate leaked tokens since last refill const now = new Date(); const timeDiff = now.getTime() - user.lastRefill.getTime(); const leakedTokens = Math.floor(timeDiff / this.REFILL_INTERVAL) * this.LEAK_RATE; // Update available tokens const newTokens = Math.min(user.maxTokens, user.tokens + leakedTokens); const newLastRefill = new Date( now.getTime() - (timeDiff % this.REFILL_INTERVAL) ); if (newTokens < 1) { return false; // No tokens available } // Consume one token await this.userRepository.updateTokens( userId, newTokens - 1, newLastRefill ); return true; } } 
Enter fullscreen mode Exit fullscreen mode

5. GraphQL for PIX Query

I implemented a complete GraphQL schema:

// src/graphql/types.ts const typeDefs = gql` type User { id: ID! email: String! name: String! } type PixQueryResult { success: Boolean! pixKey: String! value: Float! accountHolder: String! accountType: String! bankName: String! message: String! } type TokenStatus { tokens: Int! maxTokens: Int! lastRefill: String! } type Query { tokenStatus: TokenStatus! } type Mutation { register(email: String!, password: String!, name: String!): AuthResult! login(email: String!, password: String!): AuthResult! queryPixKey(pixKey: String!, value: Float!): PixQueryResult! } `; 
Enter fullscreen mode Exit fullscreen mode

6. Rate Limiting Middleware

The middleware integrates Leaky Bucket with requests:

// src/middleware/leakyBucket.ts export const leakyBucketMiddleware = async (ctx: Context, next: Next) => { const user = ctx.state.user; const hasToken = await leakyBucketService.consumeToken(user.userId); if (!hasToken) { ctx.status = 429; // Too Many Requests ctx.body = { error: 'Rate limit exceeded', message: 'You have exceeded the request limit. Try again in a few seconds.', }; return; } await next(); }; 
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช Comprehensive Testing

Unit Tests with Jest

I implemented 68 tests covering all scenarios:

// src/__tests__/leakyBucket.test.ts describe('LeakyBucketService', () => { test('should consume token when available', async () => { const service = new LeakyBucketService(); const result = await service.consumeToken('user1'); expect(result).toBe(true); }); test('should reject when no tokens', async () => { // Consume all tokens for (let i = 0; i < 10; i++) { await service.consumeToken('user1'); } const result = await service.consumeToken('user1'); expect(result).toBe(false); }); }); 
Enter fullscreen mode Exit fullscreen mode

Load Testing with k6

I created load testing scripts to validate behavior under stress:

// tests/load/stress-test.js import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '2m', target: 100 }, // Ramp up { duration: '5m', target: 100 }, // Constant load { duration: '2m', target: 0 }, // Ramp down ], }; export default function () { const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; const response = http.post( 'http://localhost:3000/graphql', { query: `mutation QueryPixKey($pixKey: String!, $value: Float!) { queryPixKey(pixKey: $pixKey, value: $value) { success pixKey value accountHolder } }`, variables: { pixKey: 'test@example.com', value: 100.5 }, }, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, } ); check(response, { 'status is 200': r => r.status === 200, 'rate limited when needed': r => r.status === 429 || r.status === 200, }); sleep(1); } 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“š Complete Documentation

Postman Collection

I created a complete collection with all endpoints:

{ "info": { "name": "Leaky Bucket API", "description": "Complete API with authentication and rate limiting" }, "item": [ { "name": "Auth", "item": [ { "name": "Register", "request": { "method": "POST", "url": "http://localhost:3000/graphql", "body": { "mode": "raw", "raw": "{\"query\":\"mutation Register($email: String!, $password: String!, $name: String!) { register(email: $email, password: $password, name: $name) { success token user { id email name } } }\",\"variables\":{\"email\":\"test@example.com\",\"password\":\"password123\",\"name\":\"Test User\"}}", "options": { "raw": { "language": "json" } } } } } ] } ] } 
Enter fullscreen mode Exit fullscreen mode

Detailed README

I documented the entire project with:

  • ๐Ÿ“– Installation and configuration guide
  • ๐Ÿ—๏ธ Architecture and data flow
  • ๐Ÿ”ง Environment configuration
  • ๐Ÿงช How to run tests
  • ๐Ÿ“Š Metrics and monitoring
  • ๐Ÿ”’ Security considerations

๐Ÿš€ Deployment and Configuration

Automation Scripts

I created scripts to facilitate development:

#!/bin/bash # scripts/setup-env.sh echo "๐Ÿš€ Setting up Leaky Bucket environment..." # Create .env file if it doesn't exist if [ ! -f .env ]; then cat > .env << EOF # Server Configuration PORT=3000 NODE_ENV=development # JWT JWT_SECRET=your-super-secret-jwt-key-change-in-production JWT_EXPIRES_IN=24h # Leaky Bucket INITIAL_TOKENS=10 MAX_TOKENS=10 LEAK_RATE=1 REFILL_INTERVAL=1000 # Logging LOG_LEVEL=info # Tests TEST_USER_EMAIL=test@example.com TEST_USER_PASSWORD=password123 TEST_USER_NAME=Test User EOF  echo "โœ… .env file created successfully!" else echo "โ„น๏ธ .env file already exists" fi echo "๐ŸŽ‰ Environment configured! Run 'npm run dev' to start the server" 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Š Results and Metrics

Test Performance

  • 68 Jest tests: โœ… All passing
  • Load tests: โœ… Rate limiting working
  • Response time: < 100ms for normal requests
  • Throughput: 1000+ req/s with active rate limiting

Feature Coverage

  • โœ… 100% of mandatory requirements implemented
  • โœ… 93% of bonus requirements implemented
  • โœ… 100% of tests passing
  • โœ… 100% of documentation complete

๐Ÿ” Challenges and Solutions

1. Token Synchronization

Challenge: Ensuring tokens are consumed atomically in multi-thread environment.

Solution: Implemented a user-based locking system using Map and atomic operations.

2. Environment Configuration

Challenge: Maintaining consistent configurations between development and production.

Solution: Created environment variable system with validation and default values.

3. Load Testing

Challenge: Simulating realistic load and validating rate limiting.

Solution: Implemented k6 tests with different scenarios (spike, stress, load).

๐ŸŽฏ Lessons Learned

  1. Modular Architecture: Clear separation of responsibilities facilitated testing and maintenance
  2. Comprehensive Testing: Unit + integration + load tests are essential
  3. Documentation: Documenting from the beginning saves time in the future
  4. Configuration: Automating environment setup improves DX
  5. Rate Limiting: Leaky Bucket is more efficient than Token Bucket for specific use cases

๐Ÿ”ฎ Next Steps

To evolve the system, I would consider:

  • ๐Ÿ”„ Implement Redis for token persistence
  • ๐Ÿ“ˆ Add metrics with Prometheus
  • ๐Ÿ” Implement refresh tokens
  • ๐ŸŒ Add IP-based rate limiting
  • ๐Ÿ“ฑ Create React + Relay frontend

๐Ÿ“ Conclusion

This project demonstrated the importance of well-thought architecture, comprehensive testing, and complete documentation. The resulting system is robust, scalable, and production-ready.

The Leaky Bucket strategy proved effective for rate limiting, and GraphQL integration provided a flexible and well-documented API.

Complete code: GitHub Repository


Liked the article? Leave a โค๏ธ and share with other developers!

Top comments (0)