Experimental status
The main client, data source, session, operations interfaces, and dependency-injection extensions produce compiler warningCHTCP0001. To acknowledge the warning while evaluating the API, add it
to NoWarn in your project file:
Supported .NET versions
The native TCP client supports the following .NET versions:- .NET 8.0
- .NET 9.0
- .NET 10.0
Supported ClickHouse versions
The TCP client officially supports the last 2 LTS versions, as well as the last 3 releases of the ClickHouse server.Installation
The native TCP client is included in theClickHouse.Driver package. Install it from NuGet:
Quick start
Migrating from the HTTP client
The TCP API is separate fromIClickHouseClient; it is not a transport option for
ClickHouseClient or ClickHouseConnection. Most SQL can be reused, but construct a
ClickHouseTcpClient and migrate each operation to its TCP equivalent.
Method equivalents
For example, an HTTP reader loop:
Options and parameters
HTTPQueryOptions and ClickHouseParameterCollection are not interchangeable with their TCP
counterparts. Move the values into ClickHouseTcpQueryOptions, where settings are strings and
parameters are part of the options object:
{name:Type} placeholders but does not rewrite
ADO.NET-style @name placeholders. It also has no per-query database, role, bearer token, custom
header, parameter resolver or formatter, or read converter. See limitations for the
features that remain HTTP-only.
Connection and result lifetime
Both high-level clients are thread-safe and should be reused. The TCP client owns persistent native connections rather than HTTP connections, implementsIAsyncDisposable, and should normally be
disposed asynchronously when the application shuts down.
QueryAsync rows are caller-owned. StreamAsync blocks and their columns are borrowed and valid
only for the current iteration; copy any values that must outlive it. Always finish or dispose an
enumeration so its connection returns to the pool. Use OpenSessionAsync when several operations
must run on the same connection and retain session state.
See the complete
HTTP-to-TCP migration example
for both clients used against the same table.
Configuration
There are two ways to configure a native TCP client:- Connection string: Semicolon-separated key/value pairs that specify the server endpoint, authentication credentials, and other connection options.
ClickHouseTcpClientOptionsobject: A strongly typed, immutable configuration object set in code.
ClickHouseTcpClient constructor. Use
ClickHouseTcpConnectionStringBuilder to parse or build a connection string and convert it to
ClickHouseTcpClientOptions.
Connection settings
Connection-string values forTimeSpan properties are expressed in seconds.
Data format and serialization
Connection pooling
One connection runs one operation at a time. The pool lets operations run concurrently and reuses connections between them.Security
The secure native protocol is a separate server endpoint rather than an in-band upgrade. A TLS
client must connect to a secure native port; it does not fall back to plaintext.
ConfigureTls is
applied last and can override certificate and hostname validation, so review custom callbacks
carefully.
Logging and debugging
The client logger records client-side lifecycle and operation events. Server log packets are sent
to
ClickHouseTcpQueryCallbacks.OnLog instead. Statement text can contain sensitive data, so it is
excluded from activity tags by default and heavily truncated in debug logs.
Custom settings
ClickHouseTcpClientOptions.CustomSettings applies ClickHouse server settings to every query and
insert. Per-operation ClickHouseTcpQueryOptions.Settings values override client-level values with
the same name.
set_:
set_ prefix when adding a setting to CustomSettings or per-operation
Settings.
ClickHouseTcpQueryOptions
ClickHouseTcpQueryOptions supplies per-operation settings. All properties are optional.
ClickHouseTcpInsertOptions
ClickHouseTcpInsertOptions extends ClickHouseTcpQueryOptions with settings for native block
inserts. All query-option properties are also available.
ClickHouseTcpClient
ClickHouseTcpClient is the high-level API for the native TCP protocol. It executes statements and
scalar queries, streams results as columnar blocks, rows, or POCOs, inserts columnar or row data,
and opens pinned sessions.
The client is thread-safe and owns a connection pool. Create one client per endpoint, share it
across the application, and dispose it at shutdown. Operations run concurrently up to
MaxPoolSize; additional operations wait for a connection for up to PoolTimeout.
Creating a client
Create a client from a connection string orClickHouseTcpClientOptions. See
Configuration for the available settings.
Using a connection string:
PoolTimeout for active operations before aborting them.
Dependency injection
AddClickHouseTcpDataSource registers one singleton data source and exposes its shared client as
IClickHouseTcpClient and IClickHouseTcpOperations:
LoggerFactory,
the registration uses the container’s ILoggerFactory. A serviceKey can register multiple
independent data sources.
Executing queries
UseExecuteAsync for statements that do not return rows, such as DDL and mutations:
ExecuteAsync reads and discards them. For
INSERT INTO ... VALUES, use InsertAsync or InsertRowsAsync and end the SQL statement at
VALUES without inline values.
Reading data
Choose the read API by the shape the application needs:Scalar reads
ExecuteScalarAsync returns the first column of the first row as an object:
NULL first value and a query
that returns no rows both produce null.
Row reads
QueryAsync streams each row as an owned object[], with values in result-column order:
POCO reads
QueryAsync<T> maps columns to public settable properties. Names match without regard to case or
underscores; use ClickHouseTcpColumn to set an explicit name and ClickHouseTcpNotMapped to
exclude a property.
Columnar block reads
StreamAsync exposes the native columnar result as a sequence of Block values. This is the fastest way to read data.
Always finish enumeration or dispose the enumerator so its connection returns to the pool.
Block.Column<T> accesses the CLR type produced by the decoder. Use Block.ReadAs<T> or a
specialized interface such as IDateTimeColumn when conversion is required.
Inserting data
Native inserts send data as columnar wire blocks. The SQL must be anINSERT INTO ... VALUES
statement with no inline value list. Use typed columns for the least client-side work, or row and
POCO overloads when the application already holds row-oriented data.
Columnar inserts
InsertAsync accepts data already grouped into typed columns. This is the fastest way to insert data.
InsertAsync, provided the
insert completes before that block is released.
Row inserts
InsertRowsAsync accepts object[] rows. Values match the SQL column list by position:
Variant or Dynamic. Do not modify the rows until the operation completes.
POCO inserts
The genericInsertRowsAsync<T> overload maps target columns to public readable properties using
the same naming and attribute rules as POCO reads:
ClickHouseTcpNotMapped keeps application-only properties such as
ImportBatch out of the mapping.
SQL parameters
The native client binds values to ClickHouse-native{name:Type} placeholders. It does not rewrite
ADO.NET-style @name placeholders.
ClickHouseTcpParameter.ClickHouseType property. Use the Identifier type for table and column
names. When a DateTime or DateTimeOffset represents an instant, include a timezone such as
{timestamp:DateTime('UTC')}; otherwise the server session timezone could change its meaning.
Query ID
Every operation has a query ID used by ClickHouse system tables, client logs, and trace spans. WhenQueryId is null or empty, the client generates a new GUID. Set it when the operation must
correlate with an application identifier:
Query progress and metadata
SetClickHouseTcpQueryOptions.Callbacks to receive metadata interleaved with the response:
OnLog, OnProfileEvents, OnTotals, and OnExtremes are borrowed and must not escape the
callback.
ClickHouse does not send query-progress packets for rows uploaded by an insert. Use
OnBlockWritten to observe blocks sent by the client; it reports transport progress, not that the
server committed the insert.
Cancellation
Every operation accepts aCancellationToken:
Server information and health checks
GetServerInfoAsync returns server identity, version, timezone, and negotiated protocol information
from the native handshake:
ProtocolRevision, which is the revision negotiated by the client and
server. Gate SQL features such as data types and functions on Version. The method uses an existing
connection when one is available and opens one when the pool is empty.
Use PingAsync for a readiness check over the native protocol:
More examples
For complete runnable examples covering reads, inserts, types, sessions, TLS, observability, and error handling, see the native TCP examples in the GitHub repository.Sessions
Ordinary client operations rent any available pooled connection, so connection-local state is neither isolated nor guaranteed to persist between calls. Different connections in the pool can connect to different servers. Open a session when temporary tables,SET statements, or another sequence of operations must use one connection.
IClickHouseTcpSession exposes the same query, execution, insert, ping, and server-information APIs
as the client. The session is not thread safe. Its operations are sequential: starting another operation before the current one
finishes is rejected. A streamed result keeps the session busy until it is fully enumerated or its
enumerator is disposed.
Each session occupies one MaxPoolSize slot for its lifetime. The client can continue using other
pool connections, but when the pool contains only one connection, other client operations wait
until the session is disposed or PoolTimeout expires.
Dispose the session before disposing its parent client. Session disposal closes the pinned
connection rather than returning it to the pool, preventing later callers from inheriting its
temporary tables or settings. A connection or protocol failure, cancellation, or an incomplete
result stream can make the session unusable; when IsOpen becomes false, open a new session.
Best practices
Client lifetime and pooling
Create oneClickHouseTcpClient or ClickHouseTcpDataSource for each distinct endpoint and
configuration, and reuse it for the lifetime of the application. The client is thread-safe and owns
its connection pool; creating one per operation creates a new pool each time and prevents connection
reuse.
In a dependency-injection application, register AddClickHouseTcpDataSource once and inject
IClickHouseTcpClient. The service provider owns the client, so consumers must not dispose the
injected instance. Outside DI, dispose the client at application shutdown with await using.
Each native connection carries one operation at a time. MaxPoolSize therefore limits both open
connections and concurrent operations; additional callers wait for a slot for up to PoolTimeout.
Choose a value that supports the application’s real concurrency without exceeding server-side
connection and query limits. Raising it does not make one query faster.
Keep these pool consumers in mind:
- A streamed result holds its connection until its enumerator is disposed.
- A session holds one connection for the session’s entire lifetime and accepts one operation at a time.
- Abandoning a result, in-flight cancellation, or a protocol/connection failure causes the connection to be closed rather than reused.
ClickHouse.Driver.Tcp.Pool logging category to diagnose pool exhaustion,
unexpected connection retirement, or frequent redials.
Timeouts and cancellation
The TCP client has separate limits for separate waits:
A checkout that first waits for a pool slot and then opens a connection can take up to
PoolTimeout + DialTimeout. ReadTimeout restarts for each transport read, so a long query that
continues sending progress or data can run longer than that value. Set it high enough for legitimate
periods of server silence; set it to TimeSpan.Zero only when another timeout reliably bounds the
operation.
Use a cancellation token when you need an end-to-end timeout:
Date and time handling
PreferDateTime('UTC') or DateTime64(S, 'UTC') for timestamps and use DateTimeOffset or a
DateTime whose Kind is Utc in application code. This makes the represented instant explicit
and avoids dependence on the server or session timezone.
The TCP client applies these rules when writing DateTime values:
An unspecified time skipped by a daylight-saving transition is rejected because it names no
instant. An ambiguous fall-back time selects the earlier occurrence. Use
DateTimeOffset when the
choice must be explicit.
For SQL parameters that represent an instant, include the timezone in the placeholder:
DateTime and DateTime64 expose their raw uint or long counts by default.
Use Block.ReadAs<DateTimeOffset>, IDateTimeColumn.GetDateTimeOffset, or a matching POCO property
when a calendar value is more convenient. Keep the raw count when every digit matters:
DateTimeOffset has 100-nanosecond resolution, so reading DateTime64 at scale 8 or 9 through it
loses sub-tick precision.
Use DateOnly for Date and Date32. Use TimeSpan for Time and Time64 values that may be
negative or exceed 24 hours; TimeOnly is appropriate only when the column is constrained to a
time of day.
Streaming and result lifetime
UseStreamAsync and typed columns for high-throughput processing. This follows ClickHouse’s
columnar layout and avoids per-row arrays, boxing, and POCO allocation. Use QueryAsync<T> when an
owned application model is more important than the lowest allocation rate, and use the untyped
QueryAsync only when the schema is not known at compile time.
Blocks returned by StreamAsync are borrowed. The block, its columns, composite child columns, and
all spans are valid only for the current loop iteration. Process them in place or copy values that
must outlive the iteration:
await foreach dispose the enumerator. If only part of
a result is needed, express that in SQL with LIMIT and select only the required columns. Breaking
out early cancels the remaining response and prevents that connection from being reused.
Insert shape and batching
PreferInsertAsync with typed columns when data is already columnar. It performs the least
client-side conversion. InsertRowsAsync<T> is the natural choice for POCO data; untyped
object[] rows are the most flexible but allocate arrays and box value types.
Send a useful number of rows in each insert call rather than issuing many tiny inserts. Within one
call, MaxRowsPerBlock splits the data into native wire blocks. Its default of 50,000 rows also
bounds the temporary column buffers used when converting row and POCO inputs. Increasing it can
improve throughput for narrow rows but increases peak memory; setting it to null writes the whole
insert as one block and converts all row-oriented input for that block together.
MaxSendBufferBytes independently limits how much encoded data is buffered before it is flushed to
the socket. It is a soft cap checked between columns, so one wide column can exceed it.
Row and POCO inputs are converted a block at a time. A bad value in a later block can therefore be
found after earlier blocks have already been sent. Validate nullability and CLR types before the
insert when data quality is uncertain, and set a data-derived DeduplicationToken whenever the
logical batch may be retried.
Supported data types
The tables below distinguish the type returned by the decodedIColumn<T> from additional types
available through conversion via Block.ReadAs<T>, QueryAsync<T> POCO mapping, or inserts. You can ask the same
resolver used by those operations whether a particular mapping is supported:
Nullable, Array, Tuple, Map, and LowCardinality apply recursively to their
inner types.
Type mapping: reading from ClickHouse
Default .NET type is the T exposed by Block.Column<T> and IColumn<T>. Types in the
Also readable as column are available through Block.ReadAs<T> and POCO properties.
Integer types
Floating point types
Decimal types
ClickHouseTcpDecimal stores an arbitrary-size integer mantissa and a scale, preserving values
whose precision exceeds the range of decimal.
Boolean type
String types
Use
IStringColumn.GetBytes(row) or read as byte[] when the data may not be valid UTF-8.
FixedString(N) preserves all N bytes, including trailing zero bytes.
Date and time types
The raw integer representation is the default so columnar reads retain the exact wire value. The
calendar conversions use the timezone declared by the type, or the session timezone when the type
does not declare one. Reading
Time or Time64 as TimeOnly throws when a value is negative or at
least 24 hours; use TimeSpan for the full ClickHouse range.
Enum types
IEnumColumn also exposes the declared label-to-ordinal mapping.
Other scalar types
Composite types
Tuples can contain up to seven elements. A
Nested column is arity-independent and is best read
through INestedColumn, which exposes its named flat field columns and shared row offsets without
allocating boxed records. With ClickHouse’s default flatten_nested = 1, nested fields arrive as
separate dotted Array(T) columns instead.
Variant, Dynamic, and JSON types
Variant and Dynamic return the CLR value for the selected type in each row, or null. Their
columnar views expose the discriminator stream and one typed child column per runtime type, avoiding
boxing when processing data in columns.
The TCP client reads JSON through ClickHouse’s String serialization. The client enables
output_format_native_write_json_as_string = 1 by default; disabling that setting makes JSON reads
unsupported. ClickHouse parses and normalizes JSON, so the returned text may differ in key order,
whitespace, and number formatting from the inserted text.
Geometry types
Geometry is exposed as an IVariantColumn over the six geometry types.
QBit type
The TCP client supports the two-argument, unstrided
QBit(T, N) form. The three-argument strided
layout is not supported.
AggregateFunction types
An
AggregateFunction column contains a function-specific intermediate state. Finalize it on the
server, for example with sumMerge(column), and read the resulting value instead.
Type mapping: writing to ClickHouse
Each entry is the CLR type of one row in theIColumn passed to InsertAsync. These mappings are
exact: the TCP client does not apply Convert-style coercions.
Integer types
Floating point types
Writing
BFloat16 truncates each float to the 16-bit brain floating-point representation.
Decimal types
The write throws
OverflowException when the scaled mantissa exceeds the declared precision.
Boolean type
String types
FixedString(N) deliberately does not accept string; encode and pad the value explicitly so its
exact binary representation is unambiguous.
Date and time types
Raw integers use the units shown in the reading table.
DateTimeOffset preserves the instant.
DateTime respects its Kind; an Unspecified value is interpreted as wall-clock time in the
column timezone, or in the session timezone when the column does not declare one.
Enum types
An unknown label is rejected before data is sent.
Other scalar types
Composite types
The accepted shape is recursive, soArray(Nullable(DateTime)) accepts a DateTime?[] for each
row and Map(String, UInt32) accepts a KeyValuePair<string, uint>[].
Direct
Nested writes use the dense column shape returned by a previous read. For application-built
inserts, keep the default flatten_nested = 1 representation and write each dotted Array(T) field
as its own column.
Variant, Dynamic, and JSON types
For
Variant, the runtime type must uniquely match an alternative’s default CLR type. Convenience
mappings such as writing a DateTime to a DateTime alternative are not used inside a variant;
use the alternative’s default raw type instead. Dynamic supports scalar and recursively supported
array, map, and tuple values.
Geometry types
For an application-built
Geometry column, only Point and MultiPolygon have unique CLR shapes.
Ring and LineString share a shape, as do Polygon and MultiLineString, so the client cannot
infer which alternative an object value intends. A decoded Geometry column retains its
discriminators and can be inserted again without that ambiguity.
QBit type
AggregateFunction types
Compression
The client enables compression by default and uses LZ4 for blocks it writes; ClickHouse also defaults to LZ4 for response blocks. Compression applies to result blocks and blocks uploaded by an insert, while query text, progress, exceptions, and other protocol packets keep their normal wire encoding. Use theCompression connection-string key to select the codec used by the client:
IClickHouseCompressor:
Compressor = null to disable compression. A custom compressor must support native block
framing; compressors that only support HTTP bodies are rejected when the client is created.
Selecting the response codec
Compressor chooses how blocks written by the client are compressed. The native query packet asks
the server for compression but does not name a codec, so ClickHouse selects the response codec from
its network_compression_method setting. LZ4 is the server default.
Set the response codec per operation when needed:
Error handling
Failures reported by ClickHouse or the native connection derive fromClickHouseTcpException,
which in turn derives from DbException:
ClickHouseTcpServerException exposes both RawCode, the exact numeric code sent by the server,
and Code, the corresponding ClickHouseErrorCode when the driver names it. It also carries the
server exception Name, ServerStackTrace, and any nested server exceptions through
InnerException.
ClickHouseErrorCode have Code == ClickHouseErrorCode.Unknown; inspect
RawCode without assuming that the enum contains every ClickHouse error.
Argument validation and object-lifecycle errors keep their normal .NET exception types.
Cancellation raises OperationCanceledException. DialTimeout and ReadTimeout raise
ClickHouseTcpConnectionException with an inner TimeoutException, and the connection is discarded.
PoolTimeout raises a plain TimeoutException, which is not a ClickHouseTcpException: no connection
failed, and the fix is fewer concurrent operations, a larger pool, or finishing result enumerations.
Retrying operations
A connection failure or timeout during an insert leaves its outcome unknown: the server may have committed the rows before the client lost the response. Set a stableDeduplicationToken for the logical batch before retrying it:
Logging and diagnostics
The TCP client integrates withMicrosoft.Extensions.Logging. Logging is optional; with no
LoggerFactory, the client creates no loggers and formats no log messages.
ILoggerFactory unless the supplied options already specify one.
Logging categories
Configure the categories through the standard .NET logging configuration:
Debug and includes at most StatementMaxLength characters of
SQL. The default is five characters. Set the value to zero or less to omit statement text from logs,
or raise it only after considering parameters and literals that may contain sensitive data.
Server log messages
The logging integration reports client-side behavior. ClickHouse’s own query log packets are separate and are delivered toClickHouseTcpQueryCallbacks.OnLog. Enable them with the
send_logs_level server setting:
OpenTelemetry
The TCP client emits distributed-tracing spans through .NET’sSystem.Diagnostics.Activity API.
Subscribe to the TCP activity source from an OpenTelemetry tracer provider:
ClickHouse.Driver.Tcp, separate from the HTTP client’s source, so
either transport can be collected independently. When no listener subscribes, operations do not
create activities.
Spans and attributes
The client emits spans for SQL operations, pings, and new connections. A SQL span is named after the statement’s leading keyword, such asSELECT or INSERT; statements without a recognizable
leading keyword use query. A connect span covers the socket connection, TLS negotiation, and
native handshake. Statement spans also include time waiting for a pooled connection.
Counters are present only when the server or client has a meaningful value to report. For example,
an insert that uploads blocks reports client-sent rows but receives no read counters from the
server. Failed spans also contain an
exception event with the exception type, message, and stack
trace.
SQL text and sensitive data
SQL text is excluded from spans by default. Enable it explicitly and set a suitable limit:StatementMaxLength caps both the db.query.text attribute and SQL in debug logs. A value of zero
or less omits it from both. Review exported exception messages and stack traces as well, because
server errors can contain query details.
Trace context propagation
WhenActivity.Current uses the W3C ID format, the client writes its trace ID, span ID, trace flags,
and trace state into the native query packet. ClickHouse can then create server spans in the same
distributed trace. Server-side span collection and sampling remain ClickHouse configuration; for
example, the sampling probability can be set per query:
system.query_log and, when configured, ClickHouse’s system.opentelemetry_span_log.
Limitations
The TCP client is experimental and does not provide every feature of the HTTP client.HTTP-only features
The TCP API is not an ADO.NET provider and cannot be used with ORMs. It also does not support CSV, JSONEachRow, Parquet, or raw stream input and output; bearer authentication or custom HTTP headers; custom parameter type resolution, formatting, or read conversion; or a per-query database or role. Use the HTTP client when an application needs one of those features.SQL parameters
Only ClickHouse-native{name:Type} placeholders are supported; the TCP client does not rewrite
ADO.NET-style @name placeholders. On ClickHouse 25.8 through 26.6, parameter names that match
server settings, such as limit or offset, may be interpreted as settings and rejected. Rename
them, for example to row_limit, when supporting those versions.
Data type coverage
The supported data types section describes the complete mappings. The current native codec has these notable boundaries:- Tuples can contain at most seven elements.
AggregateFunction(function, ...)intermediate states are not supported. Finalize them on the server, for example withsumMerge(column), and read the result instead.- Only the two-argument, unstrided
QBit(T, N)layout is supported. JSONuses ClickHouse’s String serialization and requiresoutput_format_native_write_json_as_string = 1, which the client requests by default.- Application-built inserts have no row-oriented shape for
Nested(...)whenflatten_nested = 0. Use the default flattened dottedArray(T)columns instead. - An application-built
Geometrycolumn cannot infer between alternatives that share the same CLR shape, such asRingandLineString. Insert the concrete geometry type, or reinsert a decodedGeometrycolumn that retains its discriminators.
Automatic retries
The client does not retry failed operations automatically. Retry only operations that are safe to repeat, and set a stableDeduplicationToken before retrying an insert whose outcome is unknown.
See retrying operations for details.