Skip to main content

Overview

The session management API enables zero-downtime deployments in multi-tenant SaaS platforms by allowing you to export and restore MCP sessions across container restarts. This is critical for production deployments where user sessions must survive application updates.
Added in: v2.24.1Session persistence exports session metadata and n8n credentials. Transport and server objects are recreated lazily on the first request after restore.

Key Concepts

Session State

A session consists of:
  • Metadata: Creation time, last access time
  • Context: n8n API credentials and instance configuration
  • Transport: Connection to MCP client (not persisted)
  • Server: MCP server instance (not persisted)
Only metadata and context are exported. Transport and server objects are recreated automatically when a restored session receives its first request.

Dormant Sessions

After restore, sessions are “dormant” until their first request:
  • Metadata and context are loaded into memory
  • Transport and server objects are null
  • On first request, transport/server are recreated automatically
  • Session resumes normally without client awareness

API Methods

exportSessionState()

Export all active session state before shutdown.
const sessions: SessionState[] = engine.exportSessionState();
Returns: Array of SessionState objects Behavior:
  • Only exports sessions with valid n8nApiUrl and n8nApiKey
  • Skips expired sessions (based on sessionTimeout)
  • Returns empty array if no valid sessions exist

restoreSessionState()

Restore previously exported sessions after startup.
const count: number = engine.restoreSessionState(sessions);
Parameters:
  • sessions: Array of SessionState objects from exportSessionState()
Returns: Number of sessions successfully restored Behavior:
  • Validates each session using validateInstanceContext()
  • Skips expired sessions
  • Skips sessions with invalid data
  • Logs warnings for skipped sessions
  • Respects MAX_SESSIONS limit

Usage Examples

Basic Session Persistence

import { N8NMCPEngine, SessionState } from 'n8n-mcp';
import fs from 'fs/promises';

const engine = new N8NMCPEngine();

// Before shutdown, export sessions
process.on('SIGTERM', async () => {
  console.log('Exporting sessions...');
  const sessions = engine.exportSessionState();
  
  // Save to file (encrypt in production!)
  await fs.writeFile(
    './sessions.json',
    JSON.stringify(sessions, null, 2)
  );
  
  console.log(`Saved ${sessions.length} sessions`);
  await engine.shutdown();
  process.exit(0);
});

With Encryption

Critical: Session state contains plaintext API keys. Always encrypt before persisting to disk.
import crypto from 'crypto';
import fs from 'fs/promises';
import { N8NMCPEngine } from 'n8n-mcp';

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; // 32 bytes
const SESSION_FILE = './sessions.encrypted';

function encrypt(text: string): string {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(
    'aes-256-gcm',
    Buffer.from(ENCRYPTION_KEY, 'hex'),
    iv
  );
  
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const authTag = cipher.getAuthTag();
  
  return JSON.stringify({
    iv: iv.toString('hex'),
    encrypted,
    authTag: authTag.toString('hex')
  });
}

function decrypt(encrypted: string): string {
  const { iv, encrypted: encryptedData, authTag } = JSON.parse(encrypted);
  
  const decipher = crypto.createDecipheriv(
    'aes-256-gcm',
    Buffer.from(ENCRYPTION_KEY, 'hex'),
    Buffer.from(iv, 'hex')
  );
  
  decipher.setAuthTag(Buffer.from(authTag, 'hex'));
  
  let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  
  return decrypted;
}

// Export with encryption
async function saveSessions() {
  const sessions = engine.exportSessionState();
  const json = JSON.stringify(sessions);
  const encrypted = encrypt(json);
  await fs.writeFile(SESSION_FILE, encrypted);
  console.log(`Encrypted and saved ${sessions.length} sessions`);
}

// Restore from encrypted storage
async function loadSessions() {
  const encrypted = await fs.readFile(SESSION_FILE, 'utf8');
  const json = decrypt(encrypted);
  const sessions = JSON.parse(json);
  const count = engine.restoreSessionState(sessions);
  console.log(`Restored ${count} sessions`);
}

Redis-Based Persistence

import { N8NMCPEngine, SessionState } from 'n8n-mcp';
import Redis from 'ioredis';
import crypto from 'crypto';

const redis = new Redis(process.env.REDIS_URL);
const SESSION_KEY = 'n8n-mcp:sessions';
const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');

function encrypt(data: SessionState[]): string {
  const json = JSON.stringify(data);
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
  
  let encrypted = cipher.update(json, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const authTag = cipher.getAuthTag();
  
  return JSON.stringify({
    iv: iv.toString('hex'),
    encrypted,
    authTag: authTag.toString('hex')
  });
}

function decrypt(encrypted: string): SessionState[] {
  const { iv, encrypted: data, authTag } = JSON.parse(encrypted);
  const decipher = crypto.createDecipheriv(
    'aes-256-gcm',
    ENCRYPTION_KEY,
    Buffer.from(iv, 'hex')
  );
  
  decipher.setAuthTag(Buffer.from(authTag, 'hex'));
  let decrypted = decipher.update(data, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  
  return JSON.parse(decrypted);
}

class SessionManager {
  constructor(private engine: N8NMCPEngine) {}
  
  async saveSessions(): Promise<void> {
    const sessions = this.engine.exportSessionState();
    if (sessions.length === 0) return;
    
    const encrypted = encrypt(sessions);
    await redis.set(SESSION_KEY, encrypted, 'EX', 3600); // 1 hour TTL
    console.log(`Saved ${sessions.length} sessions to Redis`);
  }
  
  async restoreSessions(): Promise<number> {
    const encrypted = await redis.get(SESSION_KEY);
    if (!encrypted) {
      console.log('No sessions in Redis');
      return 0;
    }
    
    const sessions = decrypt(encrypted);
    const count = this.engine.restoreSessionState(sessions);
    console.log(`Restored ${count} of ${sessions.length} sessions`);
    return count;
  }
  
  // Auto-save every 5 minutes
  startAutoSave(): void {
    setInterval(() => this.saveSessions(), 5 * 60 * 1000);
  }
}

const engine = new N8NMCPEngine();
const manager = new SessionManager(engine);

// Restore on startup
await manager.restoreSessions();

// Auto-save periodically
manager.startAutoSave();

// Save on shutdown
process.on('SIGTERM', async () => {
  await manager.saveSessions();
  await engine.shutdown();
  process.exit(0);
});

Kubernetes Rolling Update

apiVersion: apps/v1
kind: Deployment
metadata:
  name: n8n-mcp
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0  # Zero-downtime
  template:
    spec:
      containers:
      - name: n8n-mcp
        image: your-registry/n8n-mcp:latest
        env:
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: redis-creds
              key: url
        - name: ENCRYPTION_KEY
          valueFrom:
            secretKeyRef:
              name: encryption-key
              key: key
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "kill -TERM 1 && sleep 30"]
        terminationGracePeriodSeconds: 60

Security Considerations

Encryption Requirements

Always encrypt session state before persisting. Exported data contains:
  • n8n API keys (plaintext)
  • Instance URLs
  • Custom metadata
Recommended encryption:
  • Algorithm: AES-256-GCM (authenticated encryption)
  • Key Management: Use dedicated secret management (AWS KMS, Vault, etc.)
  • Key Rotation: Support periodic key rotation
  • Transmission: Use TLS for network transmission

Access Control

// Example: Verify tenant access before restore
function filterSessionsByTenant(sessions: SessionState[], tenantId: string) {
  return sessions.filter(s => 
    s.context.metadata?.tenantId === tenantId
  );
}

// Only restore sessions for authorized tenant
const tenantSessions = filterSessionsByTenant(allSessions, req.user.tenantId);
engine.restoreSessionState(tenantSessions);

Audit Logging

import { logger } from './logger';

function auditSessionExport(sessions: SessionState[]) {
  logger.security('session_export', {
    count: sessions.length,
    timestamp: new Date().toISOString(),
    sessionIds: sessions.map(s => s.sessionId)
  });
}

function auditSessionRestore(sessions: SessionState[], restored: number) {
  logger.security('session_restore', {
    attempted: sessions.length,
    restored,
    timestamp: new Date().toISOString()
  });
}

Configuration

Environment Variables

SESSION_TIMEOUT_MINUTES
number
default:5
Session timeout in minutes. Sessions inactive longer than this are not exported.
N8N_MCP_MAX_SESSIONS
number
default:100
Maximum concurrent sessions. Restore operations respect this limit.

Expiration Handling

Sessions are considered expired if:
const isExpired = (session: SessionState) => {
  const lastAccess = new Date(session.metadata.lastAccess);
  const age = Date.now() - lastAccess.getTime();
  return age > sessionTimeout;
};
Expired sessions are:
  • Skipped during export
  • Skipped during restore
  • Logged as warnings

Best Practices

1. Export Frequency

// Export every 5 minutes
setInterval(async () => {
  const sessions = engine.exportSessionState();
  await saveSessions(sessions);
}, 5 * 60 * 1000);

2. Storage Location

Good Options:
  • ✅ Redis with encryption
  • ✅ AWS S3 with server-side encryption
  • ✅ Database with encrypted column
  • ✅ Kubernetes Secret (small scale)
Avoid:
  • ❌ Local filesystem (not shared across pods)
  • ❌ Unencrypted storage
  • ❌ Version control (Git)

3. Error Handling

async function safeRestore(sessions: SessionState[]) {
  try {
    const count = engine.restoreSessionState(sessions);
    
    if (count < sessions.length) {
      logger.warn('Some sessions failed to restore', {
        attempted: sessions.length,
        restored: count,
        failed: sessions.length - count
      });
    }
    
    return count;
  } catch (error) {
    logger.error('Session restore failed', { error });
    // Continue startup even if restore fails
    return 0;
  }
}

4. Monitoring

// Track session persistence metrics
interface SessionMetrics {
  exportCount: number;
  restoreCount: number;
  failedRestores: number;
  lastExportTime: Date;
  lastRestoreTime: Date;
}

const metrics: SessionMetrics = {
  exportCount: 0,
  restoreCount: 0,
  failedRestores: 0,
  lastExportTime: new Date(),
  lastRestoreTime: new Date()
};

function recordExport(sessionCount: number) {
  metrics.exportCount += sessionCount;
  metrics.lastExportTime = new Date();
  
  // Send to monitoring system
  statsd.gauge('n8n_mcp.sessions.exported', sessionCount);
}

function recordRestore(attempted: number, restored: number) {
  metrics.restoreCount += restored;
  metrics.failedRestores += (attempted - restored);
  metrics.lastRestoreTime = new Date();
  
  statsd.gauge('n8n_mcp.sessions.restored', restored);
  statsd.gauge('n8n_mcp.sessions.failed_restore', attempted - restored);
}

Troubleshooting

Sessions Not Restoring

Check validation errors:
import { validateInstanceContext } from 'n8n-mcp';

for (const session of sessions) {
  const validation = validateInstanceContext(session.context);
  if (!validation.valid) {
    console.error(`Session ${session.sessionId} invalid:`, validation.errors);
  }
}
Check expiration:
const now = Date.now();
const timeout = 30 * 60 * 1000; // 30 min

for (const session of sessions) {
  const age = now - new Date(session.metadata.lastAccess).getTime();
  if (age > timeout) {
    console.log(`Session ${session.sessionId} expired (age: ${age}ms)`);
  }
}

MAX_SESSIONS Reached

// Check current session count
const info = engine.getSessionInfo();
console.log(`Active sessions: ${info.active}`);

// Increase limit if needed
process.env.N8N_MCP_MAX_SESSIONS = '200';

Memory Usage

// Monitor memory per session
const sessions = engine.exportSessionState();
const avgSize = JSON.stringify(sessions).length / sessions.length;
console.log(`Average session size: ${avgSize} bytes`);

// Consider compression for large contexts
import zlib from 'zlib';

function compressSessions(sessions: SessionState[]): string {
  const json = JSON.stringify(sessions);
  return zlib.gzipSync(json).toString('base64');
}

Build docs developers (and LLMs) love