HTTP Server
Expose a running DuckDB process over HTTP so clients can POST a SQL query and read JSON results. Single shared key or HTTP Basic auth, an embedded query UI, and zero-config startup. Best fit: small internal API surfaces and ad-hoc dashboards in front of a read-only DuckDB β not a hardened multi-tenant gateway.
Install
-- Install the extension
INSTALL httpserver FROM community;
-- Load it into your session
LOAD httpserver;
-- Start an HTTP API on localhost:9999 with Basic auth
INSTALL httpserver FROM community;
LOAD httpserver;
SELECT httpserve_start('localhost', 9999, 'user:pass');
-- Any HTTP client can now POST queries:
-- curl -X POST -d "SELECT version()" http://user:pass@localhost:9999/
-- Stop when done
SELECT httpserve_stop(); Technical Overview
Why Run DuckDB Behind HTTP?
Wrap a single DuckDB process in an HTTP endpoint so other services can POST SQL and read JSON back β no client driver, no separate query gateway. Best fit: a read-only DuckDB serving small internal APIs, BI dashboards, or DuckDB-Wasm in a browser. Not a substitute for a hardened multi-tenant SQL gateway.
π What this extension is for
Two SQL functions β httpserve_start and httpserve_stop β turn a running DuckDB into a tiny HTTP query service. The endpoint accepts SQL via POST body or ?query= parameter, returns JSON, and ships an embedded browser UI called quackplay.
- β’ Internal SQL APIs: Stand up a JSON query endpoint over a curated DuckDB database file so other services can pull aggregates without a client library. Pair with
duckdb -readonlyfor safe shared access. - β’ Ad-hoc browser exploration: The built-in quackplay UI lets a teammate paste a query into a browser tab and see results β useful when shipping a DuckDB file is easier than shipping a BI tool.
- β’ Cross-DuckDB query federation: Each DuckDB process is reachable over HTTP from any other DuckDB. Combined with the
chsqlextension'sduck_flockmacro, you can fan a single SQL statement out across multiple DuckDBs and merge the results. - β’ Glue for DuckDB-Wasm: A browser-side DuckDB can
POSTto a server-side DuckDB through this extension to pull canonical data without bundling it into the page.
π How it works
httpserve_start spawns an HTTP listener thread inside the DuckDB process, owned by the calling connection. Requests are dispatched to the same DuckDB connection, so any state the connection has (LOADed extensions, attached databases, secrets) is visible to HTTP clients. There is one global server per process β calling httpserve_start twice replaces the listener.
- β’ Endpoints:
/acceptsGETandPOSTfor query execution./pingis aGEThealth check. There is no/metrics, no/health, no OpenAPI document β the API surface is intentionally small. - β’ Query input:
POSTthe SQL as the request body, or send it as?query=/?q=.POSTis recommended once the SQL exceeds typical URL length limits. - β’ Response formats: Pass
?default_format=JSONEachRowfor newline-delimited JSON (JSONEachRow) orJSONCompactfor a single document withmeta/data/rows/statisticskeys (JSONCompact). The format vocabulary intentionally mirrors the ClickHouse HTTP interface so existing tooling works. - β’ Authentication: The third argument to
httpserve_startis one of: empty string (no auth),'user:pass'(HTTP Basic), or any other value (treated as a single shared key sent in theX-API-Keyheader). There is one credential per server.
π‘οΈ Production caveats
Exposing a SQL engine over HTTP is genuinely risky if you skip the boring parts. Read these before binding to a public interface.
- β’ Run DuckDB read-only: The endpoint executes whatever SQL it receives against the connection's full surface. Without
-readonly(see the DuckDB CLI docs), an authenticated client canATTACH,INSERT,COPY TO, install extensions, or call file-system functions. The upstream README's first security note is exactly this. - β’ No built-in TLS: The server speaks plain HTTP. Credentials and query results travel in the clear unless you front it with a TLS terminator like nginx, Caddy, or HAProxy. HTTP Basic without TLS is effectively no auth at all.
- β’ One credential, no per-user scope: Auth is binary: a request either presents the configured credential or it doesn't. There is no notion of users, roles, query allowlists, or rate limiting. For multi-tenant access, terminate auth at a gateway and run the server unauthenticated behind it on a private network.
- β’ Lock down the DuckDB itself: Even with
-readonly, follow DuckDB's hardening guidance β disable community extension installation, restrict filesystem access, and consider running the process inside a sandbox or container. - β’ Single process, no rate limiting: Concurrency and throughput are bounded by what one DuckDB process can do. There is no built-in request queue, no per-client throttling, and no slow-query cutoff. Put a reverse proxy in front for any of those.
π― Common Use Cases
Read-only API in front of a curated DuckDB
Build a Parquet / CSV pipeline into a DuckDB file, attach it -readonly, httpserve_start on a private port, and let internal services POST aggregates. Front with TLS at the proxy.
Quick exploratory UI for a teammate
Open quackplay in a browser tab, paste queries, hand someone a URL. Cheaper than provisioning a BI seat for one-off questions.
Cross-DuckDB federation with duck_flock
Run httpserve_start on N DuckDB nodes, each exposing a slice of data, then use the chsql extension's duck_flock from a coordinator DuckDB to fan queries out and merge results.
Backend for DuckDB-Wasm
Let a browser DuckDB call read_json_auto('http://server:9999/?q=...') to pull authoritative data on demand instead of bundling it into the bundle.
Deep Dive
Technical Details
What you can do with one query
Turn a DuckDB process into an HTTP query API in one statement:
INSTALL httpserver FROM community;
LOAD httpserver;
SELECT httpserve_start('localhost', 9999, 'user:pass');
-- 'HTTP server started on localhost:9999'
Now any HTTP client can POST SQL and read JSON back:
curl -X POST -d "SELECT version()" \
"http://user:pass@localhost:9999/?default_format=JSONCompact"
httpserve_start spawns an in-process listener thread; httpserve_stop tears it down. There are no other functions β the entire extension is two SQL calls and a small set of HTTP endpoints.
Exposing a DuckDB connection over HTTP is genuinely risky. Treat this extension as a building block, not a finished product. Before binding to anything beyond localhost:
- Run DuckDB with
-readonly. Without it, any authenticated client canINSERT,ATTACH,COPY TO, install extensions, or call file-system functions (SQL injectionis the entire API here β there is no safe parameterization path). The upstream READMEβs first security note is exactly this. - Front it with TLS. The server speaks plain HTTP. HTTP Basic over plaintext is effectively no auth. Use nginx, Caddy, or HAProxy as a TLS terminator.
- One credential, no per-user scope. Auth is binary β the client either presents the configured key or it doesnβt. No users, no roles, no per-query allowlists, no rate limiting.
- Lock down the DuckDB process itself. Follow DuckDBβs hardening guidance and consider disabling community-extension installation on the served process.
For internal APIs on a private network, behind a TLS proxy, against a read-only database β this extension is a great fit. For anything multi-tenant or internet-exposed, a dedicated SQL gateway is the right tool.
Architecture
httpserve_start starts an HTTP listener thread inside the DuckDB process. Requests are dispatched to the same DuckDB connection that called the function β so any extension youβve LOADed, any database youβve ATTACHed, and any secret youβve CREATEd is visible to HTTP clients. There is one global server per DuckDB process.
The endpoint surface is intentionally small:
| Endpoint | Methods | Purpose |
|---|---|---|
/ | GET, POST | Execute a SQL query, or serve the embedded quackplay UI to a browser. |
/ping | GET | Liveness check. |
There is no /metrics, no /health JSON, and no OpenAPI document β anything beyond send SQL, read JSON lives outside this extension.
The response format vocabulary mirrors the ClickHouse HTTP interface:
?default_format=JSONEachRowβ newline-delimited rows (JSONEachRow).?default_format=JSONCompactβ single document withmeta,data,rows, andstatisticskeys (JSONCompact).
Three environment variables tune the runtime: DUCKDB_HTTPSERVER_FOREGROUND=1 runs the server on the main thread (so the SQL caller blocks), DUCKDB_HTTPSERVER_DEBUG=1 enables stdout request logging, and DUCKDB_HTTPSERVER_SYSLOG=1 routes logs to syslog.
Authentication
The third argument to httpserve_start selects the auth mode:
auth argument | Mode |
|---|---|
'' (empty) | No auth. Bind to localhost only and run on a trusted network. |
'user:pass' (one colon) | HTTP Basic auth. |
| any other value | Shared key sent by clients in the X-API-Key header. |
Both auth modes are single-credential β there is one user/pass or one key per server. Multi-tenant or per-user access control belongs at a reverse proxy. The credential is checked in constant time but travels in plaintext over HTTP, so TLS in front is non-negotiable for anything beyond a loopback bind.
To rotate a credential without restarting DuckDB, call httpserve_stop and then httpserve_start again with the new value.
Scoping the served database
The HTTP endpoint inherits the DuckDB connectionβs surface in full. The most reliable way to make that surface safe is at the DuckDB level, not the HTTP level:
# Read-only β INSERT / UPDATE / COPY TO / ATTACH all fail at SQL parse time.
duckdb -readonly mydata.duckdb -c "
INSTALL httpserver FROM community;
LOAD httpserver;
SELECT httpserve_start('0.0.0.0', 9999, 'shared-api-key');
CALL pg_sleep(86400);
"
See the DuckDB CLI docs and the securing DuckDB page for the full lockdown vocabulary β read-only mode, restricting extension installation, and disabling filesystem functions.
Calling the server from another DuckDB
Any DuckDB can hit the endpoint as an HTTP source. Configure an extra_http_headers secret for the API key, then use read_json_auto, or pair with the chsql community extensionβs duck_flock macro to fan a single SQL statement out across multiple DuckDB endpoints:
CREATE SECRET extra_http_headers (
TYPE HTTP,
EXTRA_HTTP_HEADERS MAP { 'X-API-Key': 'supersecretkey' }
);
-- Fan-out across three DuckDB nodes; results are unioned.
SELECT * FROM duck_flock(
'SELECT version()',
['http://node-a:9999', 'http://node-b:9999', 'http://node-c:9999']
);
This is the closest pattern to βdistributed DuckDBβ that exists today β small, no consensus layer, no shared catalog, but adequate for read-side federation across curated slices.
Compared to alternatives
- The DuckDB CLI over SSH β works for one operator and trivial to set up, but doesnβt help when several services need to query the same DuckDB. The HTTP server replaces βSSH and run a queryβ with βPOST a query.β
- Hand-rolled FastAPI / Express service β more flexible (per-user auth, request validation, custom response shapes) but adds another runtime to deploy. This extension is the right fit when the SQL is the API.
- Airport over Arrow Flight β the right tool when both ends are inside a data system and you want columnar Arrow on the wire. Use httpserver when the client is a generic HTTP consumer (a browser, a Lambda, a curl in a cron job).
- ADBC Scanner or Airport outbound β for querying something else from DuckDB. httpserver is the inverse direction: making this DuckDB queryable.
Cookbook
Recipes β start the server, query it from curl, query it from another DuckDB, run quackplay β live in the Cookbook section below.
Reference
Extension Contents
Quick reference to all available functions and settings organized by category.
| Name | Description | |
|---|---|---|
| Server Start and stop the embedded HTTP query listener. The third argument to | ||
| httpserve_start() | Start the embedded HTTP query server | |
| httpserve_stop() | Stop the running HTTP server thread cleanly | |
API Reference
Function Documentation
Detailed documentation for each function including signatures, parameters, and examples.
httpserve_start
Signature
Parameters (Positional)
| Parameter | Type | Mode | Description |
|---|---|---|---|
host | VARCHAR | Positional | Interface to bind on. Use 'localhost' (or '127.0.0.1') to keep the server reachable only from the same machine. Use '0.0.0.0' to listen on all interfaces β only do this behind a firewall or reverse proxy. |
port | INTEGER | Positional | TCP port to listen on. Any unused port; 9999 is conventional in the upstream examples. |
auth | VARCHAR | Positional | Authentication selector. Empty string disables auth (development only). A value containing ':' is parsed as user:password and enables HTTP Basic auth. Any other value is treated as a single shared key the client must send in the X-API-Key header. There is one credential per server β no per-user accounts. |
Returns
Confirmation string of the form 'HTTP server started on <host>:<port>'.
Description
Start the embedded HTTP query server. The listener runs in a background thread inside the DuckDB process and dispatches requests to the calling connection β extensions you've LOADed and databases you've ATTACHed are visible to HTTP clients.
There is one global server per DuckDB process: calling httpserve_start again replaces the running listener. Set DUCKDB_HTTPSERVER_FOREGROUND=1 before starting DuckDB to run the server on the main thread (the SQL caller will block); set DUCKDB_HTTPSERVER_DEBUG=1 for stdout request logging or DUCKDB_HTTPSERVER_SYSLOG=1 to route logs to syslog.
The served endpoints are:
/βGETandPOSTfor SQL queries. Pass SQL in the request body, or as?query=/?q=. Add?default_format=JSONEachRowor?default_format=JSONCompactto choose the response shape (see ClickHouse formats β the names are borrowed from there)./pingβGEThealth check.- The browser UI (quackplay) is served from
/when accessed with a browser-style request.
Examples
HTTP Basic auth on localhost
SELECT httpserve_start('localhost', 9999, 'user:pass');
-- 'HTTP server started on localhost:9999'
-- From the shell:
-- curl -X POST -d "SELECT version()" http://user:pass@localhost:9999/ Single shared key (X-API-Key header)
SELECT httpserve_start('localhost', 9999, 'supersecretkey');
-- From the shell:
-- curl -X POST --header 'X-API-Key: supersecretkey' \
-- -d 'SELECT 42' http://localhost:9999/?default_format=JSONCompact No auth β only behind a private network
SELECT httpserve_start('127.0.0.1', 9999, ''); Bind on all interfaces β read-only DuckDB, behind a TLS-terminating proxy
-- Launched as: duckdb -readonly mydata.duckdb
SELECT httpserve_start('0.0.0.0', 9999, 'shared-api-key'); Related Functions
httpserve_stop
Signature
Parameters
| Parameter | Type | Mode | Description |
|---|
Returns
Confirmation string indicating the listener has been stopped.
Description
Stop the running HTTP server thread cleanly. Pair with httpserve_start when you want to rebind on a different port or rotate the auth credential without restarting DuckDB.
If the DuckDB process exits, the server thread is torn down with it β calling httpserve_stop is not strictly required for shutdown, only for in-process restarts.
Examples
Graceful stop
SELECT httpserve_stop(); Rotate credential without restarting DuckDB
SELECT httpserve_stop();
SELECT httpserve_start('localhost', 9999, 'rotated-key'); Related Functions
Practical Examples
Cookbook
Real-world recipes and patterns for common use cases.
Start the server
INSTALL httpserver FROM community;
LOAD httpserver;
SELECT httpserve_start('localhost', 9999, 'user:pass');
-- 'HTTP server started on localhost:9999'
The third argument to httpserve_start selects the auth mode β empty disables auth, user:pass enables HTTP Basic, anything else is treated as a shared key in X-API-Key.
Query from curl with HTTP Basic auth
curl -X POST -d "SELECT version()" \
"http://user:pass@localhost:9999/?default_format=JSONCompact"
Body is the SQL; response is JSONCompact β meta, data, rows, statistics.
Query from curl with a shared API key
SELECT httpserve_start('localhost', 9999, 'supersecretkey');
curl -X POST --header "X-API-Key: supersecretkey" \
-d "SELECT 'hello', version()" \
"http://localhost:9999/?default_format=JSONEachRow"
JSONEachRow returns newline-delimited objects β see the ClickHouse format reference for the exact shape.
Pass SQL as a query parameter
GET / ?query= works for short queries (mind URL length limits):
curl --header "X-API-Key: supersecretkey" \
"http://localhost:9999/?q=SELECT%2042&default_format=JSONCompact"
Pull JSON results into another DuckDB
A second DuckDB can read the endpoint with read_json_auto, passing the API key via an extra_http_headers secret:
LOAD httpfs;
LOAD json;
CREATE SECRET extra_http_headers (
TYPE HTTP,
EXTRA_HTTP_HEADERS MAP { 'X-API-Key': 'supersecretkey' }
);
SELECT * FROM read_json_auto(
'http://localhost:9999/?q=SELECT version()&default_format=JSONEachRow'
);
Fan a query across multiple DuckDB nodes
The chsql community extension provides a duck_flock macro that broadcasts one SQL statement to several HTTP endpoints and unions the results:
INSTALL chsql FROM community;
LOAD chsql;
CREATE SECRET extra_http_headers (
TYPE HTTP,
EXTRA_HTTP_HEADERS MAP { 'X-API-Key': 'supersecretkey' }
);
SELECT *
FROM duck_flock(
'SELECT now() AS server_time, version()',
['http://node-a:9999', 'http://node-b:9999', 'http://node-c:9999']
);
Run quackplay in the browser
SELECT httpserve_start('localhost', 9999, 'user:pass');
Open http://localhost:9999/ in a browser. Authenticate, then paste queries into the embedded quackplay SQL UI. Useful for ad-hoc exploration without a separate client. (The UI is marked experimental upstream.)
Serve a read-only DuckDB to a private network
duckdb -readonly mydata.duckdb -c "
INSTALL httpserver FROM community;
LOAD httpserver;
SELECT httpserve_start('0.0.0.0', 9999, 'shared-api-key');
CALL pg_sleep(86400);
"
-readonly is the simplest hard guarantee against mutation through the HTTP endpoint β see the DuckDB CLI docs.
Foreground mode and logging
Set these in the environment before launching DuckDB:
DUCKDB_HTTPSERVER_FOREGROUND=1 # Run the listener on the main thread (the SQL caller blocks).
DUCKDB_HTTPSERVER_DEBUG=1 # Verbose request logging to stdout.
DUCKDB_HTTPSERVER_SYSLOG=1 # Route logs to syslog.
FOREGROUND=1 is the simplest way to keep the process alive when launching from a one-shot duckdb -c '...' invocation in a container.
Health check
curl -i http://localhost:9999/ping
/ping is unauthenticated and returns a small 200 OK β wire it to a load balancer or docker healthcheck.
Rotate the credential
SELECT httpserve_stop();
SELECT httpserve_start('localhost', 9999, 'rotated-key');
httpserve_stop tears down the listener; the next httpserve_start can rebind on a different port or with a different credential without restarting DuckDB.
Stop the server
SELECT httpserve_stop();
Or just exit the DuckDB process β the server thread shuts down with it.
Platform Support
Compatibility
Extension availability may vary by platform and DuckDB version. Check below to ensure this extension supports your environment before installation.
Quick Facts
Platforms
Supported platform architectures
DuckDB as a small HTTP query API
Install httpserver to put a SQL endpoint in front of a DuckDB instance β single-key or Basic auth, embedded UI, no extra infrastructure.