# Scaling and multi-tenancy Design your Copilot SDK deployment to serve multiple users, handle concurrent sessions, and scale horizontally across infrastructure. This guide covers session isolation patterns, scaling topologies, and production best practices. **Best for:** Platform developers, SaaS builders, any deployment serving more than a handful of concurrent users. ## Core concepts Before choosing a pattern, understand three dimensions of scaling: ```mermaid flowchart TB subgraph Dimensions["Scaling Dimensions"] direction LR I["πŸ”’ Isolation
Who sees what?"] C["⚑ Concurrency
How many at once?"] P["πŸ’Ύ Persistence
How long do sessions live?"] end I --> I1["Shared CLI
vs. CLI per user"] C --> C1["Session pooling
vs. on-demand"] P --> P1["Ephemeral
vs. persistent"] style Dimensions fill:#0d1117,stroke:#58a6ff,color:#c9d1d9 ``` ## Session isolation patterns ### Pattern 1: isolated CLI per user Each user gets their own CLI server instance. Strongest isolationβ€”a user's sessions, memory, and processes are completely separated. ```mermaid flowchart TB LB["Load Balancer"] subgraph User_A["User A"] SDK_A["SDK Client"] --> CLI_A["CLI Server A
:4321"] CLI_A --> SA["πŸ“ Sessions A"] end subgraph User_B["User B"] SDK_B["SDK Client"] --> CLI_B["CLI Server B
:4322"] CLI_B --> SB["πŸ“ Sessions B"] end subgraph User_C["User C"] SDK_C["SDK Client"] --> CLI_C["CLI Server C
:4323"] CLI_C --> SC["πŸ“ Sessions C"] end LB --> SDK_A LB --> SDK_B LB --> SDK_C style User_A fill:#0d1117,stroke:#3fb950,color:#c9d1d9 style User_B fill:#0d1117,stroke:#3fb950,color:#c9d1d9 style User_C fill:#0d1117,stroke:#3fb950,color:#c9d1d9 ``` **When to use:** * Multi-tenant SaaS where data isolation is critical * Users with different auth credentials * Compliance requirements (SOC 2, HIPAA) ```typescript // CLI pool manager β€” one CLI per user class CLIPool { private instances = new Map(); private nextPort = 5000; async getClientForUser(userId: string, token?: string): Promise { if (this.instances.has(userId)) { return this.instances.get(userId)!.client; } const port = this.nextPort++; // Spawn a dedicated CLI for this user await spawnCLI(port, token); const client = new CopilotClient({ cliUrl: `localhost:${port}`, }); this.instances.set(userId, { client, port }); return client; } async releaseUser(userId: string): Promise { const instance = this.instances.get(userId); if (instance) { await instance.client.stop(); this.instances.delete(userId); } } } ``` ### Pattern 2: shared CLI with session isolation Multiple users share one CLI server but have isolated sessions via unique session IDs. Lighter on resources, but weaker isolation. ```mermaid flowchart TB U1["πŸ‘€ User A"] U2["πŸ‘€ User B"] U3["πŸ‘€ User C"] subgraph App["Your App"] Router["Session Router"] end subgraph CLI["Shared CLI Server :4321"] SA["Session: user-a-chat"] SB["Session: user-b-chat"] SC["Session: user-c-chat"] end U1 --> Router U2 --> Router U3 --> Router Router --> SA Router --> SB Router --> SC style App fill:#0d1117,stroke:#58a6ff,color:#c9d1d9 style CLI fill:#0d1117,stroke:#3fb950,color:#c9d1d9 ``` **When to use:** * Internal tools with trusted users * Resource-constrained environments * Lower isolation requirements ```typescript const sharedClient = new CopilotClient({ cliUrl: "localhost:4321", }); // Enforce session isolation through naming conventions function getSessionId(userId: string, purpose: string): string { return `${userId}-${purpose}-${Date.now()}`; } // Access control: ensure users can only access their own sessions async function resumeSessionWithAuth( sessionId: string, currentUserId: string ): Promise { const [sessionUserId] = sessionId.split("-"); if (sessionUserId !== currentUserId) { throw new Error("Access denied: session belongs to another user"); } return sharedClient.resumeSession(sessionId); } ``` ### Pattern 3: shared sessions (collaborative) Multiple users interact with the same sessionβ€”like a shared chat room with Copilot. ```mermaid flowchart TB U1["πŸ‘€ Alice"] U2["πŸ‘€ Bob"] U3["πŸ‘€ Carol"] subgraph App["Collaboration Layer"] Queue["Message Queue
(serialize access)"] Lock["Session Lock"] end subgraph CLI["CLI Server"] Session["Shared Session:
team-project-review"] end U1 --> Queue U2 --> Queue U3 --> Queue Queue --> Lock Lock --> Session style App fill:#0d1117,stroke:#58a6ff,color:#c9d1d9 style CLI fill:#0d1117,stroke:#3fb950,color:#c9d1d9 ``` **When to use:** * Team collaboration tools * Shared code review sessions * Pair programming assistants > ⚠️ **Important:** The SDK doesn't provide built-in session locking. You **must** serialize access to prevent concurrent writes to the same session. ```typescript import Redis from "ioredis"; const redis = new Redis(); async function withSessionLock( sessionId: string, fn: () => Promise, timeoutSec = 300 ): Promise { const lockKey = `session-lock:${sessionId}`; const lockId = crypto.randomUUID(); // Acquire lock const acquired = await redis.set(lockKey, lockId, "NX", "EX", timeoutSec); if (!acquired) { throw new Error("Session is in use by another user"); } try { return await fn(); } finally { // Release lock (only if we still own it) const currentLock = await redis.get(lockKey); if (currentLock === lockId) { await redis.del(lockKey); } } } // Usage: serialize access to shared session app.post("/team-chat", authMiddleware, async (req, res) => { const result = await withSessionLock("team-project-review", async () => { const session = await client.resumeSession("team-project-review"); return session.sendAndWait({ prompt: req.body.message }); }); res.json({ content: result?.data.content }); }); ``` ## Comparison of isolation patterns | | Isolated CLI Per User | Shared CLI + Session Isolation | Shared Sessions | |---|---|---|---| | **Isolation** | βœ… Complete | ⚠️ Logical | ❌ Shared | | **Resource usage** | High (CLI per user) | Low (one CLI) | Low (one CLI + session) | | **Complexity** | Medium | Low | High (locking) | | **Auth flexibility** | βœ… Per-user tokens | ⚠️ Service token | ⚠️ Service token | | **Best for** | Multi-tenant SaaS | Internal tools | Collaboration | ## Horizontal scaling ### Multiple CLI servers behind a load balancer ```mermaid flowchart TB Users["πŸ‘₯ Users"] --> LB["Load Balancer"] subgraph Pool["CLI Server Pool"] CLI1["CLI Server 1
:4321"] CLI2["CLI Server 2
:4322"] CLI3["CLI Server 3
:4323"] end subgraph Storage["Shared Storage"] NFS["πŸ“ Network File System
or Cloud Storage"] end LB --> CLI1 LB --> CLI2 LB --> CLI3 CLI1 --> NFS CLI2 --> NFS CLI3 --> NFS style Pool fill:#0d1117,stroke:#3fb950,color:#c9d1d9 style Storage fill:#161b22,stroke:#f0883e,color:#c9d1d9 ``` **Key requirement:** Session state must be on **shared storage** so any CLI server can resume any session. ```typescript // Route sessions to CLI servers class CLILoadBalancer { private servers: string[]; private currentIndex = 0; constructor(servers: string[]) { this.servers = servers; } // Round-robin selection getNextServer(): string { const server = this.servers[this.currentIndex]; this.currentIndex = (this.currentIndex + 1) % this.servers.length; return server; } // Sticky sessions: same user always hits same server getServerForUser(userId: string): string { const hash = this.hashCode(userId); return this.servers[hash % this.servers.length]; } private hashCode(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; } return Math.abs(hash); } } const lb = new CLILoadBalancer([ "cli-1:4321", "cli-2:4321", "cli-3:4321", ]); app.post("/chat", async (req, res) => { const server = lb.getServerForUser(req.user.id); const client = new CopilotClient({ cliUrl: server }); const session = await client.createSession({ sessionId: `user-${req.user.id}-chat`, model: "gpt-4.1", }); const response = await session.sendAndWait({ prompt: req.body.message }); res.json({ content: response?.data.content }); }); ``` ### Sticky sessions vs. shared storage ```mermaid flowchart LR subgraph Sticky["Sticky Sessions"] direction TB S1["User A β†’ always CLI 1"] S2["User B β†’ always CLI 2"] S3["βœ… No shared storage needed"] S4["❌ Uneven load if users vary"] end subgraph Shared["Shared Storage"] direction TB SH1["User A β†’ any CLI"] SH2["User B β†’ any CLI"] SH3["βœ… Even load distribution"] SH4["❌ Requires NFS / cloud storage"] end style Sticky fill:#0d1117,stroke:#58a6ff,color:#c9d1d9 style Shared fill:#0d1117,stroke:#3fb950,color:#c9d1d9 ``` **Sticky sessions** are simplerβ€”pin users to specific CLI servers. No shared storage needed, but load distribution is uneven. **Shared storage** enables any CLI to handle any session. Better load distribution, but requires networked storage for `~/.copilot/session-state/`. ## Vertical scaling ### Tuning a single CLI server A single CLI server can handle many concurrent sessions. Key considerations: ```mermaid flowchart TB subgraph Resources["Resource Dimensions"] CPU["πŸ”§ CPU
Model request processing"] MEM["πŸ’Ύ Memory
Active session state"] DISK["πŸ’Ώ Disk I/O
Session persistence"] NET["🌐 Network
API calls to provider"] end style Resources fill:#0d1117,stroke:#58a6ff,color:#c9d1d9 ``` **Session lifecycle management** is key to vertical scaling: ```typescript // Limit concurrent active sessions class SessionManager { private activeSessions = new Map(); private maxConcurrent: number; constructor(maxConcurrent = 50) { this.maxConcurrent = maxConcurrent; } async getSession(sessionId: string): Promise { // Return existing active session if (this.activeSessions.has(sessionId)) { return this.activeSessions.get(sessionId)!; } // Enforce concurrency limit if (this.activeSessions.size >= this.maxConcurrent) { await this.evictOldestSession(); } // Create or resume const session = await client.createSession({ sessionId, model: "gpt-4.1", }); this.activeSessions.set(sessionId, session); return session; } private async evictOldestSession(): Promise { const [oldestId] = this.activeSessions.keys(); const session = this.activeSessions.get(oldestId)!; // Session state is persisted automatically β€” safe to disconnect await session.disconnect(); this.activeSessions.delete(oldestId); } } ``` ## Ephemeral vs. persistent sessions ```mermaid flowchart LR subgraph Ephemeral["Ephemeral Sessions"] E1["Created per request"] E2["Destroyed after use"] E3["No state to manage"] E4["Good for: one-shot tasks,
stateless APIs"] end subgraph Persistent["Persistent Sessions"] P1["Named session ID"] P2["Survives restarts"] P3["Resumable"] P4["Good for: multi-turn chat,
long workflows"] end style Ephemeral fill:#0d1117,stroke:#58a6ff,color:#c9d1d9 style Persistent fill:#0d1117,stroke:#3fb950,color:#c9d1d9 ``` ### Ephemeral sessions For stateless API endpoints where each request is independent: ```typescript app.post("/api/analyze", async (req, res) => { const session = await client.createSession({ model: "gpt-4.1", }); try { const response = await session.sendAndWait({ prompt: req.body.prompt, }); res.json({ result: response?.data.content }); } finally { await session.disconnect(); // Clean up immediately } }); ``` ### Persistent sessions For conversational interfaces or long-running workflows: ```typescript // Create a resumable session app.post("/api/chat/start", async (req, res) => { const sessionId = `user-${req.user.id}-${Date.now()}`; const session = await client.createSession({ sessionId, model: "gpt-4.1", infiniteSessions: { enabled: true, backgroundCompactionThreshold: 0.80, }, }); res.json({ sessionId }); }); // Continue the conversation app.post("/api/chat/message", async (req, res) => { const session = await client.resumeSession(req.body.sessionId); const response = await session.sendAndWait({ prompt: req.body.message }); res.json({ content: response?.data.content }); }); // Clean up when done app.post("/api/chat/end", async (req, res) => { await client.deleteSession(req.body.sessionId); res.json({ success: true }); }); ``` ## Container deployments ### Kubernetes with persistent storage ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: copilot-cli spec: replicas: 3 selector: matchLabels: app: copilot-cli template: metadata: labels: app: copilot-cli spec: containers: - name: copilot-cli image: your-registry/copilot-cli:latest # See backend-services.md for how to build and push this image args: ["--headless", "--host", "0.0.0.0", "--port", "4321"] env: - name: COPILOT_GITHUB_TOKEN valueFrom: secretKeyRef: name: copilot-secrets key: github-token ports: - containerPort: 4321 volumeMounts: - name: session-state mountPath: /root/.copilot/session-state volumes: - name: session-state persistentVolumeClaim: claimName: copilot-sessions-pvc --- apiVersion: v1 kind: Service metadata: name: copilot-cli spec: selector: app: copilot-cli ports: - port: 4321 targetPort: 4321 ``` ```mermaid flowchart TB subgraph K8s["Kubernetes Cluster"] Svc["Service: copilot-cli:4321"] Pod1["Pod 1: CLI"] Pod2["Pod 2: CLI"] Pod3["Pod 3: CLI"] PVC["PersistentVolumeClaim
(shared session state)"] end App["Your App Pods"] --> Svc Svc --> Pod1 Svc --> Pod2 Svc --> Pod3 Pod1 --> PVC Pod2 --> PVC Pod3 --> PVC style K8s fill:#0d1117,stroke:#58a6ff,color:#c9d1d9 ``` ### Azure Container Instances ```yaml containers: - name: copilot-cli image: your-registry/copilot-cli:latest # See backend-services.md for how to build and push this image command: ["copilot", "--headless", "--host", "0.0.0.0", "--port", "4321"] volumeMounts: - name: session-storage mountPath: /root/.copilot/session-state volumes: - name: session-storage azureFile: shareName: copilot-sessions storageAccountName: myaccount ``` ## Production checklist ```mermaid flowchart TB subgraph Checklist["Production Readiness"] direction TB A["βœ… Session cleanup
cron / TTL"] B["βœ… Health checks
ping endpoint"] C["βœ… Persistent storage
for session state"] D["βœ… Secret management
for tokens/keys"] E["βœ… Monitoring
active sessions, latency"] F["βœ… Session locking
if shared sessions"] G["βœ… Graceful shutdown
drain active sessions"] end style Checklist fill:#0d1117,stroke:#3fb950,color:#c9d1d9 ``` | Concern | Recommendation | |---------|---------------| | **Session cleanup** | Run periodic cleanup to delete sessions older than your TTL | | **Health checks** | Ping the CLI server periodically; restart if unresponsive | | **Storage** | Mount persistent volumes for `~/.copilot/session-state/` | | **Secrets** | Use your platform's secret manager (Vault, K8s Secrets, etc.) | | **Monitoring** | Track active session count, response latency, error rates | | **Locking** | Use Redis or similar for shared session access | | **Shutdown** | Drain active sessions before stopping CLI servers | ## Limitations | Limitation | Details | |------------|---------| | **No built-in session locking** | Implement application-level locking for concurrent access | | **No built-in load balancing** | Use external LB or service mesh | | **Session state is file-based** | Requires shared filesystem for multi-server setups | | **30-minute idle timeout** | Sessions without activity are auto-cleaned by the CLI | | **CLI is single-process** | Scale by adding more CLI server instances, not threads | ## Next steps * **[Session Persistence](../features/session-persistence.md)**: Deep dive on resumable sessions * **[Backend Services](./backend-services.md)**: Core server-side setup * **[GitHub OAuth](./github-oauth.md)**: Multi-user authentication * **[BYOK](../auth/byok.md)**: Use your own model provider