Phase 12: Execution Model¶
Document Type: Execution Specification
Phase: 12 - Serverless Functions
Status: Active
Execution Flow¶
┌──────────┐
│ Trigger │ (HTTP, Database, Schedule, Webhook)
└────┬─────┘
│
▼
┌────────────────┐
│ Invoker │
│ 1. Lookup func │
│ 2. Load WASM │
│ 3. Call handler│
└────┬───────────┘
│
▼
┌──────────────────────────┐
│ WASM Runtime │
│ • Sandbox environment │
│ • Resource limits │
│ • Host function imports │
└────┬─────────────────────┘
│
▼
┌──────────────────────────┐
│ Function Code │
│ exports.handler(event) │
└────┬─────────────────────┘
│
▼
┌──────────────────────────┐
│ Host Functions │
│ • db_query(...) │
│ • log(...) │
│ • http_fetch(...) │
└──────────────────────────┘
Trigger Types¶
1. HTTP Trigger¶
Invocation:
POST /functions/v1/invoke/{function_name}
Authorization: Bearer <JWT>
Content-Type: application/json
{"key": "value"}
Event Payload:
{
"trigger": "http",
"method": "POST",
"path": "/functions/v1/invoke/my-function",
"headers": {"content-type": "application/json"},
"body": {"key": "value"},
"user_id": "uuid", // From JWT
"request_id": "uuid"
}
Function Handler:
export async function handler(event) {
const { body } = event;
// Process request
return {
statusCode: 200,
body: JSON.stringify({ result: "success" })
};
}
Response: Function return value becomes HTTP response body.
2. Database Trigger¶
Registration:
Function {
trigger: TriggerType::Database {
table: "users",
operation: DatabaseOperation::Insert,
}
}
When Fired:
INSERT INTO users (id, email) VALUES ('uuid', 'user@example.com');
-- After commit, function invoked
Event Payload:
{
"trigger": "database",
"table": "users",
"operation": "INSERT",
"old": null,
"new": {
"id": "uuid",
"email": "user@example.com",
"created_at": "2026-02-06T09:00:00Z"
},
"timestamp": "2026-02-06T09:00:00Z"
}
Function Handler:
export async function handler(event) {
const user = event.new;
// Create user profile
await db.query(
"INSERT INTO profiles (user_id, display_name) VALUES ($1, $2)",
[user.id, user.email.split('@')[0]]
);
return { profileCreated: true };
}
UPDATE Operation:
{
"trigger": "database",
"operation": "UPDATE",
"old": {"id": "uuid", "email": "old@example.com"},
"new": {"id": "uuid", "email": "new@example.com"},
...
}
DELETE Operation:
{
"trigger": "database",
"operation": "DELETE",
"old": {"id": "uuid", "email": "user@example.com"},
"new": null,
...
}
3. Schedule Trigger¶
Registration:
Cron Format:
┌─────minute (0 - 59)
│ ┌───hour (0 - 23)
│ │ ┌─day of month (1 - 31)
│ │ │ ┌─month (1 - 12)
│ │ │ │ ┌─day of week (0 - 6, Sunday = 0)
│ │ │ │ │
* * * * *
Examples:
"0 */6 * * *" # Every 6 hours
"*/15 * * * *" # Every 15 minutes
"0 0 1 * *" # First day of month at midnight
"0 9 * * 1-5" # Weekdays at 9 AM
Event Payload:
{
"trigger": "schedule",
"cron": "0 3 * * *",
"scheduled_time": "2026-02-06T03:00:00Z",
"actual_time": "2026-02-06T03:00:01Z"
}
Function Handler:
export async function handler(event) {
console.log("Running cleanup job");
const result = await db.query(
"DELETE FROM logs WHERE created_at < NOW() - INTERVAL '30 days'"
);
return { deletedRows: result.rowCount };
}
4. Webhook Trigger (Future)¶
Registration:
Invocation:
POST /functions/v1/webhook/{function_name}
X-Webhook-Signature: sha256=...
Content-Type: application/json
{"event": "payment.succeeded", "amount": 1000}
Event Payload:
{
"trigger": "webhook",
"headers": {"x-webhook-signature": "sha256=..."},
"body": {"event": "payment.succeeded", "amount": 1000},
"timestamp": "2026-02-06T09:00:00Z"
}
Resource Limits¶
Timeout¶
Default: 10 seconds
Range: 1s - 300s (5 minutes)
Configurable: Per-function
Enforcement:
let result = tokio::time::timeout(
func.config.timeout,
call_handler(instance, payload)
).await;
match result {
Ok(value) => Ok(value),
Err(_) => Err(FunctionError::Timeout),
}
Behavior: - Function exceeds timeout → Killed immediately - Partial work NOT rolled back (non-transactional) - Error logged, caller receives 504
Memory Limit¶
Default: 128 MB
Range: 16MB - 512MB
Configurable: Per-function
Enforcement:
let mut store = Store::new(&engine);
store.limiter(|_| ResourceLimiter {
memory_size: func.config.max_memory,
});
Behavior: - Allocation exceeds limit → WASM panic - Error logged, caller receives 507
CPU (Future)¶
Proposal: CPU instruction count limit - Track WASM instructions executed - Terminate after threshold (e.g., 1 billion instructions)
Not Implemented: Current timeout is wall-clock based
Host Functions¶
Functions call AeroDB via imported host functions.
db_query¶
Signature:
WASM Import:
// Provided by AeroDB runtime
extern "aerodb" {
function db_query(sql: string, params: any[]): Row[];
}
Example:
const users = await db.query(
"SELECT * FROM users WHERE created_at > $1",
[new Date('2026-01-01')]
);
RLS Enforcement: Function inherits RLS context from invocation. Cannot bypass without service role.
log¶
Signature:
Levels: debug, info, warn, error
Example:
Output:
{
"level": "INFO",
"source": "function:send_welcome_email",
"message": "Processing user registration",
"timestamp": "2026-02-06T09:00:00Z"
}
http_fetch (Future)¶
Signature:
Example:
const response = await http.fetch("https://api.stripe.com/v1/charges", {
method: "POST",
headers: {"Authorization": `Bearer ${env("STRIPE_KEY")}`},
body: JSON.stringify({amount: 1000})
});
Security: - Whitelist allowed domains in function config - Rate limiting per function - Timeout applies to HTTP request
env¶
Signature:
Example:
Security: - Only whitelisted env vars returned - Secrets redacted in logs - Per-function environment isolation
Error Handling¶
Function Errors¶
Types: - Timeout: Function exceeded time limit - OutOfMemory: Function exceeded memory limit - Panic: WASM runtime panic (assertion failure, null pointer, etc.) - HostFunctionError: db_query failed, http_fetch failed, etc.
Error Propagation:
export async function handler(event) {
try {
const result = await db.query("SELECT * FROM users");
return { users: result };
} catch (error) {
// Error logged, propagated to caller
throw new Error(`Database query failed: ${error.message}`);
}
}
Caller Receives:
{
"error": "500 Internal Server Error",
"message": "Database query failed: Connection refused",
"code": "FUNCTION_ERROR"
}
Database Isolation¶
Key Principle: Function errors do NOT affect database.
Scenario:
// Database trigger on INSERT users
export async function handler(event) {
// This query succeeds
await db.query("INSERT INTO profiles ...");
// This panics
throw new Error("Oops");
}
Result: - User INSERT: ✅ Committed - Profile INSERT: ✅ Committed (separate transaction) - Function: ❌ Error logged - Database: ✅ Unaffected
No Atomicity: Functions are NOT transactional with triggering operation.
Concurrency¶
Parallel Invocations¶
Multiple invocations of same function run concurrently:
Time Invocation 1 Invocation 2 Invocation 3
T0 Start
T1 db_query Start
T2 db_query Start
T3 Return Return db_query
T4 Return
No Shared State: Each invocation has isolated WASM instance.
Database Trigger Concurrency¶
-- Transaction 1
BEGIN;
INSERT INTO users VALUES (...); -- Triggers function
COMMIT;
-- Transaction 2 (concurrent)
BEGIN;
INSERT INTO users VALUES (...); -- Triggers function
COMMIT;
Behavior: - Both functions invoked concurrently - No ordering guarantee - Each sees its own inserted row
Cold Start¶
Definition: Time to load WASM module and create instance
Typical: 50-100ms
Components: 1. WASM module compilation: 30ms 2. Instance creation: 10ms 3. Host function setup: 10ms
Optimization (Future): - Module caching (reuse compiled modules) - Warm instance pool - Pre-instantiation on deploy
Invocation Lifecycle¶
1. Trigger fires
├─ HTTP: Client request
├─ Database: WAL commit
├─ Schedule: Cron tick
└─ Webhook: External POST
2. Invoker: Lookup function
└─ Registry.get_by_name(name) or get_by_trigger(trigger)
3. Invoker: Load WASM module
└─ Runtime.load_module(func.wasm_bytes)
4. Invoker: Create instance
└─ Runtime.instantiate(module, host_functions)
5. Invoker: Call handler with timeout
└─ tokio::timeout(func.config.timeout, call_handler(instance, payload))
6. Function: Execute
├─ Parse event payload
├─ Call host functions (db_query, log, etc.)
└─ Return result
7. Invoker: Cleanup
└─ Drop instance, free memory
8. Invoker: Log invocation
└─ INSERT INTO function_invocations (...)
9. Invoker: Return result to caller
Performance Characteristics¶
| Metric | Typical | Target |
|---|---|---|
| Cold start | 80ms | < 100ms |
| Execution overhead | 3ms | < 5ms |
| Memory overhead | 8MB | < 10MB |
| Throughput | 100/s | > 50/s |
| Concurrent invocations | 100 | > 100 |
Examples¶
HTTP Function with Database Access¶
export async function handler(event) {
const { user_id } = event.body;
const user = await db.query(
"SELECT * FROM users WHERE id = $1",
[user_id]
);
return {
statusCode: 200,
body: JSON.stringify(user[0])
};
}
Database Trigger with External API¶
export async function onOrderCreated(event) {
const order = event.new;
log.info(`Processing order ${order.id}`);
// Call external warehouse API
const response = await http.fetch("https://warehouse.example.com/ship", {
method: "POST",
body: JSON.stringify(order)
});
// Update order status
await db.query(
"UPDATE orders SET status = 'shipped' WHERE id = $1",
[order.id]
);
return { shipped: true };
}