<?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>Why Your MQTT TLS Connection Fails, Stage by Stage</title>
      <dc:creator>Vladimir Chemeris</dc:creator>
      <pubDate>Tue, 08 Sep 2026 16:34:03 +0000</pubDate>
      <link>https://dev.to/chamav/why-your-mqtt-tls-connection-fails-stage-by-stage-3ngm</link>
      <guid>https://dev.to/chamav/why-your-mqtt-tls-connection-fails-stage-by-stage-3ngm</guid>
      <description>&lt;p&gt;I build &lt;a href="https://mqttcommander.com/" rel="noopener noreferrer"&gt;MQTT Commander&lt;/a&gt;, an MQTT client for iPhone and iPad, so I read a lot of other people's failed TLS handshakes. The same half-dozen causes come up every time, and you can pin most of them down before you touch the broker config.&lt;/p&gt;

&lt;p&gt;Plain MQTT on port 1883 either connects or it doesn't. Add TLS and five more things sit between you and a working session: the certificate chain, hostname verification, ALPN, client certificates and broker-side authorization. The MQTT client that reports the failure sees a closed socket and little else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three layers, in order
&lt;/h2&gt;

&lt;p&gt;Before the first MQTT packet moves, three things have to succeed one after another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TCP.&lt;/strong&gt; The client resolves the broker hostname, opens a socket on 8883 (MQTT over TLS) or 443 / 8084 (MQTT over WebSocket and TLS), and completes the handshake. A firewall, a wrong port or a broker that isn't running fails here, and the fix is on the network side even when the error text mentions TLS. On iOS a LAN broker that never answers has one more possible cause: the app has no Local Network permission in Settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS handshake.&lt;/strong&gt; Client and server agree on a cipher suite, exchange certificates and set up the encrypted channel, and most secure-MQTT failures land somewhere in that exchange.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MQTT CONNECT.&lt;/strong&gt; Only now does the protocol itself start. The client sends &lt;code&gt;CONNECT&lt;/code&gt;, the broker answers with &lt;code&gt;CONNACK&lt;/code&gt;. A non-zero reason code at this point means the broker refused you on purpose: bad credentials, a client ID it won't accept, an ACL that denies you. TLS finished before any of that.&lt;/p&gt;

&lt;p&gt;Find out which of the three failed before you change anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where TLS breaks
&lt;/h2&gt;

&lt;p&gt;Most MQTT clients collapse a TLS failure into "connection failed". To see the real verdict, ask OpenSSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; broker.example.com:8883 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-servername&lt;/span&gt; broker.example.com &lt;span class="nt"&gt;-verify_hostname&lt;/span&gt; broker.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;verify error&lt;/code&gt; lines below are what it prints for each cause. Other TLS stacks word them differently, but the categories are the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  An expired or incomplete chain
&lt;/h3&gt;

&lt;p&gt;Every certificate in the chain expires, intermediates included. An expired intermediate breaks the connection while the leaf certificate is still valid, and the leaf is the one you check by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verify error:num=10:certificate has expired
verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codes 20 and 21 mean the chain stops short of a root you trust. Either the broker sends only its leaf certificate and skips the intermediate, or the root is a private CA you haven't imported yet (next section). The first case you fix on the broker: every other client hits the same wall. Code 9, &lt;code&gt;certificate is not yet valid&lt;/code&gt;, usually means the device clock is wrong rather than the certificate.&lt;/p&gt;

&lt;h3&gt;
  
  
  A private CA the device doesn't trust
&lt;/h3&gt;

&lt;p&gt;Mosquitto on a Raspberry Pi, an internal staging broker, an AWS IoT custom endpoint: a private CA signs all of these, and no phone trusts that CA out of the box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verify error:num=19:self-signed certificate in certificate chain
verify error:num=18:self-signed certificate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code 19 is a private root; code 18 is a broker that serves a self-signed leaf with no CA at all. Import the root as PEM (for code 18, the broker's certificate is its own root) and compare its &lt;code&gt;SHA-256&lt;/code&gt; fingerprint with the one your CA published before you trust it. Whoever holds the key of an unverified root can impersonate your broker.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hostname isn't in the certificate
&lt;/h3&gt;

&lt;p&gt;TLS clients check that the hostname you connected to appears in the certificate's &lt;code&gt;SAN&lt;/code&gt; (Subject Alternative Name); a name that only appears in &lt;code&gt;CN&lt;/code&gt; no longer counts on iOS. The match is exact, so &lt;code&gt;broker.example.com&lt;/code&gt; and &lt;code&gt;mqtt.example.com&lt;/code&gt; are different names to TLS even when they point at the same box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verify error:num=62:hostname mismatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect using a name that is in the &lt;code&gt;SAN&lt;/code&gt;. If you have to reach the broker by an address that isn't in the certificate, send the right server name during the handshake with an &lt;code&gt;SNI&lt;/code&gt; override instead of editing your hosts file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing or wrong ALPN
&lt;/h3&gt;

&lt;p&gt;Some brokers multiplex MQTT onto a shared port and use &lt;code&gt;ALPN&lt;/code&gt; to route it. AWS IoT Core takes MQTT with certificate authentication on port 443 only when the client advertises &lt;code&gt;x-amzn-mqtt-ca&lt;/code&gt;. Leave it out and the handshake dies with a generic alert, or the socket closes right after it, and nothing in the message says ALPN. One common form of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error:0A000438:SSL routines::tlsv1 alert internal error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard MQTT over TLS on 8883 needs no ALPN entry in most setups. On 443, ALPN is the first thing to check. The &lt;a href="https://mqttcommander.com/blog/aws-iot-mqtt-ios/" rel="noopener noreferrer"&gt;AWS IoT guide&lt;/a&gt; on my site lists the exact values for that endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  A TLS version the client won't negotiate
&lt;/h3&gt;

&lt;p&gt;iOS negotiates TLS 1.2 or 1.3 and refuses 1.0 and 1.1. A broker built on an old library that tops out at 1.1, which still happens on legacy gateways and old embedded images, produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error:0A000102:SSL routines:ssl_choose_client_version:unsupported protocol
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client is refusing what the broker offered, so the fix is on the broker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutual TLS
&lt;/h2&gt;

&lt;p&gt;Standard TLS authenticates the broker to you. mTLS authenticates you back, which is why AWS IoT device provisioning and industrial fleets use it: each device carries its own certificate instead of a shared password.&lt;/p&gt;

&lt;p&gt;You need three things: a client certificate, its private key, and the CA that signed both ends. They arrive as a passphrase-protected &lt;code&gt;.p12&lt;/code&gt; / &lt;code&gt;.pfx&lt;/code&gt; bundle, or as separate PEM files.&lt;/p&gt;

&lt;p&gt;Three failures cover most mTLS trouble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The passphrase is wrong.&lt;/strong&gt; Import fails before anything reaches the network, and OpenSSL calls it &lt;code&gt;mac verify failure&lt;/code&gt;. Passphrases are case-sensitive. If you have lost it, re-export the bundle from the CA rather than guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The certificate and key don't match.&lt;/strong&gt; Import the client identity as a single &lt;code&gt;.p12&lt;/code&gt; bundle and the pair stays together by construction. With separate PEM files, a key from the wrong export surfaces as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;key values mismatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The handshake succeeds and the broker hangs up anyway.&lt;/strong&gt; The broker accepted your certificate and then refused the session on authorization grounds. In AWS IoT Core, look for a policy that doesn't grant &lt;code&gt;iot:Connect&lt;/code&gt; for the client ID you present. With MQTT 5 the &lt;code&gt;CONNACK&lt;/code&gt; reason code says so: &lt;code&gt;0x87&lt;/code&gt;, not authorized. With MQTT 3.1.1 the broker often closes the socket without a &lt;code&gt;CONNACK&lt;/code&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading CONNACK
&lt;/h2&gt;

&lt;p&gt;If TLS completed and the session still dies, the reason code names the cause:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MQTT 5&lt;/th&gt;
&lt;th&gt;MQTT 3.1.1&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x87&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not authorized: an ACL or IoT policy denies &lt;code&gt;Connect&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x86&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bad username or password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x85&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client identifier not valid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These three cover most post-handshake refusals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Port matches the transport: 8883 for MQTT over TLS, 8084 for MQTT over WSS, 443 where ALPN does the routing&lt;/li&gt;
&lt;li&gt;Chain is complete and nothing in it has expired, intermediates included&lt;/li&gt;
&lt;li&gt;Private CA root is imported, and you verified its fingerprint before trusting it&lt;/li&gt;
&lt;li&gt;Connection hostname appears in the certificate's &lt;code&gt;SAN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALPN&lt;/code&gt; is set if the broker needs it, such as &lt;code&gt;x-amzn-mqtt-ca&lt;/code&gt; for AWS IoT on 443&lt;/li&gt;
&lt;li&gt;For mTLS: certificate, key and CA all present, passphrase correct&lt;/li&gt;
&lt;li&gt;Client key matches the certificate's public key&lt;/li&gt;
&lt;li&gt;Broker policy grants &lt;code&gt;Connect&lt;/code&gt; for this client ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Work down it in order and stop at the first item that fails. You can't test anything below it until you fix that one. The &lt;a href="https://mqttcommander.com/blog/mqtt-tls-mtls-debugging/" rel="noopener noreferrer"&gt;site version of this checklist&lt;/a&gt; sits next to a table of what each diagnostic stage looks like when it fails.&lt;/p&gt;




&lt;p&gt;On iOS I do this with &lt;a href="https://mqttcommander.com/blog/mqtt-tls-mtls-debugging/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=mqtt_tls"&gt;MQTT Commander&lt;/a&gt;. Its Connection Doctor runs the connection stage by stage, from DNS and TCP through the TLS handshake, chain, expiry, hostname and ALPN/SNI to CONNACK, authentication and a write test against the broker ACL, stops at the first stage that fails and says what to change. The free download shows the transport and MQTT stages plus whichever stage fails, TLS included; the one-time Pro unlock lists each TLS check on its own line and adds a shareable report that carries stage names and statuses only, with no hosts, topics or credentials. The Certificate Manager is free on every tier: it imports a CA as PEM and a client identity as &lt;code&gt;.p12&lt;/code&gt; or &lt;code&gt;.pfx&lt;/code&gt;, shows the &lt;code&gt;SHA-256&lt;/code&gt; fingerprint, and flags an expired certificate before you connect.&lt;/p&gt;

</description>
      <category>mqtt</category>
      <category>iot</category>
      <category>tls</category>
      <category>debugging</category>
    </item>
    <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>
