# Execution Modes

**Page:** kit/exec/execution-modes

[Download Raw Markdown](./kit/exec/execution-modes.md)

---

# 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

```javascript
// @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).

### Features

- **Persistent VM** — Created once, runs forever
- **Shared State** — `shared` object persists across all requests
- **WebSocket Support** — Required for real-time connections
- **No Concurrency Limits** — Handle unlimited simultaneous requests
- **Pre/Post Middleware** — `pre.js` and `post.js` wrap requests matching a script in the same directory
- **Zero Cold Start** — VM is already warm

### Limitations

- State lost on container restart (use SQLite for persistence)
- Higher memory usage (persistent VM overhead)

### 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

```javascript
// @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

- **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

### Limitations

- No WebSocket support (no persistent connections)
- No shared state across requests
- Slight overhead per request (VM creation)

### 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

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


  
```javascript
// @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
};
```
  
  
```javascript
// @mode serverless

// This ALWAYS starts fresh
if (!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**: `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 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 `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

```javascript
// @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);
}
```

### 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

```javascript
// @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));
  }
}
```

---

## 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

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

### Monitoring Memory Usage

Use the monitoring endpoint to track memory consumption:

```bash
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/monitor/stats"
```

Response includes:
```json
{
  "memory": {
    "used": 52428800,
    "total": 268435456,
    "percentage": 19.5
  }
}
```

### Cleanup Strategies

```javascript
// @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)
```


If your worker script accumulates data in `shared` or module-level variables without cleanup, memory usage will grow with each request. Use `GET /api/v1/exec/monitor/stats` to monitor memory and `POST /api/v1/exec/cache/clear` for periodic cleanup.


---

## WebSocket Support (Worker Mode Only)

Enable real-time bidirectional communication with two magic comments:

```javascript
// @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:**
```javascript
console.log('Active connections:', ws.connections.size);
```


WebSocket requires **both** `// @mode worker` **and** `// @websocket`. Serverless mode cannot support WebSocket — it creates a fresh VM per request with no persistent connections.


---

## Complete Real-World Examples


  

**Worker Mode** (with shared state caching):
```javascript
// 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):
```javascript
// 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
};
```

  
  

**Worker Mode Only** (WebSocket requires persistent VM):
```javascript
// chat/rooms/[roomId].ts
// @mode worker
// @websocket
// @cors reflective
// @timeout 0

const roomId = metadata.parameters.roomId;

// Initialize room state
if (!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 requests
res.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 handlers
ws.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):
```javascript
// webhooks/stripe.ts
// @mode serverless
// @concurrent false  // Process webhooks serially
// @cors none
// @timeout 30000
// @log-level full
// @log-request-body true

// Validate webhook signature
const signature = req.headers['stripe-signature'];
if (!signature) {
  res.statusCode = 401;
  return {
    error: 'Missing signature',
    message: 'Stripe-Signature header required'
  };
}

// Parse webhook payload
let 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 types
switch (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 response
res.statusCode = 200;
return {
  received: true,
  eventId: event.id,
  type: event.type,
  processedAt: new Date().toISOString()
};
```

  
  

**Worker Mode** (shared state tracks request counts per IP):
```javascript
// api/limited-endpoint.ts
// @mode worker
// @timeout 5000
// @log-level standard

// Rate limit: 10 requests per minute per IP
const RATE_LIMIT = 10;
const WINDOW_MS = 60000;

// Initialize rate limit tracking
if (!shared.rateLimits) {
  shared.rateLimits = new Map();
}

const clientIp = metadata.clientIp;
const now = Date.now();

// Get or create IP tracking
let ipData = shared.rateLimits.get(clientIp);
if (!ipData) {
  ipData = { requests: [], firstRequest: now };
  shared.rateLimits.set(clientIp, ipData);
}

// Clean old requests outside window
ipData.requests = ipData.requests.filter(
  timestamp => now - timestamp < WINDOW_MS
);

// Check rate limit
if (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 tracking
ipData.requests.push(now);

// Set rate limit headers
const 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 logic
const data = await processRequest();

return {
  success: true,
  data,
  rateLimit: {
    remaining,
    resetAt: new Date(now + WINDOW_MS).toISOString()
  }
};
```

  
  

**Worker Mode** (intercept and control AI requests):
```javascript
// ai/intercept.ts
// @mode worker
// @timeout 60000
// @cors reflective
// @log-level full
// @log-request-body true
// @log-response-body true

// Initialize MITM tracking
if (!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 observability
console.log('AI Request:', {
  model: aiRequest.model,
  messageCount: aiRequest.messages?.length,
  timestamp: new Date().toISOString()
});

// BLOCK: Prevent sensitive data leaks
const 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 prompt
if (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 analysis
shared.aiRequests.push({
  model: aiRequest.model,
  messageCount: aiRequest.messages.length,
  timestamp: new Date().toISOString(),
  modified: shared.modifiedCount > 0
});

// Keep only last 100 requests
if (shared.aiRequests.length > 100) {
  shared.aiRequests.shift();
}

// Forward to actual Hoody AI endpoint
const 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 metadata
return {
  ...result,
  _mitm: {
    modified: shared.modifiedCount > 0,
    blocked: shared.blockedCount,
    totalRequests: shared.aiRequests.length
  }
};
```

See [Hoody AI Intercept & Control](/foundation/hoody-ai/mitm/) for the complete MITM guide.

  


---

## What's Next