Bali, Indonesia
Insights

Optimizing ClickHouse for High-Throughput Web Log Analytics and Real-Time Telemetry

October 12, 2026
Modern web hosting infrastructure, content delivery networks, and microservice mesh layers generate staggering volumes of access logs, metrics, and security telemetry. Traditional relational databases and standard key-value stores crumble under the write pressure and analytical complexity of billions of daily events. ClickHouse has emerged as the premier columnar database management system for handling petabyte-scale analytical workloads. However, deploying ClickHouse out-of-the-box without meticulous storage engine and memory tuning frequently leads to part explosion, excessive background merges, and unexpected out-of-memory terminations. In this technical guide, we examine production-tested strategies for architecting high-throughput ClickHouse ingest pipelines, optimizing MergeTree family tables, configuring asynchronous inserts, and tuning system-level memory footprints for maximum query concurrency.
At the core of ClickHouse's high ingestion and query performance is the 'MergeTree' engine family. Unlike row-oriented databases that write complete records sequentially, ClickHouse stores data column by column. This architecture allows analytical queries scanning specific metrics to read only the relevant disk blocks, minimizing disk input-output operations. When data streams into ClickHouse, it is written to the file system as immutable parts. Background processes subsequently merge these small parts into larger ones to maintain read efficiency. If your ingest rate exceeds your merge rate, you encounter the dreaded 'Too many parts' exception. To prevent this, data ingestion must be carefully buffered and structured.
Consider a production HTTP access log pipeline ingesting 200,000 events per second. A poorly designed schema with incorrect primary and secondary indexing will cripple query performance.
CREATE TABLE default.http_access_logs (
    event_date Date,
    event_time DateTime,
    client_ip String,
    http_method LowCardinality(String),
    request_uri String,
    status_code UInt16,
    bytes_sent UInt64,
    request_time_ms Float32,
    user_agent String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (http_method, status_code, event_date, event_time)
SETTINGS index_granularity = 8192;
In this schema design, several crucial best practices are applied:
  • LowCardinality: Fields with limited distinct values such as HTTP methods ('GET', 'POST', 'PUT') are wrapped in 'LowCardinality(String)' to compress data dictionary representations in memory.
  • Partitioning: Partitioning by month via 'toYYYYMM(event_date)' allows ClickHouse to drop entire data directories instantly during data retention purges without executing expensive row-level delete statements.
  • Primary Key Ordering: Ordering columns from lowest cardinality to highest cardinality maximizes sparse index effectiveness and data compression ratios.
High-frequency clients sending individual HTTP requests or small batch payloads can easily saturate ClickHouse with thousands of micro-inserts per second, quickly exhausting file descriptors and creating millions of tiny parts. Enabling asynchronous inserts instructs ClickHouse to temporarily buffer incoming data in memory and flush it to disk in optimized batches.
Apply the following settings inside your server configuration profile or via user settings profiles to aggregate small inserts automatically:
<profiles>
    <default>
        <async_insert>1</async_insert>
        <async_insert_threads>4</async_insert_threads>
        <async_insert_max_data_size>10000000</async_insert_max_data_size>
        <async_insert_busy_timeout_ms>1000</async_insert_busy_timeout_ms>
    </default>
</profiles>
Setting ParameterRecommended Production ValueDescription
'async_insert''1'Globally enables asynchronous buffering for client insert queries.
'async_insert_max_data_size''10000000' (10MB)Maximum uncompressed data size accumulated before triggering a forced flush.
'async_insert_busy_timeout_ms''1000' (1s)Maximum time window data stays in memory before being written to disk.
'async_insert_threads''4'Dedicated worker threads processing background asynchronous batch writes.
ClickHouse is aggressively optimized to utilize all available system memory to accelerate sorts, aggregations, and joins. However, unconstrained queries can easily exhaust system RAM, triggering the Linux kernel's Out-Of-Memory killer and terminating the entire ClickHouse daemon. Proper resource governance requires configuring both server-wide memory limits and per-query execution guardrails.
Edit your user configuration profile to enforce strict memory boundaries:
<profiles>
    <default>
        <max_memory_usage>32000000000</max_memory_usage>
        <max_memory_usage_for_all_queries>128000000000</max_memory_usage_for_all_queries>
        <load_balancing>in_order</load_balancing>
    </default>
</profiles>
  • 'max_memory_usage': Restricts a single query to roughly 32 gigabytes of RAM before throwing an execution error instead of crashing the host.
  • 'max_memory_usage_for_all_queries': Limits total cumulative memory consumption across all concurrent queries on the instance to 128 gigabytes, protecting underlying operating system stability.
ClickHouse performance is heavily bound by storage subsystem throughput, particularly during background part merges. Utilizing high-performance NVMe storage arrays with optimized filesystem mount options yields substantial gains.
Ensure your NVMe drives are mounted with appropriate filesystem flags and the Linux kernel scheduler is optimized for solid-state non-rotational media.
# Mount options in /etc/fstab for NVMe data partitions
UUID=your-uuid-here /var/lib/clickhouse ext4 noatime,nodiratime,errors=remount-ro,discard 0 0

# Tune kernel virtual memory and dirty page flushing for high write throughput
echo 'vm.swappiness=1' >> /etc/sysctl.d/99-clickhouse.conf
echo 'vm.dirty_background_ratio=5' >> /etc/sysctl.d/99-clickhouse.conf
echo 'vm.dirty_ratio=10' >> /etc/sysctl.d/99-clickhouse.conf
sysctl --system
To verify that your tuning parameters are effective, query the internal 'system' database tables regularly to check part counts, merge speeds, and memory usage patterns.
SELECT 
    database,
    table,
    count() AS part_count,
    sum(bytes_on_disk) / 1024 / 1024 / 1024 AS size_gb,
    max(modification_time) AS last_modified
FROM system.parts
WHERE active
GROUP BY database, table
ORDER BY part_count DESC;
Monitoring the active part count ensures that your background merges are keeping pace with your ingest pipelines. If part counts consistently climb into the thousands per table, inspect your merge tree settings or increase background pool threads in 'config.xml' via 'background_pool_size'.
Scaling ClickHouse for high-throughput web log analytics and real-time telemetry requires a disciplined approach to schema design, memory governance, and ingestion buffering. By implementing strict column type definitions like 'LowCardinality', enabling asynchronous inserts for high-concurrency event streams, and enforcing robust memory limits, engineers can achieve sustainable, line-rate data processing capabilities without risking cluster stability.