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
Shutdown Handler
Startup Handler
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 in minutes. Sessions inactive longer than this are not exported.
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
Periodic Exports
On-Demand Exports
// 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' );
}