Serve over HTTP
Run a worker as a network service over HTTP instead of a co-located subprocess — on another machine, in a container, or behind a shared endpoint.
Prerequisites
Section titled “Prerequisites”- A working worker (see the tutorial).
pip install vgi-python[http].
Serve it
Section titled “Serve it”The same worker that runs over a subprocess also runs over HTTP — add --http:
vgi-serve my_worker.py --http
By default it binds a free port and prints PORT:<port> to stdout for discovery; pass --host and
--port to pin them:
vgi-serve my_worker.py --http --host 0.0.0.0 --port 8080
Attach it from DuckDB
Section titled “Attach it from DuckDB”DuckDB attaches an HTTP worker the same way as a subprocess one — just point LOCATION at the URL:
ATTACH 'calc' (TYPE vgi, LOCATION 'http://localhost:8080');
SELECT calc.double(21);
Call it from Python
Section titled “Call it from Python”You only need the Python Client when calling the worker from Python (tests, scripts, another
service) rather than from SQL. It connects with the HTTP transport instead of spawning a subprocess,
and exposes the same call methods:
# illustrative — calling the worker from Python over HTTP
from vgi.client import Client
with Client(transport="http", base_url="http://localhost:8080") as client:
... # same .scalar_function() / .table_function() calls as the subprocess transport
HTTP is the only transport that authenticates callers (subprocess and Unix-socket workers are co-located and trusted). To gate the service with bearer tokens or JWT/OAuth, see Authentication.
TCP, for trusted networks
Section titled “TCP, for trusted networks”Since 0.8.7 a worker can also serve raw Arrow IPC over TCP — lower overhead than HTTP, because there is no HTTP framing in the path:
vgi-serve my_worker.py --tcp 0.0.0.0:9000
It prints a TCP:<host>:<port> discovery line, and --tcp is mutually exclusive with --http and
--unix. From Python, connect with Client.from_tcp(host, port) (or transport="tcp"); catalog
calls route over TCP too.
Use it on loopback or a trusted network only. HTTP remains the transport for anything untrusted — it’s the one that authenticates.
Going to production
Section titled “Going to production”The HTTP layer — pre-fork hosting, response-size caps, externalized payloads, graceful drain — is
provided by vgi-rpc, the RPC framework VGI runs on. For deployment specifics see vgi-rpc’s
hosting guide. What follows is what vgi-serve itself
exposes on top.
Set a signing key before you run more than one process
Section titled “Set a signing key before you run more than one process”The signing key seals HTTP state tokens and catalog opaque data. If VGI_SIGNING_KEY is unset,
vgi-serve mints an ephemeral one per process — correct for a single process, and silently wrong
for more than one.
A client whose connection stays pinned to one process never notices. One that reconnects mid-stream —
seek_to_token, a load balancer, a respawned worker — hits a token sealed under a key the receiving
process doesn’t have, and gets an intermittent 400 that reads as flakiness. Set the key explicitly
for any load-balanced or multi-instance deployment; nothing in the process can reach a peer it didn’t
start. Since 0.24.0 an ephemeral key is at least announced at WARNING, naming the env var and
the symptom, rather than staying silent as before.
export VGI_SIGNING_KEY="$(openssl rand -hex 32)"
vgi-serve my_worker.py --http --port 8080
Choosing a server
Section titled “Choosing a server”--server waitress (the default) is pure Python. --server granian runs socket I/O and HTTP parsing
in Rust, off the GIL, which on this workload measured 1.9x over the best waitress configuration
from a single process — the win comes from the I/O leaving the interpreter, not from process
fan-out. It’s an optional extra because it’s a compiled extension.
vgi-serve my_worker.py --http --server granian --http-workers 4
--http-workers applies to granian only. Note that granian builds the app inside each forked worker,
so every worker must seal state tokens with the same key — vgi-serve mints and exports one before
forking, but see the warning above for anything spanning more than one machine.
Deployment caps and proxies
Section titled “Deployment caps and proxies”| Flag | What it does |
|---|---|
–max-stream-response-bytes | Soft cap for producer streams — a continuation token carries the overshoot to the next turn. |
–max-externalized-response-bytes | Hard cap on a single externalized response (the payload uploaded to blob storage and replaced on the wire by a pointer). Applies to every method type with no continuation escape — bytes already uploaded can’t be un-uploaded. Set it when a load balancer, API gateway or object-store policy won’t carry an arbitrary body. Default is no cap, and the header’s absence is what tells a client there is no ceiling. |
–prefix | URL prefix for the RPC endpoints. |
–access-log-sample, –access-log-async | Access-log sampling and off-thread emission. |
Two more, for workers behind a reverse proxy that terminates the only public listener:
Worker.resolve_token()— override it to exposePOST {prefix}/__introspect_token__, which resolves an opaque bearer credential to a principal. The route does not exist until you override the hook.VGI_PROXY_PROOF_MODE=require— makescreate_appadvertiseVGI-Proxy-Proof-Required, so a proxy minting proofs can confirm the worker actually enforces them. Without the advertisement,allowandrequireare indistinguishable from the proxy’s side.
The landing page
Section titled “The landing page”Since 0.27.0 an HTTP worker serves a browsable landing page at its root that reads the catalog
over the protocol — schemas, functions, macros, and argument types — instead of the static
describe.json it produced before. Disable it with --no-describe.
Next steps
Section titled “Next steps”- Lock it down → Authentication.
- Drive it from Python → Use VGI from a Python app.
- API Reference →
vgi.serve. - The transport underneath → vgi-rpc.