Skip to content

Rate Limiting

Rate limiting protects your app from abusive clients, runaway scripts, and brute-force attacks. Slingshot ships a tiered rate limiter that runs at the framework level and an extension point for per-route or per-feature limits.

The framework runs a global rate limiter as middleware before your routes. Each request is keyed by client IP (or a custom function) and counted against a sliding window. When the bucket is full, the request is rejected with 429 Too Many Requests and a Retry-After header.

Defaults: 100 requests per 60 seconds per IP, in-memory store.

app.config.ts
import { defineApp } from '@lastshotlabs/slingshot';
export default defineApp({
security: {
rateLimit: {
windowMs: 60_000, // 1 minute window
max: 100, // 100 requests per window
store: 'memory', // 'memory' | 'redis'
fingerprintLimit: false, // see Bot Protection
},
},
});

For multi-instance deployments, use the Redis store so all replicas share the same counter:

security: {
rateLimit: { store: 'redis', windowMs: 60_000, max: 100 },
},
db: { redis: process.env.REDIS_URL },
security: {
rateLimit: false, // explicitly disable — useful for tests or trusted internal endpoints
},

Apply rateLimit() middleware to a single route or domain when the global limit is too broad:

src/billing/package.ts
import { definePackage, domain, rateLimit, route } from '@lastshotlabs/slingshot';
export const billingPackage = definePackage({
name: 'billing',
middleware: {
strictLimit: rateLimit({ windowMs: 60_000, max: 10 }),
},
domains: [
domain({
name: 'webhooks',
basePath: '/webhooks',
routes: [
route.post({
path: '/stripe',
middleware: ['strictLimit'],
handler: async ({ respond }) => respond.json({ received: true }),
}),
],
}),
],
});

By default, requests are keyed by client IP. For routes that need protection beyond IP (e.g., auth endpoints where one IP can mask many bots), enable fingerprint hashing:

// @skip-typecheck
import { rateLimit } from '@lastshotlabs/slingshot';
const authLimit = rateLimit({
windowMs: 60_000,
max: 5,
fingerprintLimit: true, // hash request fingerprint into the bucket key
});

Fingerprint hashing combines client IP, user-agent, and other request headers into a single bucket key so a single client juggling user agents still hits the same bucket.

The middleware sets standard headers on every response:

HeaderMeaning
X-RateLimit-LimitMax requests in the current window
X-RateLimit-RemainingRequests left before the limiter fires
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait (only on 429)

Clients that handle these headers gracefully can self-throttle before the limiter rejects.

If your app sits behind a load balancer or CDN, configure trustProxy so the limiter keys on the real client IP rather than the proxy’s address:

security: {
trustProxy: 1, // trust 1 proxy hop — Cloudflare, ALB, etc.
rateLimit: { windowMs: 60_000, max: 100 },
},

trustProxy accepts a number (hops to trust), false (no trust — use socket IP), or a predicate for custom logic.

The default response when the bucket is empty:

HTTP/1.1 429 Too Many Requests
Retry-After: 32
Content-Type: application/json
{
"error": "Too many requests",
"retryAfter": 32
}

Override the body via the request error formatter (see Exception Handling).

Bot protection is a separate but related feature. It runs before the rate limiter and maintains a CIDR blocklist plus fingerprint-based heuristics:

security: {
botProtection: {
enabled: true,
blocklistCidrs: ['10.0.0.0/8', '203.0.113.0/24'],
fingerprintWindow: 300_000,
fingerprintMax: 50,
},
rateLimit: { windowMs: 60_000, max: 100 },
},

Blocklisted IPs and high-fingerprint matches are rejected with 403 Forbidden before ever reaching the rate limiter — saving counter slots for legitimate traffic.