How to Connect to ClickHouse from Go

7 min readClickHouse

ClickHouse has one official Go driver, and it gives you two faces: a native ClickHouse interface (clickhouse.Open) and a standard database/sql interface (clickhouse.OpenDB). The native interface is faster and exposes ClickHouse-specific features like typed batch inserts; the database/sql face trades some speed for compatibility with ORMs, query builders, and migration tools. This guide shows both, explains which transport port you actually want, and covers the configuration that matters for an OLAP workload: batching, pooling, and TLS.

The options

PackageImport pathInterfaceTransportVersion (as of June 2026)
clickhouse-go (native)github.com/ClickHouse/clickhouse-go/v2ClickHouse-specific APInative TCP (9000/9440) or HTTP (8123/8443)v2.46.0
clickhouse-go (stdlib)github.com/ClickHouse/clickhouse-go/v2 via OpenDBdatabase/sqlnative or HTTPv2.46.0
ch-gogithub.com/ClickHouse/ch-golow-level columnar APInative TCPv0.72.0

The decision tree:

  • Most applications: clickhouse-go v2 with the native interface (clickhouse.Open). You get batch inserts, typed scanning, and the binary protocol. This is the default recommendation.
  • You need database/sql (an ORM, sqlx, a migration runner): use clickhouse.OpenDB. Same driver, standard interface, slightly slower because it goes through database/sql's row-by-row plumbing.
  • You need maximum insert throughput and are willing to work at the column level: ch-go is the low-level driver that clickhouse-go is built on. It speaks columns, not rows, and is the right tool for high-volume ingestion pipelines. Most teams do not need it.

clickhouse-go v2 is a full rewrite of v1; v1 is no longer recommended for new code. Make sure your import path ends in /v2.

The port question

This trips up nearly everyone the first time. ClickHouse exposes several ports, and the driver behaves differently depending on which protocol you point it at:

PortProtocolTLSNotes
9000native TCPnodefault for self-hosted, fastest
9440native TCPyesTLS native, common in production
8123HTTPnouseful behind HTTP proxies/load balancers
8443HTTPSyesClickHouse Cloud uses this

If you set up the native interface but point it at 8123, the connection fails with a protocol mismatch. ClickHouse Cloud listens on 9440 (native+TLS) and 8443 (HTTPS); pick the matching Protocol in the driver options. Set the protocol explicitly with Protocol: clickhouse.Native or clickhouse.HTTP so there is no ambiguity.

go get github.com/ClickHouse/clickhouse-go/v2

clickhouse.Open returns a connection object that manages its own pool internally. It is safe for concurrent use by multiple goroutines, so open it once at startup and share it.

package main
 
import (
	"context"
	"crypto/tls"
	"fmt"
	"log"
	"time"
 
	"github.com/ClickHouse/clickhouse-go/v2"
)
 
func main() {
	ctx := context.Background()
 
	conn, err := clickhouse.Open(&clickhouse.Options{
		Addr: []string{"localhost:9000"},
		Auth: clickhouse.Auth{
			Database: "default",
			Username: "default",
			Password: "",
		},
		Protocol: clickhouse.Native,
		Settings: clickhouse.Settings{
			"max_execution_time": 60,
		},
		DialTimeout:     10 * time.Second,
		MaxOpenConns:    10,
		MaxIdleConns:    5,
		ConnMaxLifetime: time.Hour,
	})
	if err != nil {
		log.Fatalf("open: %v", err)
	}
	defer conn.Close()
 
	// Open is lazy. Force a real round trip at startup so misconfiguration
	// surfaces here rather than on the first query under load.
	if err := conn.Ping(ctx); err != nil {
		log.Fatalf("ping: %v", err)
	}
 
	rows, err := conn.Query(ctx,
		"SELECT id, name FROM events WHERE region = ? LIMIT 10",
		"EMEA",
	)
	if err != nil {
		log.Fatalf("query: %v", err)
	}
	defer rows.Close()
 
	for rows.Next() {
		var (
			id   uint64
			name string
		)
		if err := rows.Scan(&id, &name); err != nil {
			log.Fatalf("scan: %v", err)
		}
		fmt.Printf("%d: %s\n", id, name)
	}
	if err := rows.Err(); err != nil {
		log.Fatalf("rows: %v", err)
	}
 
	_ = tls.Config{} // see TLS section below
}

Two traps in that loop that the Go compiler will not catch:

  • defer rows.Close() is not optional. Leaking result sets eventually starves the pool.
  • rows.Err() after the loop catches errors that happen during iteration, such as a server-side timeout. rows.Next() returning false does not distinguish "done" from "failed."

The native interface uses ? placeholders for parameters. Values are sent typed over the binary protocol, so you do not build SQL strings by hand.

Batch inserts

ClickHouse is built for large, infrequent inserts, not row-at-a-time writes. Sending one INSERT per row creates a flood of small parts and triggers the "Too many parts" error as the server struggles to merge them. The native driver's batch API is the correct pattern:

batch, err := conn.PrepareBatch(ctx, "INSERT INTO events (id, name, region)")
if err != nil {
	log.Fatalf("prepare batch: %v", err)
}
 
for _, e := range incoming {
	if err := batch.Append(e.ID, e.Name, e.Region); err != nil {
		log.Fatalf("append: %v", err)
	}
}
 
if err := batch.Send(); err != nil {
	log.Fatalf("send: %v", err)
}

Accumulate thousands to tens of thousands of rows per batch. If your rows trickle in continuously, either buffer them in your application and flush periodically, or enable server-side async_insert (set it in Settings and turn on wait_for_async_insert if you need confirmation that the write landed).

database/sql interface

When a library demands the standard interface, use clickhouse.OpenDB. It takes the same Options struct, so all the configuration above carries over.

package main
 
import (
	"context"
	"database/sql"
	"log"
	"time"
 
	"github.com/ClickHouse/clickhouse-go/v2"
)
 
func main() {
	db := clickhouse.OpenDB(&clickhouse.Options{
		Addr: []string{"localhost:9000"},
		Auth: clickhouse.Auth{Database: "default", Username: "default"},
		Protocol: clickhouse.Native,
	})
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	db.SetConnMaxLifetime(time.Hour)
	defer db.Close()
 
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
 
	if err := db.PingContext(ctx); err != nil {
		log.Fatalf("ping: %v", err)
	}
 
	var count uint64
	err := db.QueryRowContext(ctx,
		"SELECT count() FROM events WHERE region = ?", "EMEA").Scan(&count)
	if err != nil {
		log.Fatalf("query: %v", err)
	}
	log.Printf("count = %d", count)
}

With database/sql you set pool sizes through the standard db.SetMaxOpenConns / SetMaxIdleConns / SetConnMaxLifetime methods rather than the Options fields. Batch inserts still work but go through the standard prepared-statement path inside a transaction, which is slower than the native PrepareBatch. If insert performance matters, that alone is a reason to prefer the native interface.

Context timeouts

Every query method takes a context.Context. Use it. OLAP queries can scan billions of rows, and a context deadline is how you bound that:

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
 
rows, err := conn.Query(ctx, "SELECT ... ")

When the context expires, the driver cancels the query on the server side as well, so a runaway aggregation does not keep burning CPU after your client has given up. This is more reliable than the max_execution_time setting alone, though setting both is reasonable.

TLS

For an encrypted native connection (port 9440) or ClickHouse Cloud (8443), pass a tls.Config:

conn, err := clickhouse.Open(&clickhouse.Options{
	Addr:     []string{"your-instance.clickhouse.cloud:9440"},
	Protocol: clickhouse.Native,
	Auth: clickhouse.Auth{
		Database: "default",
		Username: "default",
		Password: "your-password",
	},
	TLS: &tls.Config{},
})

An empty &tls.Config{} enables TLS and verifies the server certificate against the system root CAs, which is what you want for ClickHouse Cloud and any properly provisioned server. Do not reach for InsecureSkipVerify: true to make a connection work; that disables certificate verification and defeats the point of TLS. If you hit a verification error against a self-hosted server, add its CA to RootCAs instead.

Common errors

ErrorCauseFix
unexpected packet / protocol mismatchnative interface pointed at HTTP port (8123) or vice versamatch Protocol to the port
Too many partstoo many small insertsbatch rows, or use async_insert
code: 516, Authentication failedwrong user/password or databasecheck Auth fields
dial tcp ... connection refusedserver not listening on that port, or firewallconfirm the port; Cloud needs IP allowlisting
x509: certificate signed by unknown authorityself-hosted CA not in system rootsadd the CA to tls.Config.RootCAs
context deadline exceededquery slower than the deadlineraise the timeout or optimize the query

Mako connects to ClickHouse, PostgreSQL, MySQL, and more, with AI-powered autocomplete for exploring your tables. 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.