<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vladimir Chemeris</title>
    <description>The latest articles on DEV Community by Vladimir Chemeris (@chamav).</description>
    <link>https://dev.to/chamav</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3652909%2F01f175cd-4ac1-456d-875d-a661729310f8.png</url>
      <title>DEV Community: Vladimir Chemeris</title>
      <link>https://dev.to/chamav</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chamav"/>
    <language>en</language>
    <item>
      <title>ClickHouse "Too many parts": what to check before you run OPTIMIZE FINAL</title>
      <dc:creator>Vladimir Chemeris</dc:creator>
      <pubDate>Tue, 11 Aug 2026 13:59:02 +0000</pubDate>
      <link>https://dev.to/chamav/clickhouse-too-many-parts-what-to-check-before-you-run-optimize-final-596n</link>
      <guid>https://dev.to/chamav/clickhouse-too-many-parts-what-to-check-before-you-run-optimize-final-596n</guid>
      <description>&lt;p&gt;Disclosure: I build ProbeDeck, an iOS app for monitoring and operating ClickHouse clusters. The SQL&lt;br&gt;
below works in any ClickHouse client. You do not need ProbeDeck to use it.&lt;/p&gt;

&lt;p&gt;An insert fails with this message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Too many parts (N). Merges are processing significantly slower than inserts&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The tempting response is &lt;code&gt;OPTIMIZE TABLE ... FINAL&lt;/code&gt;. Hold that command. ClickHouse raises this error&lt;br&gt;
when a partition accumulates active parts faster than the server can merge them. A forced merge can&lt;br&gt;
consume the same CPU, disk bandwidth, and free space that the background merge process needs.&lt;/p&gt;

&lt;p&gt;Use this order during an incident:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Count active parts by partition and by table.&lt;/li&gt;
&lt;li&gt;Read the live delay, throw, and total-part thresholds from the cluster.&lt;/li&gt;
&lt;li&gt;Sample active merges twice to see whether the backlog shrinks.&lt;/li&gt;
&lt;li&gt;Check disk headroom and, for replicated tables, replica health.&lt;/li&gt;
&lt;li&gt;Reduce part creation at the writer or partitioning layer.&lt;/li&gt;
&lt;li&gt;Consider a forced merge after you control incoming pressure.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Find the affected partition
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;parts_to_throw_insert&lt;/code&gt; applies to active parts in one partition. A table-wide count can hide the&lt;br&gt;
shape of the problem. Run this first:&lt;/p&gt;

&lt;p&gt;The system tables below report data from the node that serves your query. A load balancer can route&lt;br&gt;
you to a healthy replica while another replica holds the backlog. Connect to each node, or list the&lt;br&gt;
configured cluster names with &lt;code&gt;SELECT DISTINCT cluster FROM system.clusters&lt;/code&gt; and replace a local&lt;br&gt;
table such as &lt;code&gt;system.parts&lt;/code&gt; with &lt;code&gt;clusterAllReplicas('my_cluster', system.parts)&lt;/code&gt;. Keep&lt;br&gt;
&lt;code&gt;hostName()&lt;/code&gt; in the result so you can see which node produced each row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;hostName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active_parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;countIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;level0_parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;highest_level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_on_disk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;size_on_disk&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'system'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'INFORMATION_SCHEMA'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'information_schema'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;partition&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;active_parts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If one partition sits far above the rest, inspect the hot partition and its partitioning key. If you&lt;br&gt;
see &lt;code&gt;level0_parts&lt;/code&gt; grow between samples, inspect the writer for frequent small inserts. If the count&lt;br&gt;
is high but spread across healthy partitions, compare the table total with &lt;code&gt;max_parts_in_total&lt;/code&gt;&lt;br&gt;
below.&lt;/p&gt;

&lt;p&gt;Do not copy a threshold from a blog post. Read the settings on the cluster you are debugging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;hostName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;changed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merge_tree_settings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'parts_to_delay_insert'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'parts_to_throw_insert'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'max_parts_in_total'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'max_avg_part_size_for_too_many_parts'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;system.merge_tree_settings&lt;/code&gt; gives you the server-level value. A table can override it in its own&lt;br&gt;
&lt;code&gt;SETTINGS&lt;/code&gt; clause. Check the table definition before you treat the result as the effective limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The writer can also pass settings with the insert query. Run the checks through the same user and&lt;br&gt;
connection path as the writer, then inspect its client configuration. Treat an app or dashboard&lt;br&gt;
threshold as a warning heuristic unless it reads the effective setting.&lt;/p&gt;

&lt;p&gt;ClickHouse's current guide to &lt;code&gt;OPTIMIZE FINAL&lt;/code&gt; uses 150 active parts as an early-warning heuristic.&lt;br&gt;
Your cluster limit comes from its effective settings. Investigate when two samples show growth or&lt;br&gt;
level-0 parts dominate. A count near &lt;code&gt;parts_to_delay_insert&lt;/code&gt; means ClickHouse may throttle inserts&lt;br&gt;
soon; &lt;code&gt;parts_to_throw_insert&lt;/code&gt; rejects them. &lt;code&gt;max_avg_part_size_for_too_many_parts&lt;/code&gt; can disable the&lt;br&gt;
delay and throw checks when the average part size in the affected partition exceeds its value. The&lt;br&gt;
total table limit still applies.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;max_parts_in_total&lt;/code&gt; protects the table across all partitions. Check the matching table total:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;hostName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active_parts_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'system'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'INFORMATION_SCHEMA'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'information_schema'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;active_parts_total&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check whether merges are making progress
&lt;/h2&gt;

&lt;p&gt;Next, inspect the active merge workload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;hostName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;progress_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_mutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;merge_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_size_bytes_compressed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;source_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory_usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merges&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;system.merges&lt;/code&gt; shows work in progress, not a queue. An empty result does not prove that merges have&lt;br&gt;
stopped. Read it together with two &lt;code&gt;system.parts&lt;/code&gt; samples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parts are high and merge progress changes between samples: the cluster is working through a
backlog. Reduce incoming pressure and keep measuring.&lt;/li&gt;
&lt;li&gt;Parts keep rising while no merge appears for the affected table: inspect disk space, merge-pool
pressure, disabled merges, replication state, and recent configuration changes.&lt;/li&gt;
&lt;li&gt;Merges run, but new level-0 parts arrive faster: fix the writer. More merge threads will not cure
a producer that creates parts without bound.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your server writes &lt;code&gt;system.part_log&lt;/code&gt;, estimate how completed merges changed the part count over&lt;br&gt;
the same window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;hostName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;countIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NewPart'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;countIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'MergeParts'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;merge_outputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sumIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merged_from&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'MergeParts'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;merge_inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;new_parts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;merge_outputs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;merge_inputs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;estimated_net_change&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;part_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;MINUTE&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'analytics'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'events'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'NewPart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'MergeParts'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;MergeParts&lt;/code&gt; event can consume several source parts, so raw event counts use different units.&lt;br&gt;
The estimate above ignores downloads, removals, and events that cross the time-window boundary.&lt;br&gt;
Use repeated &lt;code&gt;system.parts&lt;/code&gt; snapshots as the primary signal. Replace the example identifiers and&lt;br&gt;
save two or three samples a few minutes apart.&lt;/p&gt;

&lt;p&gt;Stalled merges also need disk and replica checks. On self-managed clusters, inspect every server&lt;br&gt;
that stores the affected table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;hostName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;free_space&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;free&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unreserved_space&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;unreserved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_space&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keep_free_space&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disks&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No fixed free-space percentage fits every merge because candidate parts and storage policies differ.&lt;br&gt;
Treat disk as the bottleneck when &lt;code&gt;unreserved_space&lt;/code&gt; keeps falling while merges stall, or when the&lt;br&gt;
server log reports space-reservation failures. Check disk saturation in your host monitoring. For&lt;br&gt;
&lt;code&gt;ReplicatedMergeTree&lt;/code&gt; tables, inspect replica state as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;hostName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_readonly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_session_expired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;queue_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;inserts_in_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;merges_in_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;queue_oldest_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;absolute_delay&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replicas&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_readonly&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;is_session_expired&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;queue_size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;queue_size&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A nonzero queue can be normal. Sample it twice. A read-only replica, an expired session, a growing&lt;br&gt;
queue, or rising &lt;code&gt;absolute_delay&lt;/code&gt; points to replication trouble and adds pressure to ClickHouse&lt;br&gt;
Keeper.&lt;/p&gt;
&lt;h2&gt;
  
  
  Check the writer before tuning the server
&lt;/h2&gt;

&lt;p&gt;Each synchronous insert can create at least one part for every partition touched by its block.&lt;br&gt;
ClickHouse recommends batching at least 1,000 rows per insert, with 10,000 to 100,000 rows as the&lt;br&gt;
better range for many workloads. The right batch size still depends on row width, latency, memory,&lt;br&gt;
and the number of partitions touched. For synchronous ingestion, start near one insert per second&lt;br&gt;
and adjust from measurements.&lt;/p&gt;

&lt;p&gt;Check these failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A producer sends one row or a tiny batch per request.&lt;/li&gt;
&lt;li&gt;One insert touches many partitions.&lt;/li&gt;
&lt;li&gt;Several writers use incompatible async-insert settings, so ClickHouse cannot combine their data
into one buffer.&lt;/li&gt;
&lt;li&gt;Materialized views multiply one source insert into parts in several target tables.&lt;/li&gt;
&lt;li&gt;Mutations, TTL work, or disk pressure compete with regular merges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check partition granularity before you tune merge pools. Run &lt;code&gt;SHOW CREATE TABLE&lt;/code&gt; and count the&lt;br&gt;
active partitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;hostName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uniqExact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active_partitions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active_parts&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'system'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'INFORMATION_SCHEMA'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'information_schema'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;active_partitions&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A large partition count alone does not prove a bad key. Look for a key that opens a partition per&lt;br&gt;
ID, per tenant, or per timestamp truncated to an hour or less. ClickHouse cannot merge parts across&lt;br&gt;
partition boundaries. Fixing that design requires a replacement table with a coarser partition key&lt;br&gt;
and a controlled data migration.&lt;/p&gt;

&lt;p&gt;ClickHouse 26.3 LTS enables async inserts by default. On older versions, enable them on the ingest&lt;br&gt;
path when client-side batching is not practical. On 26.3 and later, verify that the client did not&lt;br&gt;
override the setting. Check it using the same user and connection settings as the writer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;hostName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'async_insert'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wait_for_async_insert'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'compatibility'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An older pinned &lt;code&gt;compatibility&lt;/code&gt; value can preserve the previous async-insert default after a server&lt;br&gt;
upgrade. Trust the &lt;code&gt;async_insert&lt;/code&gt; value returned through the writer's connection instead of the&lt;br&gt;
server version alone.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;wait_for_async_insert = 1&lt;/code&gt; when the client needs acknowledgement after the buffer reaches&lt;br&gt;
durable storage. With &lt;code&gt;0&lt;/code&gt;, the server can acknowledge data while it still lives in memory, and the&lt;br&gt;
client will not receive flush errors in the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;OPTIMIZE FINAL&lt;/code&gt; does not fix ingestion
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;OPTIMIZE TABLE ... FINAL&lt;/code&gt; rewrites the parts that exist now. It does not change the rate at which&lt;br&gt;
the writer creates new ones. When ingestion resumes, the exception can return. The forced merge can&lt;br&gt;
also create a large part and consume resources on a cluster that already lacks merge capacity.&lt;/p&gt;

&lt;p&gt;Reserve a forced merge for cases where you understand the affected table and partition, have enough&lt;br&gt;
disk and I/O headroom, and have reduced the source of new parts. Raising &lt;code&gt;parts_to_throw_insert&lt;/code&gt; has&lt;br&gt;
the same problem: it moves the safety boundary without repairing ingestion.&lt;/p&gt;

&lt;p&gt;The durable fixes are upstream:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Batch more rows per insert.&lt;/li&gt;
&lt;li&gt;Reduce insert frequency.&lt;/li&gt;
&lt;li&gt;Use async inserts where they fit the durability and latency requirements.&lt;/li&gt;
&lt;li&gt;Revisit a high-cardinality partition key through a planned migration.&lt;/li&gt;
&lt;li&gt;Tune merge resources after measurements show a server-side bottleneck.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;During an incident, I run the same checks from the &lt;a href="https://probedeck.app/blog/clickhouse-too-many-parts-mobile/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=probedeck_parts"&gt;ProbeDeck guide to ClickHouse &lt;code&gt;Too many parts&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
ProbeDeck shows raw maximum active-part counts and current merges from an iPhone or iPad. Compare&lt;br&gt;
those counts with the effective settings on your cluster. Monitoring is free. Credentials stay in&lt;br&gt;
the iOS Keychain, and the app has no backend between the device and your cluster.&lt;/p&gt;

&lt;p&gt;ClickHouse is a registered trademark of ClickHouse, Inc. ProbeDeck is not affiliated with,&lt;br&gt;
endorsed by, or sponsored by ClickHouse, Inc.&lt;/p&gt;

</description>
      <category>sre</category>
      <category>clickhouse</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why iperf3 Says “Bad File Descriptor” and What to Check First</title>
      <dc:creator>Vladimir Chemeris</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:58:34 +0000</pubDate>
      <link>https://dev.to/chamav/why-iperf3-says-bad-file-descriptor-and-what-to-check-first-2f2c</link>
      <guid>https://dev.to/chamav/why-iperf3-says-bad-file-descriptor-and-what-to-check-first-2f2c</guid>
      <description>&lt;p&gt;Disclosure: I develop an iPerf3 app for Apple platforms and Android. The examples below use the upstream iperf3 CLI and apply to compatible clients and servers.&lt;/p&gt;

&lt;p&gt;Suppose iperf3 prints this line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;iperf3: error - unable to write to the control socket:&lt;/code&gt; &lt;code&gt;Bad file descriptor&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keep the whole line. &lt;code&gt;Bad file descriptor&lt;/code&gt; comes from your operating system. It does not identify the operation that failed. The text before it names the iperf3 stage.&lt;/p&gt;

&lt;p&gt;In this case, iperf3 tried to write to its control socket. The operating system rejected the descriptor passed to that write with &lt;code&gt;EBADF&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One line, two sources
&lt;/h2&gt;

&lt;p&gt;iperf3 builds many error strings from two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iperf3 operation:&lt;/strong&gt; &lt;code&gt;unable to write to the control socket&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS errno:&lt;/strong&gt; &lt;code&gt;Bad file descriptor&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The iperf3 message answers: &lt;strong&gt;What was the program doing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;errno&lt;/code&gt; text answers: &lt;strong&gt;Why did that system call fail?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The implementation is visible in &lt;a href="https://github.com/esnet/iperf/blob/master/src/iperf_error.c" rel="noopener noreferrer"&gt;&lt;code&gt;src/iperf_error.c&lt;/code&gt;&lt;/a&gt;. For &lt;code&gt;IECTRLWRITE&lt;/code&gt;, iperf3 starts with &lt;code&gt;unable to write to the control socket&lt;/code&gt;. It then appends &lt;code&gt;strerror(errno)&lt;/code&gt; when the failing call supplied an errno.&lt;/p&gt;

&lt;p&gt;Some messages have no appended errno. &lt;code&gt;control socket has closed unexpectedly&lt;/code&gt; is one example. iperf3 uses it after a control read returns no state byte. On the client, an empty read points to end-of-file from the peer. The server can reach the same message after its timed read returns without data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;EBADF&lt;/code&gt; tells you
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Bad file descriptor&lt;/code&gt; is the text for &lt;code&gt;EBADF&lt;/code&gt;. The process passed an invalid descriptor to a system call.&lt;/p&gt;

&lt;p&gt;That establishes one local fact: the descriptor was invalid inside the process that printed the error. It does not tell you why the descriptor reached that state. Possible triggers include cleanup after another failure, a descriptor-lifetime bug, or code running after a connection teardown.&lt;/p&gt;

&lt;p&gt;A firewall or the remote peer can start a failure sequence by closing a connection. The final &lt;code&gt;EBADF&lt;/code&gt; still describes only the local system call that failed at the end of that sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Similar messages point to different stages
&lt;/h2&gt;

&lt;p&gt;Read the iperf3 part before interpreting the suffix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;Failed stage&lt;/th&gt;
&lt;th&gt;First check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unable to write to the control socket: &amp;lt;errno&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A write to the control socket failed.&lt;/td&gt;
&lt;td&gt;Check the previous state exchange and the appended errno.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unable to receive cookie at server: &amp;lt;errno&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The server accepted a data-stream socket but could not read its cookie.&lt;/td&gt;
&lt;td&gt;Compare the client log and the server errno.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;select failed: &amp;lt;errno&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The kernel rejected something in a &lt;code&gt;select()&lt;/code&gt; descriptor set.&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;--debug&lt;/code&gt; to find the last completed stage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;control socket has closed unexpectedly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A control read returned no state byte. No errno is appended.&lt;/td&gt;
&lt;td&gt;Compare both logs at the same timestamp.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table explains why searching only for &lt;code&gt;bad file descriptor&lt;/code&gt; produces weak answers. The same suffix can appear after several iperf3 operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  A debugging sequence that keeps the evidence
&lt;/h2&gt;

&lt;p&gt;Start with the process that printed the line. Then collect the matching evidence from the other side.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save the complete client and server commands.&lt;/li&gt;
&lt;li&gt;Save the full output from both processes.&lt;/li&gt;
&lt;li&gt;Record &lt;code&gt;iperf3 --version&lt;/code&gt; on both machines. iperf2 and iperf3 do not interoperate.&lt;/li&gt;
&lt;li&gt;Classify the failed stage from the iperf3 message.&lt;/li&gt;
&lt;li&gt;Interpret &lt;code&gt;errno&lt;/code&gt; only after you know that stage.&lt;/li&gt;
&lt;li&gt;Repeat the test with debug logs:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iperf3 &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--debug&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;server.log 2&amp;gt;&amp;amp;1
iperf3 &lt;span class="nt"&gt;-c&lt;/span&gt; 192.168.1.10 &lt;span class="nt"&gt;--debug&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;client.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Match the last completed state on both sides. If one log stops first, check whether that host slept, the process exited, or the network path changed.&lt;/p&gt;

&lt;p&gt;Run a short and a long test when the failure appears late:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iperf3 &lt;span class="nt"&gt;-c&lt;/span&gt; 192.168.1.10 &lt;span class="nt"&gt;-t&lt;/span&gt; 30
iperf3 &lt;span class="nt"&gt;-c&lt;/span&gt; 192.168.1.10 &lt;span class="nt"&gt;-t&lt;/span&gt; 300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A short run that passes and a long run that fails gives you a time-dependent lead. Check idle firewall state, device sleep, Wi-Fi roaming, and process restarts. Treat each as a testable candidate.&lt;/p&gt;

&lt;p&gt;If the logs disagree about which side stopped first, capture port 5201 and compare packet timestamps with both logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;tcpdump &lt;span class="nt"&gt;-ni&lt;/span&gt; any tcp port 5201
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a real interface name on systems without the &lt;code&gt;any&lt;/code&gt; pseudo-interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other suffixes use the same method
&lt;/h2&gt;

&lt;p&gt;The same reading order works for &lt;code&gt;Connection refused&lt;/code&gt;, &lt;code&gt;Permission denied&lt;/code&gt;, and &lt;code&gt;Address already in use&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;unable to connect to server: Connection refused&lt;/code&gt; identifies a failed connect attempt. The suffix says the destination rejected that TCP connection. Check the server process, address, and port before changing unrelated socket settings.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://iperf3app.com/blog/iperf3-error-messages/" rel="noopener noreferrer"&gt;full iperf3 error reference&lt;/a&gt; covers busy servers, listener failures, temporary-file permissions, socket buffers, mobile sleep, and long-test failures.&lt;/p&gt;

&lt;p&gt;If you want to run the same tests from an iPhone, iPad, or Mac, the &lt;a href="https://iperf3app.com/ios/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=iperf3_errors"&gt;iPerf3 app for Apple platforms&lt;/a&gt; can act as a client or server.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>networking</category>
      <category>tutorial</category>
      <category>performance</category>
    </item>
    <item>
      <title>Measuring real LAN speed from a phone with iperf3</title>
      <dc:creator>Vladimir Chemeris</dc:creator>
      <pubDate>Wed, 05 Aug 2026 16:49:27 +0000</pubDate>
      <link>https://dev.to/chamav/measuring-real-lan-speed-from-a-phone-with-iperf3-3644</link>
      <guid>https://dev.to/chamav/measuring-real-lan-speed-from-a-phone-with-iperf3-3644</guid>
      <description>&lt;p&gt;Disclosure before we start: I build the native iPerf3 apps for iPhone, iPad, Mac, and Android mentioned near the end. Everything in this post also works with the free iperf3 CLI on two computers. The phone enters the picture when one endpoint has to move around the building.&lt;/p&gt;

&lt;p&gt;Speedtest answers one question: how fast is the path from your device to the ISP's nearest test server. Inside a home or office a different question comes up: how fast is the link between the phone and the NAS, between two mesh nodes, between the office switch and the upstairs access point. Traffic between two local devices never leaves the LAN, so an internet speed test cannot see it.&lt;/p&gt;

&lt;p&gt;iperf3 measures that link. ESnet maintains it as a from-scratch redesign of the original iperf, and common Linux, BSD, and macOS package repositories carry it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How iperf3 measures
&lt;/h2&gt;

&lt;p&gt;iperf3 is a client-server tool. One device listens (&lt;code&gt;iperf3 -s&lt;/code&gt;), the other connects to it, pushes traffic for a fixed duration, and counts what arrived. Because you control both endpoints, the result describes one specific link instead of a whole path across the internet.&lt;/p&gt;

&lt;p&gt;The server side installs in one line on Linux and macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian/Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;iperf3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; iperf3 &lt;span class="nt"&gt;-s&lt;/span&gt;

&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;iperf3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; iperf3 &lt;span class="nt"&gt;-s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ESnet does not publish or support native Windows binaries. If you need a Windows endpoint, check the &lt;a href="https://software.es.net/iperf/obtaining.html" rel="noopener noreferrer"&gt;upstream installation notes&lt;/a&gt; before choosing a third-party build.&lt;/p&gt;

&lt;p&gt;Some router and NAS platforms make iperf3 available through their package systems. Availability depends on the model and firmware, so check the package source before treating the appliance as a permanent server.&lt;/p&gt;

&lt;h2&gt;
  
  
  A five-minute test
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start the server on a wired device and note its LAN IP, for example &lt;code&gt;192.168.1.10&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;From the client, start with the default ten-second test. Use sixty seconds when you need to see changes caused by interference, roaming, or sustained load:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iperf3 &lt;span class="nt"&gt;-c&lt;/span&gt; 192.168.1.10 &lt;span class="nt"&gt;-t&lt;/span&gt; 60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Read the &lt;code&gt;receiver&lt;/code&gt; line at the end. That number is the throughput the link sustained.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A healthy wired 1GbE TCP link often sustains about 930 to 950 Mbit/s after protocol overhead. Wi-Fi varies with channel width, spatial streams, interference, access-point configuration, and the phone's radio. Compare the same phone and settings across locations instead of judging a result from the Wi-Fi generation label alone. If a wired link reports far below its expected rate, check the negotiated link speed, cable, switch port, USB adapter, and CPU load on both endpoints.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Now walk. Run the same test from the far bedroom, the garage, the conference room. The room-to-room gap is the thing a signal-strength bar hides.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Which output fields to read
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Throughput&lt;/strong&gt; is the headline number: application data delivered per second, usually shown in Mbit/s or Gbit/s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retransmits&lt;/strong&gt; (TCP) count segments the sender had to transmit again. A clean wired LAN should produce few retransmits. A rising count on repeated runs gives you a reason to isolate the cable, port, queueing, or wireless hop; the counter alone does not identify the cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jitter and packet loss&lt;/strong&gt; appear in UDP mode. Set the target below the link's measured TCP capacity, then raise it in steps. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iperf3 &lt;span class="nt"&gt;-c&lt;/span&gt; 192.168.1.10 &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; 100M &lt;span class="nt"&gt;-t&lt;/span&gt; 60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Packet loss and jitter affect calls and games before raw throughput becomes the obvious problem. Your acceptable limits depend on the application, codec, and traffic pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traps that produce wrong numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Both endpoints on Wi-Fi.&lt;/strong&gt; You measure the weaker of two radio links plus their contention for airtime. Put the server on Ethernet and test the wireless hop you care about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;USB Ethernet adapters.&lt;/strong&gt; USB 2.0 adapters often top out around 300 to 350 Mbit/s in practice. Check the adapter and negotiated link speed before blaming the network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short tests.&lt;/strong&gt; Connection setup, TCP ramp-up, and brief interference can dominate a short result. Use longer runs when you care about stability under sustained load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A sleeping phone.&lt;/strong&gt; Some phones reduce background network activity or suspend the test when the screen locks. Keep the app in the foreground and the screen on during a run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running iperf3 on the phone itself
&lt;/h2&gt;

&lt;p&gt;The phone is often the endpoint that matters: it is the device the complaint is about, and it is the device you carry from room to room. Options for a native endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iPhone / iPad / Mac:&lt;/strong&gt; &lt;a href="https://iperf3app.com/ios/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=iperf3_lan"&gt;iPerf3 Client &amp;amp; Server&lt;/a&gt; on the App Store. One purchase ($6.99, one-time) covers iPhone, iPad, and Mac. Client and server modes, TCP/UDP, live charts, history, CSV/JSON export, Apple Shortcuts automation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.iperf3client.pro&amp;amp;referrer=utm_source%3Ddev.to%26utm_medium%3Dreferral%26utm_campaign%3Diperf3_lan"&gt;iPerf3 TCP/UDP Client &amp;amp; Server&lt;/a&gt; on Google Play ($3.99, one-time). Same idea: both modes, advanced parameters, export.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both apps use the iperf3 protocol, so a phone in server mode can accept tests from compatible iperf3 CLI versions and vice versa. If two endpoints fail to negotiate, compare their iperf3 versions before debugging the network.&lt;/p&gt;

&lt;p&gt;Two phones make a portable end-to-end test kit: put one in server mode and use the other as the client. Because both endpoints share Wi-Fi airtime, this setup cannot isolate mesh backhaul performance. Use a wired server when you need to test one wireless hop or compare rooms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go deeper
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/esnet/iperf" rel="noopener noreferrer"&gt;iperf3 upstream project&lt;/a&gt; (source, releases, and official documentation)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://iperf3app.com/blog/how-to-test-lan-speed/" rel="noopener noreferrer"&gt;How to test LAN speed, step by step&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://iperf3app.com/blog/speedtest-vs-iperf3/" rel="noopener noreferrer"&gt;Why Speedtest and iperf3 answer different questions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions about a weird result are welcome in the comments; I read all of them.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>ios</category>
      <category>android</category>
      <category>homelab</category>
    </item>
  </channel>
</rss>
