How to Connect to ClickHouse from Node.js

6 min readClickHouse

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

There are two published clients, and picking the wrong one is the first mistake:

PackageUse it forTransport
@clickhouse/clientNode.js servers, scripts, backendsHTTP, with Node streams
@clickhouse/client-webBrowsers, edge runtimes, Cloudflare WorkersHTTP 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

ErrorCauseFix
Protocol error / hangPointing at port 9000 (native)Use 8123 (HTTP) or 8443 (HTTPS)
Too many partsInserting one row at a timeBatch inserts, or enable async_insert
Authentication failedWrong user/passwordCheck credentials; default user often has empty password locally
ECONNREFUSEDServer not running or wrong hostVerify host/port, server up
Out of memory in app.json() on a huge resultStream with rs.stream()
Unknown settingSetting name typo in clickhouse_settingsCheck 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.

Mako — open source

Skip the terminal. Use Mako.

Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.