Max Sessions Per User Limit
Status: ❌ Not Implemented Priority: 🟢 LOW Estimated Time: 4-6 hours Risk Level: LOW Impact: Resource exhaustion prevention and abuse protection
Problem Statement
Users can currently create unlimited concurrent MCP sessions, with each session consuming memory and Redis storage. While individual sessions are bounded by TTL (24 hours default), there's no per-user limit to prevent resource exhaustion or abuse.
Current Behavior:
- No limit on concurrent sessions per user
- Each session consumes ~50KB memory + Redis storage
- Malicious or buggy clients could create thousands of sessions
- No protection against resource exhaustion attacks
Risk Scenarios:
Malicious abuse:
- Attacker creates 10,000 sessions → 500MB memory + Redis storage
- Impacts service availability for legitimate users
Buggy client:
- Client fails to reuse sessions, creates new session every minute
- Over 24 hours: 1,440 sessions per user
- 1,000 users: 1.44 million sessions
Resource exhaustion:
- No per-user fairness guarantees
- Single user can consume disproportionate resources
Proposed Solution
Implement configurable per-user session limit with automatic eviction of oldest sessions.
Configuration
# Maximum concurrent sessions per user
SESSION_MAX_PER_USER=10
# Eviction policy: oldest | least_recently_used
SESSION_EVICTION_POLICY=least_recently_usedImplementation
Location: src/mcp/mcp.ts
// src/services/session-store.ts
export interface SessionMetadata {
sessionId: string;
userId: string;
createdAt: number;
lastAccessedAt: number;
expiresAt: number;
}
/**
* Get all sessions for a user
*/
export async function getUserSessions(userId: string): Promise<SessionMetadata[]> {
const redis = getRedisClient();
const keys = await redis.keys(`${SESSION_KEY_PREFIX}${userId}:*`);
const sessions: SessionMetadata[] = [];
for (const key of keys) {
const data = await redis.get(key);
if (data) {
sessions.push(JSON.parse(data));
}
}
return sessions;
}
/**
* Remove session from both Redis and in-memory transports
*/
export async function removeSession(sessionId: string): Promise<void> {
const sessionStore = getSessionStore();
const metadata = await sessionStore.get(sessionId);
if (metadata) {
// Remove from Redis
await sessionStore.delete(sessionId);
// Remove from in-memory transports
const { removeTransport } = await import("../mcp/mcp.js");
await removeTransport(sessionId);
}
}// src/mcp/mcp.ts - Session creation with limit enforcement
mcpRouter.post("/", async (req: Request, res: Response): Promise<void> => {
// ... existing authentication logic ...
// Check session limit before creating new session
const maxSessionsPerUser = config.session.maxPerUser || 10;
const userSessions = await getUserSessions(user.sub);
if (userSessions.length >= maxSessionsPerUser) {
// Evict oldest or least recently used session
const evictionPolicy = config.session.evictionPolicy || "least_recently_used";
let sessionToEvict: SessionMetadata;
if (evictionPolicy === "oldest") {
// Evict session with earliest createdAt
sessionToEvict = userSessions.sort((a, b) => a.createdAt - b.createdAt)[0];
} else {
// Evict session with earliest lastAccessedAt (LRU)
sessionToEvict = userSessions.sort((a, b) =>
a.lastAccessedAt - b.lastAccessedAt
)[0];
}
logger.info("Evicting session due to per-user limit", {
userId: user.sub,
evictedSessionId: sessionToEvict.sessionId,
policy: evictionPolicy,
limit: maxSessionsPerUser,
});
await removeSession(sessionToEvict.sessionId);
// Increment eviction metric
sessionEvictions.inc({
reason: "max_sessions_exceeded",
policy: evictionPolicy,
});
}
// ... proceed with session creation ...
});Prometheus Metrics
// src/services/metrics.ts
export const sessionEvictions = new Counter({
name: "session_evictions_total",
help: "Total session evictions",
labelNames: ["reason", "policy"], // max_sessions_exceeded, oldest/lru
registers: [register],
});
export const sessionsPerUser = new Histogram({
name: "sessions_per_user",
help: "Distribution of sessions per user",
buckets: [1, 2, 5, 10, 20, 50],
registers: [register],
});Alternative Approaches
Option 1: Reject New Sessions (Not Recommended)
Approach: Return error when limit reached, don't create new session.
if (userSessions.length >= maxSessionsPerUser) {
return res.status(429).json({
jsonrpc: "2.0",
error: {
code: -32001,
message: "Too many sessions",
data: {
reason: "max_sessions_exceeded",
details: `Maximum ${maxSessionsPerUser} concurrent sessions allowed`,
currentSessions: userSessions.length,
},
},
id: null,
});
}Pros:
- Simple implementation
- User explicitly notified of limit
Cons:
- Poor user experience (connection refused)
- Requires user to manually clean up old sessions
- Breaks "just works" principle
Option 2: LRU Eviction with Warning (Recommended)
Approach: Automatically evict least recently used session, include warning in response.
if (userSessions.length >= maxSessionsPerUser) {
const evicted = await evictLeastRecentlyUsed(user.sub);
logger.warn("Auto-evicted session", {
userId: user.sub,
evictedSessionId: evicted.sessionId,
lastAccessed: new Date(evicted.lastAccessedAt).toISOString(),
});
// Add warning header
res.setHeader("X-Session-Evicted", evicted.sessionId);
res.setHeader("X-Session-Eviction-Reason", "max_sessions_exceeded");
}Pros:
- Transparent to user (connection succeeds)
- Automatic cleanup of inactive sessions
- Good user experience
Cons:
- May disconnect active session unexpectedly
- Requires session access tracking
User-Facing Behavior
Scenario 1: Normal Usage (≤ 10 sessions)
User creates sessions 1-10
→ All sessions work normally
→ No evictionsScenario 2: Exceeds Limit (11th session)
User has 10 active sessions
User creates 11th session
→ Least recently used session evicted
→ New session created successfully
→ User unaware unless monitoring old sessionScenario 3: Monitoring Evictions
User queries session list (future Session Management API)
→ Sees 10 sessions
→ Notices old session disappeared
→ Can adjust client behaviorConfiguration Options
// src/config/session.ts
export interface SessionConfig {
// Maximum sessions per user (0 = unlimited)
maxPerUser: number;
// Eviction policy: oldest | least_recently_used
evictionPolicy: "oldest" | "least_recently_used";
// Session TTL in milliseconds
ttl: number;
}
export const sessionConfig: SessionConfig = {
maxPerUser: parseInt(process.env.SESSION_MAX_PER_USER || "10", 10),
evictionPolicy:
(process.env.SESSION_EVICTION_POLICY as "oldest" | "least_recently_used") ||
"least_recently_used",
ttl: parseInt(process.env.SESSION_TTL_MS || String(24 * 60 * 60 * 1000), 10),
};Testing Strategy
// src/mcp/mcp.test.ts
describe("Session limit enforcement", () => {
it("should allow up to max sessions per user", async () => {
const user = { sub: "user123", email: "test@example.com" };
// Create 10 sessions (at limit)
for (let i = 0; i < 10; i++) {
const res = await request(app)
.post("/mcp")
.set("Authorization", `Bearer ${generateToken(user)}`)
.send({ jsonrpc: "2.0", method: "initialize", params: {}, id: 1 });
expect(res.status).toBe(200);
}
const sessions = await getUserSessions(user.sub);
expect(sessions).toHaveLength(10);
});
it("should evict oldest session when limit exceeded", async () => {
const user = { sub: "user123", email: "test@example.com" };
// Create 10 sessions
const sessionIds: string[] = [];
for (let i = 0; i < 10; i++) {
const res = await request(app)
.post("/mcp")
.set("Authorization", `Bearer ${generateToken(user)}`)
.send({ jsonrpc: "2.0", method: "initialize", params: {}, id: 1 });
sessionIds.push(res.headers["mcp-session-id"]);
await sleep(10); // Ensure different createdAt timestamps
}
// Create 11th session
const res = await request(app)
.post("/mcp")
.set("Authorization", `Bearer ${generateToken(user)}`)
.send({ jsonrpc: "2.0", method: "initialize", params: {}, id: 1 });
expect(res.status).toBe(200);
// Verify oldest session evicted
const sessions = await getUserSessions(user.sub);
expect(sessions).toHaveLength(10);
expect(sessions.find(s => s.sessionId === sessionIds[0])).toBeUndefined();
});
it("should evict least recently used session", async () => {
config.session.evictionPolicy = "least_recently_used";
const user = { sub: "user123", email: "test@example.com" };
// Create 10 sessions
const sessionIds: string[] = [];
for (let i = 0; i < 10; i++) {
const res = await request(app)
.post("/mcp")
.set("Authorization", `Bearer ${generateToken(user)}`)
.send({ jsonrpc: "2.0", method: "initialize", params: {}, id: 1 });
sessionIds.push(res.headers["mcp-session-id"]);
}
// Access session 0 to update lastAccessedAt
await request(app)
.post("/mcp")
.set("Authorization", `Bearer ${generateToken(user)}`)
.set("mcp-session-id", sessionIds[0])
.send({ jsonrpc: "2.0", method: "ping", params: {}, id: 1 });
// Create 11th session - should evict session 1 (not 0)
const res = await request(app)
.post("/mcp")
.set("Authorization", `Bearer ${generateToken(user)}`)
.send({ jsonrpc: "2.0", method: "initialize", params: {}, id: 1 });
const sessions = await getUserSessions(user.sub);
expect(sessions.find(s => s.sessionId === sessionIds[0])).toBeDefined();
expect(sessions.find(s => s.sessionId === sessionIds[1])).toBeUndefined();
});
});Acceptance Criteria
- [ ] Add
SESSION_MAX_PER_USERconfiguration option (default: 10) - [ ] Add
SESSION_EVICTION_POLICYconfiguration option (default: least_recently_used) - [ ] Implement
getUserSessions()to query user's active sessions - [ ] Implement session eviction logic in MCP route handler
- [ ] Add
sessionEvictionsPrometheus counter - [ ] Add
sessionsPerUserPrometheus histogram - [ ] Comprehensive test coverage (≥90%)
- [ ] Update session architecture documentation
- [ ] Add monitoring guide for session limits
Sample Queries
# Session eviction rate
rate(session_evictions_total[5m])
# Users hitting session limit
count(sessions_per_user_bucket{le="10"}) by (user)
# Average sessions per user
avg(sessions_per_user)
# Alert: High eviction rate (users creating too many sessions)
rate(session_evictions_total[5m]) > 0.1Related Enhancements
- Session Management API - User-facing session control
- Session Expiration Notification - Session lifecycle awareness
- Enhanced Rate Limiting - Additional abuse protection
Future Considerations
- Per-tenant limits: Different limits for different organizations
- Session pooling: Reuse sessions across requests from same client
- Graceful eviction notification: Notify client when session evicted
- Session priority: Pin important sessions to prevent eviction