-
Notifications
You must be signed in to change notification settings - Fork 0
Rate Limiting
This page documents the RDCP SDK's rate limiting capability, including configuration, standard headers (draft-7), and structured error details.
- Supports per-endpoint and per-tenant token-bucket limits.
- Emits standard RateLimit draft-7 headers when enabled (or legacy X-RateLimit-* headers when configured).
- Returns RDCP_RATE_LIMITED (429) with structured error details when a request is limited.
- Adapters (Express, Fastify, Koa) behave identically and clean up per-request rate events in a finally block.
Enable and configure rate limiting via RDCPServer options in your adapter setup.
import express from 'express'
import { adapters, auth } from '@rdcp.dev/server'
const app = express()
app.use(express.json())
app.use(
adapters.express.createRDCPMiddleware({
authenticator: auth.validateRDCPAuth,
capabilities: {
rateLimit: {
enabled: true,
headers: true, // emit standard RateLimit headers
headersMode: 'draft-7', // 'draft-7' | 'x'
defaultRule: { windowMs: 60000, maxRequests: 120 },
perEndpoint: { // optional endpoint-specific rules
control: { windowMs: 10000, maxRequests: 10 },
status: { windowMs: 500, maxRequests: 5 },
},
perTenant: { // optional tenant-specific rules
'tenant-A': { windowMs: 60000, maxRequests: 30 },
},
},
},
})
)
app.listen(3000)Notes:
- headers: true enables header emission. headersMode selects between standard draft-7 and legacy X-RateLimit-*.
- The token-bucket limiter refills continuously over the window, enforcing an average rate.
When headers are enabled in 'draft-7' mode, responses include:
- RateLimit: e.g. "limit=10, remaining=7, reset=30"
- RateLimit-Policy: e.g. "10;w=60"
- RateLimit-Remaining: numeric remaining tokens
- RateLimit-Reset: epoch seconds when the window resets
- Retry-After: seconds until retry is permitted (when the request was limited)
Legacy mode ('x') uses:
- X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, plus Retry-After on limited responses.
Example (viewing headers on discovery):
curl -i \
-H 'X-RDCP-Auth-Method: api-key' \
-H 'X-RDCP-Client-ID: demo-client' \
-H 'Authorization: Bearer dev-key-change-in-production-min-32-chars' \
http://localhost:3000/.well-known/rdcp | grep -i 'ratelimit\|retry-after'When a request is rate limited, the server returns 429 with:
{
"error": {
"code": "RDCP_RATE_LIMITED",
"message": "Control rate limited. Retry after 300ms",
"protocol": "rdcp/1.0",
"timestamp": "2025-01-01T00:00:00.000Z",
"details": {
"limit": 10,
"remaining": 0,
"reset": 1737072000,
"retryAfterSec": 1,
"policy": "10;w=60",
"requestId": "b8e6f..."
}
}
}Field meanings:
- limit: configured token capacity in the window.
- remaining: tokens remaining after the current request attempt (0 when limited).
- reset: epoch seconds for when the limiter is considered reset.
- retryAfterSec: seconds to wait before retrying (optional, present on limited responses).
- policy: human-readable policy string ";w=".
- requestId: correlation id, present when available.
- Express, Fastify, and Koa adapters:
- Emit identical RateLimit headers when enabled (per headersMode).
- Return the same RDCP_RATE_LIMITED structure.
- Always echo a correlation id as X-Request-Id in responses. If clients supply X-RDCP-Request-ID (UUID), it is echoed; otherwise a UUID is generated. Invalid IDs return RDCP_REQUEST_ID_INVALID (400).
- Clean up per-request rate limiter state in a finally block to prevent leaks on all code paths.
Here's a complete server implementation with comprehensive rate limiting:
// server.js - Complete rate limiting implementation
import express from 'express'
import { adapters, auth } from '@rdcp.dev/server'
const app = express()
app.use(express.json())
// Production-ready rate limiting configuration
const rdcpMiddleware = adapters.express.createRDCPMiddleware({
authenticator: auth.validateRDCPAuth,
capabilities: {
rateLimit: {
enabled: true,
headers: true,
headersMode: 'draft-7', // Use standard headers
// Global default: 120 requests per minute
defaultRule: {
windowMs: 60000, // 1 minute window
maxRequests: 120 // 120 requests max
},
// Endpoint-specific limits
perEndpoint: {
// Control endpoint: stricter limits
control: {
windowMs: 60000, // 1 minute window
maxRequests: 30 // Only 30 control operations per minute
},
// Status endpoint: more lenient for monitoring
status: {
windowMs: 60000, // 1 minute window
maxRequests: 600 // 600 status checks per minute
},
// Discovery: very permissive
discovery: {
windowMs: 60000, // 1 minute window
maxRequests: 1000 // 1000 discovery calls per minute
}
},
// Tenant-specific limits (organization isolation)
perTenant: {
// Premium tenant: higher limits
'tenant-premium-001': {
windowMs: 60000, // 1 minute window
maxRequests: 300 // 300 requests per minute
},
// Basic tenant: standard limits
'tenant-basic-002': {
windowMs: 60000, // 1 minute window
maxRequests: 60 // 60 requests per minute
},
// Free tier: very restrictive
'tenant-free-003': {
windowMs: 60000, // 1 minute window
maxRequests: 10 // Only 10 requests per minute
}
}
},
// Enable audit to track rate limiting events
audit: {
enabled: true,
sink: 'console',
sampleRate: 1.0 // Log all rate limit events
}
}
})
app.use(rdcpMiddleware)
// Demo route to show rate limiting in action
app.get('/api/demo', (req, res) => {
res.json({
message: 'This route is not rate limited by RDCP',
timestamp: new Date().toISOString()
})
})
app.listen(3000, () => {
console.log('π RDCP server with rate limiting running on port 3000')
console.log('\nπ Rate Limits Configured:')
console.log(' β’ Global default: 120 req/min')
console.log(' β’ Control endpoint: 30 req/min')
console.log(' β’ Status endpoint: 600 req/min')
console.log(' β’ Premium tenants: 300 req/min')
console.log(' β’ Free tenants: 10 req/min')
})// rate-limit-client.js - Client that handles rate limits gracefully
class RateLimitedRDCPClient {
constructor(baseUrl, apiKey, clientId = 'rate-limited-client') {
this.baseUrl = baseUrl
this.apiKey = apiKey
this.clientId = clientId
this.requestQueue = []
this.isProcessing = false
}
async makeRequest(endpoint, options = {}) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
'X-API-Key': this.apiKey,
'X-RDCP-Auth-Method': 'api-key',
'X-RDCP-Client-ID': this.clientId,
'X-RDCP-Request-ID': crypto.randomUUID(),
...options.headers
}
})
// Log rate limit headers for monitoring
const rateLimitHeaders = this.extractRateLimitHeaders(response)
if (rateLimitHeaders.limit) {
console.log('π Rate Limit Status:', {
endpoint: endpoint,
limit: rateLimitHeaders.limit,
remaining: rateLimitHeaders.remaining,
reset: new Date(rateLimitHeaders.reset * 1000).toISOString(),
policy: rateLimitHeaders.policy
})
}
// Handle rate limiting
if (response.status === 429) {
const errorData = await response.json()
const retryAfterSec = errorData.error.details?.retryAfterSec || 60
console.warn(`π¦ Rate limited! Waiting ${retryAfterSec}s before retry...`)
// Wait for the specified retry period
await new Promise(resolve => setTimeout(resolve, retryAfterSec * 1000))
// Retry the request
return this.makeRequest(endpoint, options)
}
if (!response.ok) {
const errorData = await response.json()
throw new Error(`RDCP Error: ${errorData.error?.code} - ${errorData.error?.message}`)
}
return response.json()
}
extractRateLimitHeaders(response) {
return {
limit: response.headers.get('RateLimit-Limit'),
remaining: response.headers.get('RateLimit-Remaining'),
reset: response.headers.get('RateLimit-Reset'),
policy: response.headers.get('RateLimit-Policy'),
retryAfter: response.headers.get('Retry-After')
}
}
// Queue-based request management to prevent overwhelming the server
async queuedRequest(endpoint, options = {}) {
return new Promise((resolve, reject) => {
this.requestQueue.push({ endpoint, options, resolve, reject })
this.processQueue()
})
}
async processQueue() {
if (this.isProcessing || this.requestQueue.length === 0) return
this.isProcessing = true
while (this.requestQueue.length > 0) {
const { endpoint, options, resolve, reject } = this.requestQueue.shift()
try {
const result = await this.makeRequest(endpoint, options)
resolve(result)
// Small delay between requests to avoid rapid bursts
await new Promise(resolve => setTimeout(resolve, 100))
} catch (error) {
reject(error)
}
}
this.isProcessing = false
}
// High-level methods
async getStatus() {
return this.queuedRequest('/rdcp/v1/status')
}
async enableCategories(categories) {
return this.queuedRequest('/rdcp/v1/control', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'enable',
categories: categories
})
})
}
}
// Usage demonstration
async function demonstrateRateLimit() {
const client = new RateLimitedRDCPClient(
'http://localhost:3000',
'dev-key-change-in-production-min-32-chars'
)
console.log('π§ͺ Testing rate limit handling...')
// Make rapid requests to trigger rate limiting
const promises = []
for (let i = 0; i < 50; i++) {
promises.push(
client.enableCategories(['DATABASE']).catch(err => {
console.log(`Request ${i} failed:`, err.message)
})
)
}
await Promise.allSettled(promises)
console.log('β
Rate limit test completed')
}
// Run demo
if (import.meta.url === new URL(process.argv[1], 'file://').href) {
demonstrateRateLimit().catch(console.error)
}// tenant-rate-limit-demo.js - Show per-tenant rate limiting
async function testTenantRateLimit() {
const tenants = [
{ id: 'tenant-premium-001', limit: 300, name: 'Premium' },
{ id: 'tenant-basic-002', limit: 60, name: 'Basic' },
{ id: 'tenant-free-003', limit: 10, name: 'Free' }
]
for (const tenant of tenants) {
console.log(`\nπ’ Testing ${tenant.name} tenant (${tenant.id}):`)
const requests = []
const startTime = Date.now()
// Make requests up to the tenant's limit + 5 more
for (let i = 0; i < tenant.limit + 5; i++) {
requests.push(
fetch('http://localhost:3000/rdcp/v1/status', {
headers: {
'X-API-Key': 'dev-key-change-in-production-min-32-chars',
'X-RDCP-Auth-Method': 'api-key',
'X-RDCP-Client-ID': `${tenant.id}-client`,
'X-RDCP-Tenant-ID': tenant.id
}
}).then(response => ({
status: response.status,
headers: {
remaining: response.headers.get('RateLimit-Remaining'),
reset: response.headers.get('RateLimit-Reset')
}
}))
)
}
const results = await Promise.allSettled(requests)
const successful = results.filter(r => r.value?.status === 200).length
const rateLimited = results.filter(r => r.value?.status === 429).length
console.log(` β
Successful requests: ${successful}`)
console.log(` π¦ Rate limited requests: ${rateLimited}`)
console.log(` β±οΈ Total time: ${Date.now() - startTime}ms`)
}
}
// Run tenant demo
if (import.meta.url === new URL(process.argv[1], 'file://').href) {
testTenantRateLimit().catch(console.error)
}# First request - should succeed with rate limit headers
curl -i \
-H 'X-RDCP-Auth-Method: api-key' \
-H 'X-RDCP-Client-ID: test-client' \
-H 'X-API-Key: dev-key-change-in-production-min-32-chars' \
http://localhost:3000/.well-known/rdcp | grep -i ratelimit# Make rapid control requests to trigger rate limiting
for i in {1..35}; do
echo "Request $i:"
curl -s -w 'Status: %{http_code}\n' \
-X POST \
-H 'X-RDCP-Auth-Method: api-key' \
-H 'X-RDCP-Client-ID: burst-test' \
-H 'X-API-Key: dev-key-change-in-production-min-32-chars' \
-H 'Content-Type: application/json' \
-d '{"action":"enable","categories":["DATABASE"]}' \
http://localhost:3000/rdcp/v1/control
sleep 0.1
done# Script to monitor rate limit consumption
#!/bin/bash
echo "Monitoring rate limit headers..."
for i in {1..10}; do
echo "\n--- Request $i ---"
curl -s -i \
-H 'X-RDCP-Auth-Method: api-key' \
-H 'X-RDCP-Client-ID: monitor-test' \
-H 'X-API-Key: dev-key-change-in-production-min-32-chars' \
http://localhost:3000/rdcp/v1/status | grep -E '(RateLimit|X-Request-Id)'
sleep 1
doneSee also:
- Error-Responses.md (RDCP_RATE_LIMITED details)
- Basic-Usage.md (capabilities configuration)
- Monitoring & Metrics (Monitoring.md) β /status measured metrics and Prometheus /metrics
Getting Started: Installation β’ Basic Usage β’ Authentication
Migration: From Manual Implementation β’ Framework Examples β’ Publishing Guide
Protocol: RDCP v1.0 Specification β’ Implementation Guide β’ API Reference
π Home | π¦ NPM Package | π GitHub | π Issues
RDCP SDK v1.0.0 - Runtime Debug Control Protocol implementation for JavaScript/Node.js applications
- Application-Control-Plane-Concepts
- rdcp-technical-analysis
- Migration-Guide
- Multi-Tenancy
- Audit-Trail
- Performance Metrics
- Implementation-Status
- JavaScript-vs-TypeScript-Boundaries
- Core-Package-Boundaries
- Publishing-Setup
- Contributing
- API-Reference
- Client-Fetch-API-Examples
- Tracing-Library-Integration-Examples
- Integration-Scenarios
- Trace-Propagation-Demo
- RDCP-Demo-App
- Conformance-Kit
- Conformance-Setup
- Conformance-Tags
- Conformance-Reports
- Conformance-Requirements
- Conformance-CLI
- Protocol Specification
- Implementation Guide
- RDCP-Primitive-Types
- Protocol-Schemas
- Protocol-Error-Codes
- API-Reference
Version: 1.0.0
Protocol: RDCP v1.0
License: Apache-2.0