Skip to content
Query.Farm
Talk with Us

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.

  • A working worker (see the tutorial).
  • pip install vgi-python[http].

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

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);

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
Want to require authentication?

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.

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.

TCP carries no auth and no encryption

Use it on loopback or a trusted network only. HTTP remains the transport for anything untrusted — it’s the one that authenticates.

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.

The failure is load-dependent, which is what makes it expensive

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

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

FlagWhat it does
–max-stream-response-bytesSoft cap for producer streams — a continuation token carries the overshoot to the next turn.
–max-externalized-response-bytesHard 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.
–prefixURL prefix for the RPC endpoints.
–access-log-sample, –access-log-asyncAccess-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 expose POST {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 — makes create_app advertise VGI-Proxy-Proof-Required, so a proxy minting proofs can confirm the worker actually enforces them. Without the advertisement, allow and require are indistinguishable from the proxy’s side.

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.