Execution Modes
Section titled “Execution Modes”One decision changes everything. Every Hoody Exec script declares an execution mode that fundamentally determines how it runs. Choose wisely — it affects state, performance, concurrency, and capabilities.
Worker Mode — Persistent & Stateful
Section titled “Worker Mode — Persistent & Stateful”// @mode workerThink: Long-running Node.js server that never restarts.
Worker mode creates a persistent V8 isolate that stays alive across all requests. The shared object acts as your in-memory store — write to it once, read it forever (until container restart).
Features
Section titled “Features”- Persistent VM — Created once, runs forever
- Shared State —
sharedobject persists across all requests - WebSocket Support — Required for real-time connections
- No Concurrency Limits — Handle unlimited simultaneous requests
- Pre/Post Middleware —
pre.jsandpost.jswrap requests matching a script in the same directory - Zero Cold Start — VM is already warm
Limitations
Section titled “Limitations”- State lost on container restart (use SQLite for persistence)
- Higher memory usage (persistent VM overhead)
Perfect For
Section titled “Perfect For”- WebSocket servers (chat, live dashboards, real-time updates)
- Session management and caching
- High-traffic APIs (no cold starts)
- Rate limiting with per-IP tracking
- Hoody AI interception (MITM proxy for controlling AI requests)
- Any scenario where you need state that survives across requests
Serverless Mode — Isolated & Fresh
Section titled “Serverless Mode — Isolated & Fresh”// @mode serverless // or omit (default)Think: AWS Lambda, Vercel Functions, Cloudflare Workers.
Serverless mode creates a brand new V8 isolate for every single request. Complete isolation between requests — no state leakage, no shared memory, no side effects.
Features
Section titled “Features”- Fresh Context — Brand new VM for every request
- Complete Isolation — Zero state leakage between requests
- Concurrency Control —
@concurrent 5limits parallel execution - Lower Memory — No persistent VM overhead
- Safer for Untrusted Code — Better isolation guarantees
Limitations
Section titled “Limitations”- No WebSocket support (no persistent connections)
- No shared state across requests
- Slight overhead per request (VM creation)
Perfect For
Section titled “Perfect For”- Webhook receivers (Stripe, GitHub, Slack)
- Isolated tasks (data processing, API calls)
- Sporadic traffic (pay-per-execution model)
- Stateless microservices
- Untrusted user scripts (better isolation)
- Any scenario where isolation matters more than performance
Shared State
Section titled “Shared State”The shared object is available in both modes but behaves very differently:
// @mode worker
// Initialize on first requestif (!shared.requestCount) { shared.requestCount = 0; shared.users = new Map(); shared.sessions = new Set();}
shared.requestCount++; // Persists across ALL requestsshared.users.set(userId, userData);
return { count: shared.requestCount, // Increments: 1, 2, 3, 4... cachedUsers: shared.users.size, // Accumulates over time activeSessions: shared.sessions.size};// @mode serverless
// This ALWAYS starts freshif (!shared.requestCount) { shared.requestCount = 0; // Runs EVERY request}
shared.requestCount++; // Always equals 1 (resets each time)
return { count: shared.requestCount // Always returns 1};Key differences:
- Worker:
sharedpersists between requests → perfect for caching, counters, sessions - Serverless:
sharedresets every request → no persistence benefit, use for request-scoped temp data - Both modes:
sharedlost on container restart/reboot → use SQLite for permanent storage
Shared State Persistence Details
Section titled “Shared State Persistence Details”Shared state is in-memory only, scoped per hostname (each exec instance has its own shared object). There is no automatic TTL — values persist for the entire lifetime of the server process. This means:
- Data stays in
shareduntil explicitly deleted or the container restarts - There is no expiration mechanism — implement your own if needed
- For long-running workers, explicitly clean up stale data to prevent unbounded memory growth
// @mode worker// Explicit cleanup pattern for long-running workersif (!shared.cache) shared.cache = new Map();
// Add with timestampshared.cache.set(key, { value: data, createdAt: Date.now() });
// Periodic cleanup (e.g., remove entries older than 1 hour)const ONE_HOUR = 3600000;for (const [k, v] of shared.cache) { if (Date.now() - v.createdAt > ONE_HOUR) shared.cache.delete(k);}State Loss on Restart
Section titled “State Loss on Restart”Shared state is lost on container restart — this is by design. The shared object lives entirely in memory. If you need data to survive restarts:
- SQLite — Use the bundled
Database(Bun.Database) for structured persistent storage - External databases — Connect to external services for critical data
- File system — Write to
/hoody/storage/for simple persistence
// @mode worker// Persist critical data to SQLiteconst db = new Database('/hoody/databases/app.db');db.run('CREATE TABLE IF NOT EXISTS sessions (id TEXT, data TEXT, created INTEGER)');
// Use shared for fast access, SQLite for durabilityif (!shared.sessions) { // Restore from SQLite on first request after restart shared.sessions = new Map(); const rows = db.query('SELECT id, data FROM sessions').all(); for (const row of rows) { shared.sessions.set(row.id, JSON.parse(row.data)); }}Cold Start Behavior
Section titled “Cold Start Behavior”The execution mode directly affects startup latency:
Serverless mode — Every request creates a fresh V8 isolate. There is a small overhead for VM creation and script compilation on each request. This is the “cold start” cost, similar to AWS Lambda or Cloudflare Workers.
Worker mode — Only the first request incurs the VM creation cost. After that, the persistent VM stays warm and all subsequent requests have zero cold start. The VM remains active until the container restarts.
| First Request | Subsequent Requests | |
|---|---|---|
| Serverless | VM creation + compilation | VM creation + compilation (same cost every time) |
| Worker | VM creation + compilation | Zero overhead (VM already warm) |
For latency-sensitive endpoints handling frequent traffic, worker mode eliminates cold start entirely.
Memory Considerations
Section titled “Memory Considerations”Worker mode’s persistent VM retains all variables between requests. This is powerful but requires awareness:
- Memory accumulates — Every
Map,Set, array, or object added toshared(or any persistent variable) stays in memory - No automatic garbage collection of shared data — Only unreferenced local variables are GC’d between requests
- Growing arrays or maps indefinitely will eventually consume all available memory
Monitoring Memory Usage
Section titled “Monitoring Memory Usage”Use the monitoring endpoint to track memory consumption:
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/monitor/stats"Response includes:
{ "memory": { "used": 52428800, "total": 268435456, "percentage": 19.5 }}Cleanup Strategies
Section titled “Cleanup Strategies”// @mode worker
// 1. Cap collection sizesif (shared.logs && shared.logs.length > 1000) { shared.logs = shared.logs.slice(-500); // Keep last 500}
// 2. Use the cache clear API for full reset// POST /api/v1/exec/cache/clear
// 3. Avoid patterns that grow indefinitely// BAD: shared.allRequests.push(req) — grows forever// GOOD: shared.recentRequests = shared.recentRequests.slice(-100)WebSocket Support (Worker Mode Only)
Section titled “WebSocket Support (Worker Mode Only)”Enable real-time bidirectional communication with two magic comments:
// @mode worker// @websocket// @cors reflective
// Serve HTML UI for HTTP requests (optional)res.writeHead(200, { 'Content-Type': 'text/html' });res.end('<h1>WebSocket Server</h1>');
// WebSocket handlers (Direct Assignment Pattern)ws.message = (socket, data) => { console.log('Received:', data); ws.broadcast(data); // Send to all clients};
ws.close = (socket, code, reason) => { console.log('Client disconnected');};
// OR use Event Emitter Pattern:ws.on('message', (socket, data) => { socket.send('Echo: ' + data);});Connection tracking:
console.log('Active connections:', ws.connections.size);Complete Real-World Examples
Section titled “Complete Real-World Examples”Worker Mode (with shared state caching):
// @mode worker// @cors reflective// @timeout 5000// @log-level standard
// Initialize cache on first requestif (!shared.usersCache) { shared.usersCache = new Map(); shared.cacheHits = 0; shared.cacheMisses = 0;}
const userId = metadata.parameters.id;
// Check cache firstif (shared.usersCache.has(userId)) { shared.cacheHits++; return { user: shared.usersCache.get(userId), cached: true, cacheHitRate: shared.cacheHits / (shared.cacheHits + shared.cacheMisses) };}
// Fetch from database (cache miss)shared.cacheMisses++;const user = await fetchUserFromDatabase(userId);
// Cache for next requestshared.usersCache.set(userId, user);
return { user, cached: false, cacheHitRate: shared.cacheHits / (shared.cacheHits + shared.cacheMisses)};Serverless Mode (fresh, stateless):
// @mode serverless// @concurrent 10// @cors reflective// @timeout 5000// @log-level standard
const userId = metadata.parameters.id;
// Validate inputif (!userId || userId.length !== 24) { res.statusCode = 400; return { error: 'Invalid user ID format', expected: '24-character hex string' };}
// Fresh database query every time (no cache)const user = await fetchUserFromDatabase(userId);
if (!user) { res.statusCode = 404; return { error: 'User not found', userId };}
// Clean response (isolated execution)return { user, requestId: metadata.executionId};Worker Mode Only (WebSocket requires persistent VM):
// @mode worker// @websocket// @cors reflective// @timeout 0
const roomId = metadata.parameters.roomId;
// Initialize room stateif (!shared.rooms) { shared.rooms = new Map();}
if (!shared.rooms.has(roomId)) { shared.rooms.set(roomId, { users: new Map(), messages: [], created: new Date() });}
const room = shared.rooms.get(roomId);
// Serve HTML UI for HTTP requestsres.writeHead(200, { 'Content-Type': 'text/html' });res.end(` <!DOCTYPE html> <html> <head><title>Chat Room: ${roomId}</title></head> <body> <div id="messages"></div> <input id="input" placeholder="Type message..." /> <script> const ws = new WebSocket(location.href.replace('http', 'ws')); ws.onmessage = e => { const div = document.createElement('div'); div.textContent = e.data; document.getElementById('messages').appendChild(div); }; document.getElementById('input').onkeypress = e => { if (e.key === 'Enter') { ws.send(e.target.value); e.target.value = ''; } }; </script> </body> </html>`);
// WebSocket handlersws.open = (socket, req) => { const userId = socket.data.executionId; room.users.set(userId, { connectedAt: new Date(), ip: socket.data.ip });
// Broadcast join message to all in room ws.broadcast(JSON.stringify({ type: 'join', roomId, userId, userCount: room.users.size }));};
ws.message = (socket, data) => { const message = { type: 'message', roomId, userId: socket.data.executionId, text: data, timestamp: new Date().toISOString() };
// Save to room history room.messages.push(message);
// Broadcast to all users in THIS room only ws.broadcast(JSON.stringify(message));};
ws.close = (socket, code, reason) => { const userId = socket.data.executionId; room.users.delete(userId);
// Cleanup empty rooms if (room.users.size === 0 && room.messages.length === 0) { shared.rooms.delete(roomId); }
ws.broadcast(JSON.stringify({ type: 'leave', roomId, userId, userCount: room.users.size }));};Serverless Mode Best Practice (isolation prevents cross-webhook contamination):
// @mode serverless// @concurrent false // Process webhooks serially// @cors none// @timeout 30000// @log-level full// @log-request-body true
// Validate webhook signatureconst signature = req.headers['stripe-signature'];if (!signature) { res.statusCode = 401; return { error: 'Missing signature', message: 'Stripe-Signature header required' };}
// Parse webhook payloadlet event;try { // req.rawBody is the raw request Buffer (preserved for signature verification) const rawBody = req.rawBody.toString(); event = JSON.parse(rawBody);} catch (err) { res.statusCode = 400; return { error: 'Invalid payload', message: err.message };}
// Handle event typesswitch (event.type) { case 'payment_intent.succeeded': await processPayment(event.data.object); break;
case 'customer.subscription.created': await createSubscription(event.data.object); break;
case 'invoice.payment_failed': await handleFailedPayment(event.data.object); break;
default: console.log('Unhandled event type:', event.type);}
// Stripe requires 200 responseres.statusCode = 200;return { received: true, eventId: event.id, type: event.type, processedAt: new Date().toISOString()};Worker Mode (shared state tracks request counts per IP):
// @mode worker// @timeout 5000// @log-level standard
// Rate limit: 10 requests per minute per IPconst RATE_LIMIT = 10;const WINDOW_MS = 60000;
// Initialize rate limit trackingif (!shared.rateLimits) { shared.rateLimits = new Map();}
const clientIp = metadata.clientIp;const now = Date.now();
// Get or create IP trackinglet ipData = shared.rateLimits.get(clientIp);if (!ipData) { ipData = { requests: [], firstRequest: now }; shared.rateLimits.set(clientIp, ipData);}
// Clean old requests outside windowipData.requests = ipData.requests.filter( timestamp => now - timestamp < WINDOW_MS);
// Check rate limitif (ipData.requests.length >= RATE_LIMIT) { const oldestRequest = Math.min(...ipData.requests); const resetIn = WINDOW_MS - (now - oldestRequest);
res.statusCode = 429; res.setHeader('Retry-After', Math.ceil(resetIn / 1000)); res.setHeader('X-RateLimit-Limit', RATE_LIMIT); res.setHeader('X-RateLimit-Remaining', 0); res.setHeader('X-RateLimit-Reset', new Date(now + resetIn).toISOString());
return { error: 'Rate limit exceeded', limit: RATE_LIMIT, window: '1 minute', retryAfter: Math.ceil(resetIn / 1000) };}
// Add this request to trackingipData.requests.push(now);
// Set rate limit headersconst remaining = RATE_LIMIT - ipData.requests.length;res.setHeader('X-RateLimit-Limit', RATE_LIMIT);res.setHeader('X-RateLimit-Remaining', remaining);res.setHeader('X-RateLimit-Reset', new Date(now + WINDOW_MS).toISOString());
// Execute actual endpoint logicconst data = await processRequest();
return { success: true, data, rateLimit: { remaining, resetAt: new Date(now + WINDOW_MS).toISOString() }};Worker Mode (intercept and control AI requests):
// @mode worker// @timeout 60000// @cors reflective// @log-level full// @log-request-body true// @log-response-body true
// Initialize MITM trackingif (!shared.aiRequests) { shared.aiRequests = []; shared.blockedCount = 0; shared.modifiedCount = 0;}
// Parse AI request (req.body is the auto-parsed JSON payload)const aiRequest = req.body;
// Log for observabilityconsole.log('AI Request:', { model: aiRequest.model, messageCount: aiRequest.messages?.length, timestamp: new Date().toISOString()});
// BLOCK: Prevent sensitive data leaksconst hasSensitiveData = aiRequest.messages?.some(msg => /api[_-]?key|password|secret|token/i.test(msg.content));
if (hasSensitiveData) { shared.blockedCount++; res.statusCode = 403; return { error: 'Blocked: Sensitive data detected', reason: 'AI request contains potential API keys or secrets', blocked: shared.blockedCount, timestamp: new Date().toISOString() };}
// MODIFY: Add system promptif (aiRequest.messages[0]?.role !== 'system') { aiRequest.messages.unshift({ role: 'system', content: 'You are a helpful assistant. Be concise and accurate.' }); shared.modifiedCount++;}
// TRACK: Store request for analysisshared.aiRequests.push({ model: aiRequest.model, messageCount: aiRequest.messages.length, timestamp: new Date().toISOString(), modified: shared.modifiedCount > 0});
// Keep only last 100 requestsif (shared.aiRequests.length > 100) { shared.aiRequests.shift();}
// Forward to actual Hoody AI endpointconst hoodyAIResponse = await fetch('https://ai.hoody.icu/api/v1/chat/completions', { method: 'POST', headers: { 'Authorization': req.headers.authorization, 'Content-Type': 'application/json' }, body: JSON.stringify(aiRequest)});
const result = await hoodyAIResponse.json();
// Return with observability metadatareturn { ...result, _mitm: { modified: shared.modifiedCount > 0, blocked: shared.blockedCount, totalRequests: shared.aiRequests.length }};See Hoody AI Intercept & Control for the complete MITM guide.