Tutorials· beginner

The ClickHouse HTTP interface

Run queries and insert data with plain curl: query parameters, authentication, formats, safe parameter binding, compression, async inserts and error handling.

Every ClickHouse server speaks HTTP, by default on port 8123 (8443 for TLS). Anything that can make a request, from curl to a serverless function, can query and load data without a driver. This article covers the parts worth knowing; the examples need a running server, so they are not run here.

Replace clickhouse.example.com and the credentials with your own.

Read: the query in the URL or the body

A GET request is read-only. The query goes in the query parameter:

curl 'http://clickhouse.example.com:8123/?query=SELECT%201'

URL encoding is tedious. Use --data-urlencode and let curl do it, or send the query as a POST body, which has no length limit and is the normal way:

curl 'http://clickhouse.example.com:8123/' --data-binary 'SELECT count() FROM analytics.events'

POST queries may modify data. A GET is rejected for anything but reads, which makes GET a cheap safety net for read-only tools.

Pick an output format

The result is in TabSeparated unless told otherwise. Either put FORMAT in the query or set the default_format parameter:

curl 'http://clickhouse.example.com:8123/?default_format=JSONEachRow' \
     --data-binary 'SELECT event_type, count() AS n FROM analytics.events GROUP BY event_type'

JSONEachRow, CSVWithNames, Parquet, ArrowStream and Native are the popular ones. Binary formats work too (curl -o out.parquet).

Authenticate

Basic auth or two headers; never put passwords in the URL, because URLs end up in logs.

curl -u 'analyst:<password>' 'http://clickhouse.example.com:8123/' --data-binary 'SELECT 1'

curl -H 'X-ClickHouse-User: analyst' -H 'X-ClickHouse-Key: <password>' \
     'http://clickhouse.example.com:8123/' --data-binary 'SELECT 1'

Use HTTPS on any network you do not control.

Choose a database and settings per request

Any ClickHouse setting can be a URL parameter. database selects the default database:

curl 'http://clickhouse.example.com:8123/?database=analytics&max_execution_time=10' \
     --data-binary 'SELECT count() FROM events'

A setting the user is not allowed to change fails the request, so the URL cannot escape the profile’s constraints.

Bind parameters instead of building strings

Never concatenate user input into a query. ClickHouse has typed query parameters: declare a placeholder as {name:Type} and pass param_name=value:

curl 'http://clickhouse.example.com:8123/?param_country=DE&param_since=2026-01-01' \
     --data-binary 'SELECT count() FROM analytics.events
                    WHERE country = {country:String} AND event_time >= {since:Date}'

The server parses the value according to the declared type, so quoting and SQL injection are not your problem.

Insert data

Send the data in the body and name the table and format in the query parameter. Pipe a file with --data-binary @-:

echo '{"event_time":"2026-01-01 00:00:00","user_id":1,"event_type":"view"}' |
curl 'http://clickhouse.example.com:8123/?query=INSERT%20INTO%20analytics.events%20(event_time,user_id,event_type)%20FORMAT%20JSONEachRow' \
     --data-binary @-

Use --data-binary, not -d: -d strips newlines, which corrupts row-delimited formats.

Everything about insert batching applies: send many rows per request. If many small producers each send a few rows, turn on asynchronous inserts.

Asynchronous inserts over HTTP

Let the server buffer small inserts and write them together:

echo '{"a": "asd"}' |
curl 'http://clickhouse.example.com:8123/?query=INSERT%20INTO%20demo%20FORMAT%20JSONEachRow&async_insert=1&wait_for_async_insert=1&async_insert_busy_timeout_ms=5000' \
     --data-binary @-
  • async_insert=1 buffers the insert.
  • wait_for_async_insert=1 holds the HTTP response until the buffer is flushed to storage, so a 200 means the data is durable. Set it to 0 only if you accept losing the buffered data on failure.
  • async_insert_busy_timeout_ms bounds how long a buffer waits before flush. Check your version’s system.settings: newer releases have adaptive timeouts (async_insert_use_adaptive_busy_timeout, async_insert_busy_timeout_min_ms, async_insert_busy_timeout_max_ms).

See Async inserts explained for the full picture.

Compression

Ask for a compressed response with a standard header and enable it for the request:

curl --compressed 'http://clickhouse.example.com:8123/?enable_http_compression=1' \
     --data-binary 'SELECT * FROM analytics.events LIMIT 100000'

For uploads, send Content-Encoding: gzip (or zstd, br) with a compressed body.

Errors: the status code is not always the truth

ClickHouse streams results. By the time an error happens halfway through a long SELECT, the 200 status line and part of the body are already gone. Options:

  • wait_end_of_query=1 (with http_response_buffer_size for large results) makes the server buffer the response and return a real error status; the price is memory and latency.
  • Check the X-ClickHouse-Exception-Code header and look for the exception text at the end of the body.

Also useful: X-ClickHouse-Query-Id in the response, so you can find the request in system.query_log, and /ping for a load-balancer health check (it returns Ok.).

Long queries and progress

For a long-running query set send_progress_in_http_headers=1 to get X-ClickHouse-Progress headers while it runs, which keeps proxies from idling out the connection and lets a client show a progress bar. Mind the timeouts of anything between you and the server.

httpcurlinserts

Discussion

Comments live in GitHub Discussions. Sign in with GitHub to join in.