Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

- **🔥 Blazing Fast**: Built on Bun - up to 4x faster than Node.js alternatives
- **🎯 Zero Config**: Works out of the box with sensible defaults
- **🧠 Smart Load Balancing**: 5 load balancing algorithms: `round-robin`, `least-connections`, `random`, `weighted`, `ip-hash`
- **🧠 Smart Load Balancing**: Multiple algorithms: `round-robin`, `least-connections`, `random`, `weighted`, `ip-hash`, `p2c` (power-of-two-choices), `latency`, `weighted-least-connections`
- **🛡️ Production Ready**: Circuit breakers, health checks, and auto-failover
- **🔐 Built-in Authentication**: JWT, API keys, JWKS, and OAuth2 support out of the box
- **🎨 Developer Friendly**: Full TypeScript support with intuitive APIs
Expand Down Expand Up @@ -107,6 +107,9 @@ console.log('🚀 Bungate running on http://localhost:3000')
- **Least Connections**: Route to the least busy server
- **IP Hash**: Consistent routing based on client IP for session affinity
- **Random**: Randomized distribution for even load
- **Power of Two Choices (p2c)**: Pick the better of two random targets by load/latency
- **Latency**: Prefer the target with the lowest average response time
- **Weighted Least Connections**: Prefer targets with fewer connections normalized by weight
- **Sticky Sessions**: Session affinity with cookie-based persistence

### 🛡️ **Reliability & Resilience**
Expand Down
2 changes: 1 addition & 1 deletion examples/echo-server-1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const server = serve({

// Echo endpoint - return request details

// Add random latency delay (0-500ms)
// Add random latency delay (0-200ms)
const delay = Math.floor(Math.random() * 200)
await new Promise((resolve) => setTimeout(resolve, delay))

Expand Down
133 changes: 126 additions & 7 deletions examples/lb-example-all-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const gateway = new BunGateway({
})

// =============================================================================
// 1. ROUND ROBIN LOAD BALANCER WITH HEALTH CHECKS
// ROUND ROBIN LOAD BALANCER WITH HEALTH CHECKS
// =============================================================================
console.log('\n1️⃣ Round Robin with Health Checks')

Expand Down Expand Up @@ -83,7 +83,7 @@ gateway.addRoute({
})

// =============================================================================
// 2. WEIGHTED LOAD BALANCER FOR HIGH-PERFORMANCE SCENARIOS
// WEIGHTED LOAD BALANCER FOR HIGH-PERFORMANCE SCENARIOS
// =============================================================================
console.log('2️⃣ Weighted Load Balancer (Performance Optimized)')

Expand Down Expand Up @@ -151,7 +151,7 @@ gateway.addRoute({
})

// =============================================================================
// 4. IP HASH FOR SESSION AFFINITY
// IP HASH FOR SESSION AFFINITY
// =============================================================================
console.log('4️⃣ IP Hash for Session Affinity')

Expand Down Expand Up @@ -190,7 +190,7 @@ gateway.addRoute({
})

// =============================================================================
// 5. RANDOM STRATEGY WITH ADVANCED ERROR HANDLING
// RANDOM STRATEGY WITH ADVANCED ERROR HANDLING
// =============================================================================
console.log('5️⃣ Random Strategy with Advanced Error Handling')

Expand Down Expand Up @@ -296,7 +296,118 @@ gateway.addRoute({
})

// =============================================================================
// 7. MONITORING AND METRICS ENDPOINT
// POWER OF TWO CHOICES (P2C) STRATEGY
// =============================================================================
console.log('6️⃣➕ Power of Two Choices (P2C) Strategy')

gateway.addRoute({
pattern: '/api/p2c/*',
loadBalancer: {
strategy: 'p2c',
targets: [
{ url: 'http://localhost:8080' },
{ url: 'http://localhost:8081' },
],
healthCheck: {
enabled: true,
interval: 10000,
timeout: 4000,
path: '/',
expectedStatus: 200,
},
},
proxy: {
pathRewrite: (path) => path.replace('/api/p2c', ''),
},
hooks: {
beforeRequest: async (req) => {
logger.debug(`🎲 P2C routing for: ${req.url}`)
},
},
})

// Alias for P2C
gateway.addRoute({
pattern: '/api/power-of-two/*',
loadBalancer: {
strategy: 'power-of-two-choices',
targets: [
{ url: 'http://localhost:8080' },
{ url: 'http://localhost:8081' },
],
healthCheck: {
enabled: true,
interval: 10000,
timeout: 4000,
path: '/',
expectedStatus: 200,
},
},
proxy: {
pathRewrite: (path) => path.replace('/api/power-of-two', ''),
},
})

// =============================================================================
// LATENCY-BASED STRATEGY
// =============================================================================
console.log('6️⃣✅ Latency-based Strategy')

gateway.addRoute({
pattern: '/api/latency/*',
loadBalancer: {
strategy: 'latency',
targets: [
{ url: 'http://localhost:8080' },
{ url: 'http://localhost:8081' },
],
healthCheck: {
enabled: true,
interval: 8000,
timeout: 3000,
path: '/',
expectedStatus: 200,
},
},
proxy: {
pathRewrite: (path) => path.replace('/api/latency', ''),
timeout: 10000,
},
hooks: {
afterResponse: async (req, res) => {
logger.info(`⏱️ Latency strategy served with ${res.status}`)
},
},
})

// =============================================================================
// WEIGHTED LEAST CONNECTIONS
// =============================================================================
console.log('6️⃣🔢 Weighted Least Connections Strategy')

gateway.addRoute({
pattern: '/api/wlc/*',
loadBalancer: {
strategy: 'weighted-least-connections',
targets: [
{ url: 'http://localhost:8080', weight: 3 },
{ url: 'http://localhost:8081', weight: 1 },
],
healthCheck: {
enabled: true,
interval: 12000,
timeout: 4000,
path: '/',
expectedStatus: 200,
},
},
proxy: {
pathRewrite: (path) => path.replace('/api/wlc', ''),
},
})

// =============================================================================
// MONITORING AND METRICS ENDPOINT
// =============================================================================
console.log('7️⃣ Monitoring and Metrics')

Expand Down Expand Up @@ -327,7 +438,7 @@ gateway.addRoute({
})

// =============================================================================
// 8. HEALTH CHECK ENDPOINT
// HEALTH CHECK ENDPOINT
// =============================================================================
gateway.addRoute({
pattern: '/health',
Expand All @@ -348,7 +459,7 @@ gateway.addRoute({
})

// =============================================================================
// 9. DEMO ENDPOINTS FOR TESTING
// DEMO ENDPOINTS FOR TESTING
// =============================================================================
gateway.addRoute({
pattern: '/demo',
Expand All @@ -359,6 +470,10 @@ gateway.addRoute({
'Least Connections': '/api/least-connections/get',
'IP Hash': '/api/ip-hash/get',
Random: '/api/random/get',
'Power of Two Choices (P2C)': '/api/p2c/get',
'P2C Alias (power-of-two-choices)': '/api/power-of-two/get',
'Latency-based': '/api/latency/get',
'Weighted Least Connections': '/api/wlc/get',
'Users Service': '/api/users/1',
'Posts Service': '/api/posts/1',
Metrics: '/metrics',
Expand Down Expand Up @@ -430,6 +545,10 @@ try {
console.log(' • Least Connections: /api/least-connections/*')
console.log(' • IP Hash: /api/ip-hash/*')
console.log(' • Random: /api/random/*')
console.log(' • Power of Two Choices (P2C): /api/p2c/*')
console.log(' • P2C Alias (power-of-two-choices): /api/power-of-two/*')
console.log(' • Latency-based: /api/latency/*')
console.log(' • Weighted Least Connections: /api/wlc/*')
console.log(' • Users Service: /api/users/*')
console.log(' • Posts Service: /api/posts/*')

Expand Down
28 changes: 26 additions & 2 deletions src/gateway/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ export class BunGateway implements Gateway {
}
}

// Measure end-to-end time to update latency metrics in the load balancer
const startedAt = Date.now()
increaseTargetConnectionsIfLeastConnections(
route.loadBalancer?.strategy,
target,
Expand All @@ -347,12 +349,22 @@ export class BunGateway implements Gateway {
route.loadBalancer?.strategy,
target,
)
// Update latency stats for strategies like 'latency' and as tie-breakers
try {
const duration = Date.now() - startedAt
loadBalancer.recordResponse(target.url, duration, false)
} catch {}
},
onError: (req: Request, error: Error) => {
decreaseTargetConnectionsIfLeastConnections(
route.loadBalancer?.strategy,
target,
)
// Record error with latency to penalize target appropriately
try {
const duration = Date.now() - startedAt
loadBalancer.recordResponse(target.url, duration, true)
} catch {}
if (route.hooks?.onError) {
route.hooks.onError!(req, error)
}
Expand Down Expand Up @@ -490,7 +502,13 @@ function increaseTargetConnectionsIfLeastConnections(
strategy: string | undefined,
target: any,
): void {
if (strategy === 'least-connections' && target.connections !== undefined) {
if (
(strategy === 'least-connections' ||
strategy === 'weighted-least-connections' ||
strategy === 'p2c' ||
strategy === 'power-of-two-choices') &&
target.connections !== undefined
) {
target.connections++
}
}
Expand All @@ -499,7 +517,13 @@ function decreaseTargetConnectionsIfLeastConnections(
strategy: string | undefined,
target: any,
): void {
if (strategy === 'least-connections' && target.connections !== undefined) {
if (
(strategy === 'least-connections' ||
strategy === 'weighted-least-connections' ||
strategy === 'p2c' ||
strategy === 'power-of-two-choices') &&
target.connections !== undefined
) {
target.connections--
}
}
9 changes: 9 additions & 0 deletions src/interfaces/load-balancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export interface LoadBalancerConfig {
| 'random' // Randomly selects a target
| 'weighted' // Uses target weights for distribution
| 'ip-hash' // Routes based on client IP hash for session affinity
| 'p2c' // Power of two choices: pick best of two random targets
| 'power-of-two-choices' // Alias for p2c
| 'latency' // Chooses target with the lowest avg response time
| 'weighted-least-connections' // Least connections normalized by weight

/**
* List of backend targets to load balance across
Expand Down Expand Up @@ -113,6 +117,11 @@ export interface LoadBalancerConfig {
* @example 'OK' or 'healthy'
*/
expectedBody?: string
/**
* HTTP method to use for health checks
* @default 'GET'
*/
method?: 'GET' | 'HEAD'
}

/**
Expand Down
Loading