How to Connect to ClickHouse from Node.js
ClickHouse has one official Node.js client, @clickhouse/client, written in TypeScript with zero dependencies and tested against real ClickHouse versions. Unlike Postgres or MySQL, there's no field of competing third-party drivers worth weighing -- the official package is the answer for server-side Node. This guide shows working code for queries, inserts, and streaming, plus the configuration that trips people up first.
The package
npm install @clickhouse/clientThere are two published clients, and picking the wrong one is the first mistake:
| Package | Use it for | Transport |
|---|---|---|
@clickhouse/client | Node.js servers, scripts, backends | HTTP, with Node streams |
@clickhouse/client-web | Browsers, edge runtimes, Cloudflare Workers | HTTP via fetch/Web Streams |
For a normal Node backend, always import from @clickhouse/client. The -web variant exists for environments without Node's stream APIs and lacks some features (no compression for inserts, web streams instead of Node streams). Current version is 1.20.0 (as of June 2026).
Note the client speaks ClickHouse's HTTP interface (port 8123), not the native TCP protocol (port 9000). This matters below.
Connecting and querying
import { createClient } from "@clickhouse/client";
const client = createClient({
url: "http://localhost:8123",
username: "default",
password: "",
database: "default",
});
const resultSet = await client.query({
query: "SELECT name, value FROM events WHERE value > {threshold:UInt32}",
query_params: { threshold: 100 },
format: "JSONEachRow",
});
const rows = await resultSet.json();
console.log(rows);
await client.close();Two things to internalize:
format decides what .json() gives you. JSONEachRow returns an array of row objects, which is what you want most of the time. Other formats (JSON, CSV, TabSeparated) change the shape, and the client doesn't pick one for you on query(). If you forget format, you get raw text.
Parameters use {name:Type}, not ? or $1. ClickHouse parameter binding is typed: {threshold:UInt32} in the SQL, query_params: { threshold: 100 } in the call. The type is mandatory and part of the placeholder. This is server-side substitution that prevents injection, so build queries this way rather than string-concatenating values.
The port trap
createClient defaults to http://localhost:8123. The single most common connection failure is pointing it at 9000, ClickHouse's native TCP port, and getting a cryptic protocol error or a hang. The Node client does not speak the native protocol. If you've copied a port from a clickhouse-client CLI example or a JDBC string, it's likely 9000 (native) or 9440 (native TLS) -- the HTTP client needs 8123 (HTTP) or 8443 (HTTPS). For ClickHouse Cloud, use the HTTPS endpoint on 8443.
Inserting data
query() is for reads. Use insert() for writes -- it's optimized for batching and avoids building giant SQL strings:
await client.insert({
table: "events",
values: [
{ name: "click", value: 1, ts: "2026-06-15 10:00:00" },
{ name: "view", value: 1, ts: "2026-06-15 10:00:01" },
],
format: "JSONEachRow",
});Batch, don't trickle. ClickHouse is a columnar store built for large inserts. One row per insert call creates one part per call, and ClickHouse will choke on the resulting part count (the Too many parts error). Buffer rows in your app and insert in batches of thousands to tens of thousands, or enable async inserts.
Async inserts push the batching to the server, which is often easier than buffering in app code:
await client.insert({
table: "events",
values: rows,
format: "JSONEachRow",
clickhouse_settings: {
async_insert: 1,
wait_for_async_insert: 1, // wait for the server-side flush to confirm
},
});With async_insert, the server collects small inserts into larger blocks before writing. Set wait_for_async_insert: 1 if you need confirmation the data landed; 0 returns sooner but gives up the durability guarantee on that call.
Streaming large result sets
Don't call .json() on a query that returns millions of rows -- it buffers everything in memory. Stream instead:
const rs = await client.query({
query: "SELECT * FROM big_table",
format: "JSONEachRow",
});
const stream = rs.stream();
for await (const rows of stream) {
for (const row of rows) {
process(row.json());
}
}The stream yields chunks of rows, so you process the result without holding it all at once.
Connection management
createClient returns a client backed by an HTTP connection pool; create it once and reuse it, like any pooled client. Useful options:
max_open_connections: caps concurrent sockets. Default is fine for most apps; raise it for high-concurrency read workloads.request_timeout: defaults to 30s. Long analytical queries may need more.compression:{ response: true }enables gzip on responses, worth it for large result sets over a network.keep_alive: on by default, reuses sockets across requests.
Call client.close() on shutdown to drain the pool.
TLS and ClickHouse Cloud
For ClickHouse Cloud or any TLS endpoint, use the HTTPS URL and credentials from the console:
const client = createClient({
url: "https://abc123.eu-central-1.aws.clickhouse.cloud:8443",
username: "default",
password: process.env.CLICKHOUSE_PASSWORD,
});Cloud services idle to sleep; the first query after idle may take a few seconds to wake the service. Account for that in your request_timeout rather than treating the delay as a connection failure.
Error reference
| Error | Cause | Fix |
|---|---|---|
| Protocol error / hang | Pointing at port 9000 (native) | Use 8123 (HTTP) or 8443 (HTTPS) |
Too many parts | Inserting one row at a time | Batch inserts, or enable async_insert |
Authentication failed | Wrong user/password | Check credentials; default user often has empty password locally |
ECONNREFUSED | Server not running or wrong host | Verify host/port, server up |
| Out of memory in app | .json() on a huge result | Stream with rs.stream() |
Unknown setting | Setting name typo in clickhouse_settings | Check the setting exists for your server version |
Connecting with a GUI instead
For exploring tables, checking the schema, or testing the SQL you're about to embed in code, a client beats console.log. Mako connects to ClickHouse with AI-assisted SQL autocomplete; the same guide compares the rest of the ClickHouse tooling honestly.
For the SQL side once you're connected, see our ClickHouse guides on materialized views, the MergeTree engine family, and aggregate functions. For loading data, see importing CSV to ClickHouse.
Mako connects to ClickHouse (and 8 other databases) with AI-powered autocomplete. Try it free at mako.ai.
Skip the terminal. Use Mako.
Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.