🌐

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 -readonly for 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 chsql extension's duck_flock macro, you can fan a single SQL statement out across multiple DuckDBs and merge the results.
  • β€’ Glue for DuckDB-Wasm: A browser-side DuckDB can POST to 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: / accepts GET and POST for query execution. /ping is a GET health check. There is no /metrics, no /health, no OpenAPI document β€” the API surface is intentionally small.
  • β€’ Query input: POST the SQL as the request body, or send it as ?query= / ?q=. POST is recommended once the SQL exceeds typical URL length limits.
  • β€’ Response formats: Pass ?default_format=JSONEachRow for newline-delimited JSON (JSONEachRow) or JSONCompact for a single document with meta / data / rows / statistics keys (JSONCompact). The format vocabulary intentionally mirrors the ClickHouse HTTP interface so existing tooling works.
  • β€’ Authentication: The third argument to httpserve_start is one of: empty string (no auth), 'user:pass' (HTTP Basic), or any other value (treated as a single shared key sent in the X-API-Key header). 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 can ATTACH, 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.

This is a thin SQL bridge, not a hardened API gateway

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 can INSERT, ATTACH, COPY TO, install extensions, or call file-system functions (SQL injection is 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:

EndpointMethodsPurpose
/GET, POSTExecute a SQL query, or serve the embedded quackplay UI to a browser.
/pingGETLiveness 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 with meta, data, rows, and statistics keys (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 argumentMode
'' (empty)No auth. Bind to localhost only and run on a trusted network.
'user:pass' (one colon)HTTP Basic auth.
any other valueShared 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 selects the auth mode β€” empty (no auth), user:pass (HTTP Basic), or any other value (shared key in X-API-Key).

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

Scalar Function Server
Signature
httpserve_start(host: VARCHAR, port: INTEGER, auth: VARCHAR) β†’ VARCHAR
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:

  • / β€” GET and POST for SQL queries. Pass SQL in the request body, or as ?query= / ?q=. Add ?default_format=JSONEachRow or ?default_format=JSONCompact to choose the response shape (see ClickHouse formats β€” the names are borrowed from there).
  • /ping β€” GET health check.
  • The browser UI (quackplay) is served from / when accessed with a browser-style request.
Examples
1

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/
2

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
3

No auth β€” only behind a private network

SELECT httpserve_start('127.0.0.1', 9999, '');
4

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

httpserve_stop

Scalar Function Server
Signature
httpserve_stop() β†’ VARCHAR
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
1

Graceful stop

SELECT httpserve_stop();
2

Rotate credential without restarting DuckDB

SELECT httpserve_stop();
SELECT httpserve_start('localhost', 9999, 'rotated-key');

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

Software License MIT
Pricing Free
Written In C++
Source Available Yes
View on GitHub
Usage
7,649+
loads in last 30 days

Platforms

Linux
Linux (musl)
macOS
Windows
WASM
Supported platform architectures
Linux: x86_64, aarch64
Linux (musl): Not available
macOS: Apple Silicon, Intel
Windows: x86_64
WASM: Browser-based

DuckDB Versions

Release calendar
Supported
v1.1.0 v1.1.1 v1.1.2 v1.1.3

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.