Horizontal Scaling
Slingshot’s HTTP layer is stateless. Add instances freely - but shared storage for sessions, a shared event bus for app events, and a shared WebSocket transport are required so activity on one instance reaches clients on another.
What needs to be shared
Section titled “What needs to be shared”| Concern | Single instance | Multiple instances |
|---|---|---|
| Sessions | In-memory or local SQLite | Redis or Postgres |
| Event bus | InProcessAdapter | BullMQ or custom bus |
| Cache | In-memory | Redis |
| File uploads | Local disk | S3 or shared volume |
| WebSocket pub/sub | Bun native (in-process) | Redis transport |
Sessions and storage
Section titled “Sessions and storage”Switch auth sessions to Redis. Each instance reads and writes the same session store:
createAuthPlugin({ auth: { roles: ['user'], defaultRole: 'user' }, db: { auth: 'postgres', // durable records in Postgres sessions: 'redis', // sessions shared via Redis oauthState: 'redis', // OAuth flow state shared via Redis },});Event bus
Section titled “Event bus”The default InProcessAdapter is process-local. Events emitted on instance A never reach instance B.
Switch to a shared event bus so app events cross instances. The built-in option is BullMQ:
import { defineApp } from '@lastshotlabs/slingshot';import { createBullMQAdapter } from '@lastshotlabs/slingshot-bullmq';
export default defineApp({ eventBus: createBullMQAdapter({ connection: { host: process.env.REDIS_HOST ?? '127.0.0.1', port: 6379 }, prefix: 'slingshot:events', attempts: 5, }),});With a shared event bus:
- SSE clients on instance A receive events emitted on instance B
- plugin event subscriptions still fire on every instance
- the canonical
EventEnvelopesurvives transport, so SSE and webhooks see the same scope/exposure metadata everywhere
WebSockets across instances
Section titled “WebSockets across instances”Bun’s native ws.publish() is in-process only. WebSocket room fan-out uses ws.transport, not the app event bus. For multi-instance room delivery, configure the Redis WebSocket transport:
import { createRedisTransport, defineApp } from '@lastshotlabs/slingshot';
export default defineApp({ ws: { transport: createRedisTransport({ connection: process.env.REDIS_URL ?? 'redis://127.0.0.1:6379', channelPrefix: 'ws:room:', }), endpoints: { '/ws/chat': { presence: true }, }, },});Once ws.transport is configured, Slingshot bridges room pub/sub through Redis:
// Client connected to instance A sends a messageexport async function wsMessage(ws: ServerWebSocket<{ roomId: string }>, msg: string | Buffer) { // publish goes to Redis -> received by all instances -> forwarded to subscribers on each ws.publish(ws.data.roomId, msg);}Clients connected to instance B, subscribed to the same room, receive the message.
Load balancer config
Section titled “Load balancer config”For sticky sessions, configure session affinity on your load balancer so WebSocket connections stay on the same instance. Not required with a Redis WebSocket transport, but it reduces Redis traffic.
For ECS, the generated sst.config.ts sets up ALB with health check routing. For EC2, the Caddyfile handles round-robin or least-connections load balancing.
Rate limiting across instances
Section titled “Rate limiting across instances”In-memory rate limiting doesn’t scale. Switch the rate limiter to Redis:
export default defineApp({ security: { rateLimit: { windowMs: 900_000, max: 100, store: 'redis', }, },});With Redis-backed rate limiting, a client making 50 requests on instance A and 50 on instance B is correctly throttled at 100 total.
Switch the cache adapter to Redis so all instances share the same cache:
export default defineApp({ db: { redis: process.env.REDIS_URL!, }, cache: { store: 'redis', },});Full multi-instance config
Section titled “Full multi-instance config”import { createRedisTransport, defineApp } from '@lastshotlabs/slingshot';import { createAuthPlugin } from '@lastshotlabs/slingshot-auth';import { createBullMQAdapter } from '@lastshotlabs/slingshot-bullmq';import { createCommunityPackage } from '@lastshotlabs/slingshot-community';
export default defineApp({ port: 3000, db: { postgres: process.env.DATABASE_URL!, redis: process.env.REDIS_URL!, }, eventBus: createBullMQAdapter({ connection: { host: process.env.REDIS_HOST ?? '127.0.0.1', port: 6379 }, }), ws: { transport: createRedisTransport({ connection: process.env.REDIS_URL!, }), endpoints: { '/ws/chat': { presence: true }, }, }, security: { signing: { secret: process.env.SIGNING_SECRET! }, rateLimit: { windowMs: 900_000, max: 100, store: 'redis', }, }, plugins: [ createAuthPlugin({ auth: { roles: ['user'], defaultRole: 'user' }, db: { auth: 'postgres', sessions: 'redis', oauthState: 'redis', }, }), ], packages: [createCommunityPackage({ containerCreation: 'user' })],});Scaling in production
Section titled “Scaling in production”With slingshot-infra, set scaling targets in slingshot.infra.ts:
export default defineInfra({ stacks: ['main'], size: 'medium',
scaling: { min: 2, // always at least 2 instances for availability max: 10, // scale up to 10 under load targetCpuPercent: 60, },
uses: ['db', 'cache'], // DATABASE_URL and REDIS_URL auto-wired});ECS auto-scales on CPU. Traffic spikes spin up new instances; idle periods scale them back down.
See also
Section titled “See also”- Custom Event Bus - BullMQ and custom event bus implementations
- WebSockets - WebSocket rooms and pub/sub
- Deployment - slingshot-infra scaling config