Skip to content

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.

ConcernSingle instanceMultiple instances
SessionsIn-memory or local SQLiteRedis or Postgres
Event busInProcessAdapterBullMQ or custom bus
CacheIn-memoryRedis
File uploadsLocal diskS3 or shared volume
WebSocket pub/subBun native (in-process)Redis transport

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
},
});

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:

app.config.ts (excerpt)
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 EventEnvelope survives transport, so SSE and webhooks see the same scope/exposure metadata everywhere

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:

app.config.ts (excerpt)
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 message
export 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.

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.

In-memory rate limiting doesn’t scale. Switch the rate limiter to Redis:

app.config.ts (excerpt)
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:

app.config.ts (excerpt)
export default defineApp({
db: {
redis: process.env.REDIS_URL!,
},
cache: {
store: 'redis',
},
});
app.config.ts
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' })],
});

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.