SaaS API Rate Limiting (Bun)
A complete example for a SaaS application with tiered rate limits based on subscription plans.
Uses @joint-ops/hitlimit-bun with Redis for distributed state across multiple Bun instances.
Scenario
- Free tier: 100 API requests/hour
- Pro tier: 5,000 API requests/hour
- Enterprise: Unlimited
Implementation
A Bun.serve implementation with tiered rate limiting, Redis-backed state, and custom JSON error responses:
import { hitlimit } from '@joint-ops/hitlimit-bun'
import { redisStore } from '@joint-ops/hitlimit-bun/stores/redis'
// Tier configuration from your pricing page
const TIER_LIMITS = {
free: { limit: 100, window: '1h' },
pro: { limit: 5000, window: '1h' },
enterprise: { limit: Infinity }
} as const
const apiLimiter = hitlimit({
store: redisStore({ url: Bun.env.REDIS_URL }),
tiers: TIER_LIMITS,
// Get tier from auth header/token
tier: (req) => {
return getUserTier(req) || 'free'
},
// Rate limit by workspace, not individual user
key: (req) => {
const user = getUser(req)
return user ? `workspace:${user.workspaceId}` : getIP(req)
},
// Custom response matching your API style
response: (info) => ({
error: 'RATE_LIMITED',
message: `Rate limit exceeded. Try again in ${Math.ceil(info.resetIn / 60)} minutes.`,
upgrade: 'https://example.com/pricing'
})
})
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url)
// Skip rate limiting for health checks
if (url.pathname === '/health') {
return new Response('OK')
}
const { allowed, remaining, reset, limit } = await apiLimiter.check(req)
const headers = {
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': String(remaining),
'X-RateLimit-Reset': String(reset),
'Content-Type': 'application/json'
}
if (!allowed) {
return Response.json(
{
error: 'RATE_LIMITED',
message: 'Rate limit exceeded',
upgrade: 'https://example.com/pricing'
},
{ status: 429, headers: { ...headers, 'Retry-After': String(reset) } }
)
}
// Route handling
if (url.pathname.startsWith('/api/projects')) {
return Response.json({ projects: [] }, { headers })
}
return Response.json({ status: 'ok' }, { headers })
}
}) Elysia Version
The same tiered SaaS rate limiting implemented with the Elysia framework:
import { Elysia } from 'elysia'
import { hitlimit } from '@joint-ops/hitlimit-bun/elysia'
import { redisStore } from '@joint-ops/hitlimit-bun/stores/redis'
const TIER_LIMITS = {
free: { limit: 100, window: '1h' },
pro: { limit: 5000, window: '1h' },
enterprise: { limit: Infinity }
} as const
const app = new Elysia()
.use(hitlimit({
store: redisStore({ url: Bun.env.REDIS_URL }),
tiers: TIER_LIMITS,
tier: ({ request }) => {
return getUserTier(request) || 'free'
},
key: ({ request }) => {
const user = getUser(request)
return user ? `workspace:${user.workspaceId}` : 'anonymous'
},
response: {
error: 'RATE_LIMITED',
message: 'Rate limit exceeded',
upgrade: 'https://example.com/pricing'
}
}))
.get('/api/projects', () => ({ projects: [] }))
.post('/api/projects', ({ body }) => ({ created: true }))
.get('/api/projects/:id', ({ params }) => ({ id: params.id }))
.listen(3000) Key Points
- Per-workspace limits: Rate limits are shared across all users in a workspace, preventing one power user from exhausting the team's quota
- Redis for distributed state: When running multiple Bun instances behind a load balancer, Redis ensures rate limit counters are shared across all nodes
- Bun.env for configuration: Uses
Bun.env.REDIS_URLinstead ofprocess.envfor Bun-native environment variable access - 100/hour for free tier: Enough for basic integrations, but encourages upgrade for heavy use
- 5,000/hour for pro: ~83 req/min — sufficient for most SaaS integrations
- Infinity for enterprise: No rate limit enforced, but requests are still tracked for analytics
- Custom JSON responses: Return structured error bodies that match your API convention, including an upgrade URL for monetization
- Health check skip: Always bypass rate limiting for health check endpoints used by load balancers and uptime monitors