how-it-works
serverless-functions

How It Works

The Handler Pattern

Every function exports a handler function. It receives the request data and returns a result:

function handler(data) {
  return { message: "Hello, " + data.name };
}

Async functions are supported:

async function handler(url) {
  const response = await fetch(url);
  return await response.json();
}

Web Worker Isolation

Each deployed function runs in its own Deno Web Worker. Workers are:

  • Memory-isolated — one function cannot access another's state
  • Permission-restricted — only net is allowed (no filesystem, no env, no subprocess)
  • Long-lived — the Worker stays in memory after deploy for fast warm invocations
  • Terminable — delete a function and the Worker is terminated immediately
Deno Runtime Pod
  ├─ Main thread (HTTP server, routing)
  ├─ Worker: hello         ← isolated
  ├─ Worker: process-data  ← isolated
  └─ Worker: notify-slack  ← isolated

Permissions

PermissionAllowedWhy
netYesFunctions need HTTP fetch for APIs, webhooks
envNoPrevents access to K8s secrets/env vars
readNoNo filesystem access
writeNoNo filesystem writes
runNoCannot spawn subprocesses
ffiNoNo foreign function interface

Timeouts

Default timeout is 30 seconds. If a function exceeds the timeout, the invocation fails with an error but the Worker stays alive for future calls.

{
  "error": "Execution timeout (30s)"
}

Deploy Lifecycle

  1. POST /deploy — server receives code, creates a Web Worker
  2. Worker compiles and sends a ready signal (10-50ms)
  3. Worker stays in memory, ready for invocations
  4. Each POST /invoke/{id} sends a message to the Worker, Worker returns result (1-5ms)
  5. DELETE /delete/{id} — Worker is terminated, memory freed

Provisioning Hooks

When used with the K8s Provisioning service, functions can act as lifecycle hooks:

HookWhenContext available
pre-provisionBefore pipelineprojectId, tier, databaseType
post-provisionAfter ACTIVEAbove + credentials (host, port, password)
pre-backupBefore backupprojectId
post-backupAfter backupprojectId, backupId
pre-deprovisionBefore deleteprojectId
post-deprovisionAfter cleanupprojectId

Example post-provision hook:

async function handler(ctx) {
  // Notify Slack
  await fetch("https://hooks.slack.com/services/...", {
    method: "POST",
    body: JSON.stringify({ text: `DB ${ctx.projectId} is ready!` })
  });

  // Install pgvector
  await fetch(`http://provisioning-svc/api/provision/${ctx.projectId}/migrations`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ sql: "CREATE EXTENSION IF NOT EXISTS vector", version: "001", name: "pgvector" })
  });

  return { notified: true, extensionInstalled: true };
}

Hook failures are non-blocking — the provisioning pipeline always completes regardless.