Overview

WolfFunctions is WolfStack’s built-in serverless / functions-as-a-service platform. Instead of provisioning an LXC, Docker container, or VM, you write (or have AI generate) a small handler function, and WolfStack keeps warm, sandboxed copies of it running across your cluster nodes. Any node can serve an invocation, and losing a node changes nothing — the function definition is replicated everywhere and warm copies are automatically re-placed.

It is the self-hosted equivalent of AWS Lambda or Google Cloud Functions, built directly into the WolfStack binary with zero external dependencies — no cloud account, no separate control plane.

WolfFunctions dashboard — function list, warm instances, and per-node runtime health across the cluster

Key Features

  • Strong isolation — every function runs inside a gVisor sandbox (the same userspace-kernel isolation Google Cloud Functions uses), not a bare process. A misbehaving function can’t reach the host kernel.
  • Runs anywhere — gVisor’s runsc is a single static binary that needs no hardware virtualization, so functions work on cloud VMs and VPS instances where KVM isn’t available. A Docker execution fallback covers nodes where runsc can’t run (clearly labelled as reduced isolation).
  • Warm by default — configurable provisioned concurrency keeps instances hot, so invocations don’t pay a cold start. Optional scale-to-zero for rarely-used functions.
  • Cluster-wide HA — function definitions replicate to every node; the cluster leader places warm replicas across nodes. If a node fails, other nodes keep serving and the leader re-places.
  • Multiple runtimes — Python 3.12 and Node.js 22, with the runtime layer built so more can be added.
  • Triggers — an authenticated invoke API, an optional public HTTP URL per function, cron-style schedules, and thirteen internal WolfStack events (alerts, node up/down, backups, monitor state changes, container failover and image updates, UPS power events).
  • Create with AI — describe what the function should do and the configured AI assistant drafts the handler for you to review before deploying.
  • Per-function limits — memory, timeout, replica count, and max concurrency per node, all Lambda-style.
  • Live testing & logs — test-invoke from the UI and see each invocation’s result, duration, and captured print/console.log output.

How WolfFunctions Works

Each function is a single handler file. In Python:

def handler(event, context):
    # event   = the trigger payload (dict, or None)
    # context = { function, version, memory_mb, node, trigger }
    return {"greeting": f"hello {event.get('name', 'world')}"}

In Node.js:

exports.handler = async (event, context) => {
    return { greeting: `hello ${event?.name ?? 'world'}` };
};

When a function is invoked, WolfStack claims a warm sandbox on the serving node (or cold-starts one), passes it the event over the node’s loopback, and returns the handler’s JSON result. Each sandbox handles one request at a time (Lambda semantics); concurrent invocations burst extra sandboxes up to the per-node limit and idle ones are reaped.

Cluster model

WolfFunctions uses the same leaderless replication as WolfStack’s status pages: every node holds the full set of function definitions, and changes are broadcast to same-cluster peers. The cluster leader (lowest node ID among online nodes) runs a reconciliation loop that:

  1. Places replicas — picks the healthiest nodes (by load) to keep a warm copy of each function, honouring its replica count.
  2. Warms instances — every node ensures a warm sandbox exists for each function placed on it.
  3. Fires schedules — invokes functions whose interval has elapsed.
  4. Re-places on failure — when a node goes offline, its functions are re-placed onto surviving nodes.

Invocation routing: a node serves a function locally if it holds a warm copy, otherwise it forwards to a node that does; if every placed node is unreachable mid-failover, it cold-starts locally so the call still succeeds.

A WolfFunctions test invocation returning a JSON result from inside a sandbox

Creating a Function

Open the WolfFunctions page under a cluster in the sidebar and click New Function.

WolfFunctions editor — handler code, Generate-with-AI box, memory/timeout/replica limits, and triggers
  1. Enter a name (lowercase letters, digits, and hyphens).
  2. Pick a runtime (Python 3.12 or Node.js 22).
  3. Write the handler in the code editor — or type a description into the Generate box and let AI draft it for you to review.
  4. Set memory (MB), timeout (seconds), replicas (how many nodes keep a warm copy), and max per node (burst concurrency).
  5. Optionally add environment variables (KEY=VALUE per line), a public URL slug, a schedule, and event triggers.
  6. Click Create, then Test to invoke it.

Triggers

  • Manual / APIPOST /api/wolffunctions/<id>/invoke with the event as the JSON body (authenticated).
  • Public HTTP — give the function a slug and it’s reachable, no auth, at /fn/<slug>. This is opt-in per function and rate-limited; the handler receives the method, query string, body, and source IP.
  • Schedule — run every N seconds (minimum 60).
  • Events — run on internal WolfStack events. Thirteen are available:
    • alert_fired — any WolfStack alert was raised.
    • node_offline / node_online — a cluster node went down or came back.
    • backup_completed / backup_failed — a backup job finished or failed.
    • monitor_down / monitor_up — a status-page monitor entered Down (3 consecutive failures) or recovered.
    • container_failover — WolfRun promoted a standby after an instance failed.
    • container_updated / container_update_failed — the image watcher recreated a container on a new image digest, or the update attempt failed / rolled back.
    • ups_on_battery / ups_online / ups_stage_firedUPS Power events: mains lost, mains restored, or a staged-shutdown threshold fired (payload names the stage percentage and its actions).

Using WolfFunctions in the real world

The most common use is a public JSON API: you give a function a public slug and it becomes callable over HTTP with no authentication. Whatever your handler returns is sent straight back as the JSON response body. That makes WolfFunctions a fast way to stand up webhooks, small APIs, form handlers, and glue endpoints without deploying a server or wiring up a framework.

Exposing a function as a web API

Set a Public URL slug on the function (say convert). It is then callable at:

https://<node>:8553/fn/convert
  • WolfStack serves HTTPS on port 8553 by default (a self-signed certificate until you issue a real one from the Certificates page, or terminate TLS at a reverse proxy).
  • Both GET and POST are accepted. Your handler’s event contains method, query (the raw query string), body (parsed JSON when the request sent JSON, otherwise the raw text), and source_ip.
  • The handler’s return value is sent back as an application/json 200 response. A handler error returns a generic 500 (details stay in the invocation log, never leaked to the caller); an unknown or disabled slug returns 404.
  • Each node rate-limits a slug to 60 requests/minute.

Note: the response is plain JSON with no CORS headers, so it is ideal for server-to-server calls, webhooks, and same-origin requests. To call it from browser JavaScript on a different origin, add CORS headers at a reverse proxy (WolfProxy / nginx).

On a 3-node cluster with public IPs

Because a function’s definition replicates to every node, the public endpoint is live on all of them at once. On a three-node cluster with external IPs (say 203.0.113.10, .11, and .12) the same function answers on every node:

https://203.0.113.10:8553/fn/convert
https://203.0.113.11:8553/fn/convert
https://203.0.113.12:8553/fn/convert

Set replicas to cover your nodes (e.g. 3 on a 3-node cluster) so each node keeps a warm copy and answers instantly. If a request lands on a node the function isn’t placed on, that node transparently forwards it to one that is.

To give it a single, stable URL, put the three IPs behind one of:

  • DNS round-robin — point api.example.com at all three external IPs (three A records). Clients spread across the nodes, and if one is down they retry another.
  • A reverse proxy — front the cluster with WolfProxy (or WolfRouter / nginx) on :443 and proxy /fn/* to the WolfStack port. You get a clean https://api.example.com/fn/convert, real-certificate TLS, and one place to add CORS, caching, or extra rate-limiting.

High availability is automatic. Every node serves the endpoint and every node either holds or can reach a warm replica, so losing a node simply sends traffic to a survivor — no cold start, no downtime.

Example 1 — A public JSON API (GET)

A temperature converter that reads query parameters and returns computed JSON. Python 3.12 · public slug convert.

import urllib.parse

def handler(event, context):
    params = urllib.parse.parse_qs(event.get("query", ""))
    celsius = float(params.get("c", ["0"])[0])
    return {
        "celsius": celsius,
        "fahrenheit": round(celsius * 9 / 5 + 32, 2),
        "kelvin": round(celsius + 273.15, 2),
    }
$ curl "https://api.example.com/fn/convert?c=100"
{"celsius": 100.0, "fahrenheit": 212.0, "kelvin": 373.15}

Example 2 — A webhook that relays to Slack (POST)

A public endpoint your other systems POST to; it forwards a formatted message to a Slack (or Discord) incoming webhook. The webhook URL lives in an environment variable, not in the code. Python 3.12 · public slug notify · env SLACK_WEBHOOK=https://hooks.slack.com/services/…

import json, os, urllib.request

def handler(event, context):
    body = event.get("body") or {}
    if not isinstance(body, dict):
        return {"ok": False, "error": "send a JSON body"}
    text = body.get("message", "(no message)")
    payload = json.dumps({"text": f":bell: {text}"}).encode()
    req = urllib.request.Request(
        os.environ["SLACK_WEBHOOK"], data=payload,
        headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req, timeout=8)
    return {"ok": True}
$ curl -X POST https://api.example.com/fn/notify \
    -H "Content-Type: application/json" \
    -d '{"message": "deploy finished on prod"}'
{"ok": true}

Example 3 — A contact-form handler (POST)

Validates a JSON form submission and relays it to a chat webhook, returning a clear error for bad input so your front-end can show it. Python 3.12 · public slug contact · env FORM_WEBHOOK.

import json, os, urllib.request

def handler(event, context):
    form = event.get("body") or {}
    if not isinstance(form, dict):
        return {"ok": False, "error": "send JSON"}
    email = (form.get("email") or "").strip()
    message = (form.get("message") or "").strip()
    if "@" not in email or not message:
        return {"ok": False, "error": "email and message are required"}
    hook = os.environ.get("FORM_WEBHOOK")
    if hook:
        note = json.dumps({"content": f"Contact from {email}:\n{message}"}).encode()
        urllib.request.urlopen(urllib.request.Request(
            hook, data=note, headers={"Content-Type": "application/json"}), timeout=8)
    return {"ok": True}

Example 4 — A compute API in Node.js (POST)

Accepts a list of numbers and returns summary statistics. Shows the Node.js runtime and context.node (which node served the request). Node.js 22 · public slug stats.

exports.handler = async (event, context) => {
    const body = event.body || {};
    const nums = Array.isArray(body.numbers) ? body.numbers : [];
    if (!nums.length) return { error: "POST { numbers: [ ... ] }" };
    const sum = nums.reduce((a, b) => a + b, 0);
    return {
        count: nums.length,
        sum,
        mean: sum / nums.length,
        min: Math.min(...nums),
        max: Math.max(...nums),
        served_by: context.node,
    };
};
$ curl -X POST https://api.example.com/fn/stats \
    -H "Content-Type: application/json" \
    -d '{"numbers": [4, 8, 15, 16, 23, 42]}'
{"count": 6, "sum": 108, "mean": 18, "min": 4, "max": 42, "served_by": "node-2"}

For internal, service-to-service calls you don’t want exposed publicly, skip the slug and use the authenticated endpoint instead: POST /api/wolffunctions/<id>/invoke with a session cookie or an API key.

Runtime Requirements

  • Docker is used to build each runtime’s root filesystem (from the official python:3.12-slim / node:22-slim images), so a function-capable node needs Docker installed. Nodes without it are shown as not function-capable.
  • gVisor runsc is downloaded automatically from Google’s official release bucket (checksum-verified) and cached per node. Builds are available for x86_64 and arm64.
  • The runtime layer is prepared lazily and shared read-only across every sandbox of that runtime, so cold starts after the first are fast.
  • Each node’s execution mode is configurable: Auto (prefer gVisor, fall back to Docker), gVisor, or Docker. The UI always shows which mode a node is using and why.

Only the standard library of each runtime is available inside a function — there is no package installation step in this release. Functions are intended for glue logic, webhooks, scheduled jobs, and event handlers rather than heavyweight applications (use WolfRun for long-running services).

Security

  • Handlers run inside a gVisor sandbox with a read-only root filesystem, a memory limit, and a per-invocation timeout. Only the function’s own code directory and a small writable scratch area are mounted.
  • The public /fn/<slug> endpoint is off by default and must be enabled per function. Errors returned to public callers are generic — internal details go only to the invocation log.
  • Public triggers are rate-limited per node.
  • Function names, slugs, and environment values are validated and never interpolated into a shell.

Troubleshooting

A node shows “not function-capable”

  • Install Docker on the node — it’s required to prepare the runtime images.
  • If the node is set to gVisor mode but runsc can’t run, either switch it to Auto/Docker mode or check the node can reach Google’s release bucket to download runsc.

First invocation is slow

  • The very first call on a fresh node prepares the runtime (downloads runsc, builds the root filesystem). Set replicas to 1 or more so the reconciler pre-warms the function instead of paying that cost on the first request.

An invocation returns an error

  • Open the function’s Logs — each invocation records its result, duration, node, and the handler’s captured output and traceback.
Esc