Skip to content

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.


// @mode worker

Think: 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).

  • Persistent VM — Created once, runs forever
  • Shared Stateshared object persists across all requests
  • WebSocket Support — Required for real-time connections
  • No Concurrency Limits — Handle unlimited simultaneous requests
  • Pre/Post Middlewarepre.js and post.js wrap requests matching a script in the same directory
  • Zero Cold Start — VM is already warm
  • State lost on container restart (use SQLite for persistence)
  • Higher memory usage (persistent VM overhead)
  • 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

// @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.

  • Fresh Context — Brand new VM for every request
  • Complete Isolation — Zero state leakage between requests
  • Concurrency Control@concurrent 5 limits parallel execution
  • Lower Memory — No persistent VM overhead
  • Safer for Untrusted Code — Better isolation guarantees
  • No WebSocket support (no persistent connections)
  • No shared state across requests
  • Slight overhead per request (VM creation)
  • 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

The shared object is available in both modes but behaves very differently:

// @mode worker
// Initialize on first request
if (!shared.requestCount) {
shared.requestCount = 0;
shared.users = new Map();
shared.sessions = new Set();
}
shared.requestCount++; // Persists across ALL requests
shared.users.set(userId, userData);
return {
count: shared.requestCount, // Increments: 1, 2, 3, 4...
cachedUsers: shared.users.size, // Accumulates over time
activeSessions: shared.sessions.size
};

Key differences:

  • Worker: shared persists between requests → perfect for caching, counters, sessions
  • Serverless: shared resets every request → no persistence benefit, use for request-scoped temp data
  • Both modes: shared lost on container restart/reboot → use SQLite for permanent storage

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 shared until 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 workers
if (!shared.cache) shared.cache = new Map();
// Add with timestamp
shared.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);
}

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 SQLite
const 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 durability
if (!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));
}
}

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 RequestSubsequent Requests
ServerlessVM creation + compilationVM creation + compilation (same cost every time)
WorkerVM creation + compilationZero overhead (VM already warm)

For latency-sensitive endpoints handling frequent traffic, worker mode eliminates cold start entirely.


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 to shared (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

Use the monitoring endpoint to track memory consumption:

Terminal window
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
}
}
// @mode worker
// 1. Cap collection sizes
if (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)

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);

Worker Mode (with shared state caching):

api/users/[id].ts
// @mode worker
// @cors reflective
// @timeout 5000
// @log-level standard
// Initialize cache on first request
if (!shared.usersCache) {
shared.usersCache = new Map();
shared.cacheHits = 0;
shared.cacheMisses = 0;
}
const userId = metadata.parameters.id;
// Check cache first
if (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 request
shared.usersCache.set(userId, user);
return {
user,
cached: false,
cacheHitRate: shared.cacheHits / (shared.cacheHits + shared.cacheMisses)
};

Serverless Mode (fresh, stateless):

api/users/[id].ts
// @mode serverless
// @concurrent 10
// @cors reflective
// @timeout 5000
// @log-level standard
const userId = metadata.parameters.id;
// Validate input
if (!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
};