How to Connect to ClickHouse from C# (.NET)
ClickHouse from C# is a different decision than PostgreSQL or MySQL: there is no single obvious driver, and the choice you make affects which port you connect to and how inserts perform. There are three serious ADO.NET providers, and they split along the protocol ClickHouse exposes. This guide covers picking one, the port trap that catches almost everyone, parameters, bulk inserts, Dapper, and the errors you will hit.
The protocol decision comes first
ClickHouse exposes two interfaces: the HTTP interface on port 8123 and the native binary protocol on port 9000 (8443 and 9440 respectively for TLS). The driver you pick is, in practice, a choice of which one you talk to.
| Library | NuGet ID | Protocol / port | Maintainer | Notes |
|---|---|---|---|---|
| ClickHouse.Driver | ClickHouse.Driver | Binary over HTTP (8123/8443) | ClickHouse (official) | Official ADO.NET driver, current as of June 2026. The default recommendation for new code. |
| ClickHouse.Client | ClickHouse.Client | Binary over HTTP (8123/8443) | Oleg Kozlyuk (community) | Mature, widely used, full ADO.NET surface. Predates the official driver and is still actively maintained. |
| Octonica.ClickHouseClient | Octonica.ClickHouseClient | Native TCP (9000/9440) | Octonica (community) | Native-protocol ADO.NET provider. Use when you specifically want native-protocol features or TCP. |
The official ClickHouse.Driver (1.2.0 as of June 2026) is the right starting point for new projects: it is maintained by ClickHouse, implements ADO.NET so Dapper and Linq2db work on top of it, and uses binary-over-HTTP, which is firewall-friendly and load-balancer-friendly. ClickHouse.Client (7.14.0) is the long-standing community option and remains a solid choice, particularly if you already depend on it. Octonica.ClickHouseClient (4.1.4) is the one to reach for when you want the native TCP protocol specifically.
The examples below use the official ClickHouse.Driver. The ADO.NET surface is close enough that ClickHouse.Client code looks almost identical; only the connection class names differ.
dotnet add package ClickHouse.DriverThe port trap
The single most common ClickHouse connection mistake across every language is pointing an HTTP client at port 9000. Port 9000 speaks the native binary protocol; the HTTP-based drivers (ClickHouse.Driver, ClickHouse.Client) need 8123. If you give an HTTP driver port 9000 you get a confusing protocol or parse error, not a clean "wrong port" message. The reverse is also true: Octonica's native driver wants 9000, not 8123.
On ClickHouse Cloud, the HTTP interface is 8443 over TLS. There is no plain-HTTP option there.
A first connection
using ClickHouse.Driver.ADO;
var connString =
"Host=localhost;Port=8123;Username=default;Password=;Database=default";
await using var conn = new ClickHouseConnection(connString);
await conn.OpenAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT number, number * 2 AS doubled FROM system.numbers LIMIT 5";
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var n = reader.GetUInt64(0);
var doubled = reader.GetUInt64(1);
Console.WriteLine($"{n} -> {doubled}");
}Use the async methods in any server application. ClickHouse maps UInt64 (its default integer width for system.numbers and many counters) to .NET ulong, so read it with GetUInt64, not GetInt32. Reading the wrong width is a frequent source of InvalidCastException.
Parameters: never concatenate
Bind values rather than building SQL strings. The drivers use named parameters:
await using var cmd = conn.CreateCommand();
cmd.CommandText =
"SELECT count() FROM events WHERE event_type = {type:String} AND ts >= {since:DateTime}";
cmd.Parameters.Add(new ClickHouseDbParameter
{
ParameterName = "type",
Value = "purchase"
});
cmd.Parameters.Add(new ClickHouseDbParameter
{
ParameterName = "since",
Value = new DateTime(2026, 1, 1)
});
var count = (ulong)(await cmd.ExecuteScalarAsync())!;ClickHouse's server-side parameter syntax is {name:Type}, where the type is a ClickHouse type name (String, UInt64, DateTime, Array(String), and so on). This is different from the @name style of SQL Server or the $1 of PostgreSQL, and the type annotation is required. Getting the type wrong (for example {id:Int32} for a UInt64 column) produces a type-mismatch error from the server.
Inserting data: batch, do not loop
This is where ClickHouse differs most from a row-oriented database. ClickHouse is a columnar store built for large, infrequent inserts. Inserting rows one at a time creates a storage part per insert, and parts have to be merged in the background. Flood it with single-row inserts and you hit the famous error:
DB::Exception: Too many parts (300). Merges are processing significantly slower than inserts.
The fix is to batch. Insert thousands to hundreds of thousands of rows per statement. The official driver exposes a bulk-copy helper for this:
using ClickHouse.Driver.Copy;
await using var bulkCopy = new ClickHouseBulkCopy(conn)
{
DestinationTable = "events",
ColumnNames = new[] { "event_type", "user_id", "ts" },
BatchSize = 100_000
};
await bulkCopy.InitAsync();
var rows = data.Select(d => new object[] { d.Type, d.UserId, d.Timestamp });
await bulkCopy.WriteToServerAsync(rows);If your data genuinely arrives one row at a time (per-request inserts in a web service, for example), use ClickHouse's asynchronous inserts instead of batching in your application. Set the server-side setting and let ClickHouse buffer and batch on its side:
await using var cmd = conn.CreateCommand();
cmd.CommandText = "INSERT INTO events (event_type, user_id, ts) VALUES ({t:String}, {u:UInt64}, now())";
// enable async inserts so the server batches small inserts
cmd.CommandText = "SET async_insert = 1, wait_for_async_insert = 1; " + cmd.CommandText;wait_for_async_insert = 1 keeps the call durable (it returns once the data is committed); setting it to 0 returns sooner but risks losing buffered rows on a crash. Either way, never solve high insert rates by raising the parts limit -- fix the insert pattern.
Dapper
Because these are ADO.NET providers, Dapper works without anything ClickHouse-specific:
using Dapper;
await using var conn = new ClickHouseConnection(connString);
var topTypes = await conn.QueryAsync<EventCount>(
"SELECT event_type AS Type, count() AS Total FROM events GROUP BY event_type ORDER BY Total DESC LIMIT 10");Dapper 2.1.79 is current as of June 2026. Note that Dapper's parameter handling (@name) does not map onto ClickHouse's {name:Type} server-side syntax, so for parameterized queries through Dapper, check your driver's documentation on how it translates parameters -- the drivers handle the common cases, but the type-annotated server syntax is the native form.
Connection lifetime and pooling
ClickHouse connections are cheaper to think about than transactional databases, but the HTTP drivers still pool underlying HTTP connections. Create one ClickHouseConnection per unit of work (or use a connection factory / DI registration) rather than sharing a single connection across threads. For analytical workloads -- a handful of large queries rather than thousands of tiny transactions -- you do not need a large pool. A small number of connections per process is usually right, because each query scans a lot of data and you are bound by the server's CPU and IO, not by connection count.
ClickHouse has no multi-statement transactions in the OLTP sense, so there is no BeginTransaction pattern to manage across your reads. Inserts are atomic per part.
TLS and ClickHouse Cloud
For ClickHouse Cloud or any networked deployment, use TLS. With the HTTP drivers that means the https interface on 8443:
var connString =
"Host=your-instance.clickhouse.cloud;Port=8443;Username=default;Password=...;Database=default;Protocol=https";Cloud instances also idle-sleep: the first query after an idle period can be slow while the instance wakes. Build a short retry or a generous command timeout into health checks so a cold start does not read as an outage.
Errors you will actually hit
| Error | Cause | Fix |
|---|---|---|
| Protocol / unexpected-packet error on connect | HTTP driver pointed at native port 9000 (or native driver at 8123) | Use 8123/8443 for ClickHouse.Driver/ClickHouse.Client; 9000/9440 for Octonica |
Too many parts (N) | Single-row or tiny inserts creating too many storage parts | Batch inserts (bulk copy) or enable async_insert -- do not raise the parts limit |
Authentication failed | Wrong user/password, or user not allowed from your host | Check credentials; review users.xml / access control host rules |
Code: 60. Table ... does not exist | Wrong database in the connection string, or unqualified table name | Set Database= correctly or qualify as db.table |
InvalidCastException reading a column | Reading a UInt64/Int64 column with GetInt32, or wrong .NET type | Match the read method to the ClickHouse column type (GetUInt64 for UInt64) |
| Type mismatch on a parameter | {name:Type} annotation does not match the column type | Use the exact ClickHouse type in the parameter (e.g. {id:UInt64}) |
| Timeout on the first Cloud query | ClickHouse Cloud instance waking from idle | Increase command timeout / add a retry for cold starts |
Where Mako fits
Mako connects to ClickHouse with AI-assisted SQL autocomplete, which helps when you are exploring tables and column types before you decide which driver to wire in and how to shape your inserts. It is a read and query tool, not a connection library or an ingestion pipeline: you still use ClickHouse.Driver, ClickHouse.Client, or Octonica in your application code, and you still batch your inserts. 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.