<?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>Two Modbus Bugs That Cancel Out: How 230.4 V Reads as 230.92 V</title>
      <dc:creator>Vladimir Chemeris</dc:creator>
      <pubDate>Fri, 30 Oct 2026 16:25:33 +0000</pubDate>
      <link>https://dev.to/chamav/two-modbus-bugs-that-cancel-out-how-2304-v-reads-as-23092-v-dlb</link>
      <guid>https://dev.to/chamav/two-modbus-bugs-that-cancel-out-how-2304-v-reads-as-23092-v-dlb</guid>
      <description>&lt;p&gt;I build &lt;a href="https://modbusapp.com/" rel="noopener noreferrer"&gt;Modbus TCP FieldKit&lt;/a&gt;, an iPhone and iPad app for Modbus TCP. You won't need it here: the scripts below run on a laptop with Python and pymodbus.&lt;/p&gt;

&lt;p&gt;Picture a meter manual with the line &lt;code&gt;Voltage L1: 40021, FLOAT32&lt;/code&gt; and no mention of word order. You drop the leading 4, pass 21 as the address, decode with your library's default and print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;volts: 230.92
hertz: 49.75
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The meter's display reads 230.4 V and 49.98 Hz. Half a volt looks like calibration drift, so you move on to the next register.&lt;/p&gt;

&lt;p&gt;That output carries two bugs. You would catch either one alone in seconds, because it gives you a number like &lt;code&gt;-3.2e+26&lt;/code&gt;. Together they give you a reading close enough to pass a glance at the display.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it
&lt;/h2&gt;

&lt;p&gt;I wrote a small simulated meter with a layout you will meet on real devices: low word first. Install pymodbus and run this in one terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# meter.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pymodbus.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpServer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pymodbus.simulator&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SimData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SimDevice&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cdab&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;high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;HH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;registers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;cdab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;230.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;cdab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;49.98&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SimDevice&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="n"&gt;simdata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;SimData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;registers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datatype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REGISTERS&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ModbusTcpServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15020&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then read it the way the manual suggests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# read_meter.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pymodbus.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15020&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;regs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_holding_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;registers&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert_from_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DATATYPE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FLOAT32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volts:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hertz:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran both on 30 October 2026 with pymodbus 3.16.1 and Python 3.15 on macOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two bugs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Off by one.&lt;/strong&gt; &lt;code&gt;40021&lt;/code&gt; names the 21st holding register, and the frame carries it as address &lt;code&gt;20&lt;/code&gt;. Section 4.4 of the &lt;a href="https://www.modbus.org/file/secure/modbusprotocolspecification.pdf" rel="noopener noreferrer"&gt;Modbus specification&lt;/a&gt; numbers data model elements from 1 and PDU addresses from 0. I wrote up the prefixes and 6-digit references in this &lt;a href="https://modbusapp.com/blog/modbus-register-addressing-40001/" rel="noopener noreferrer"&gt;guide to register addressing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Word order.&lt;/strong&gt; The specification defines 16-bit registers and leaves multi-register values to the vendor. My meter stores the low word first, and &lt;code&gt;convert_from_registers&lt;/code&gt; in pymodbus defaults to &lt;code&gt;word_order="big"&lt;/code&gt;, high word first. The &lt;a href="https://modbusapp.com/blog/modbus-float-word-order/" rel="noopener noreferrer"&gt;four word orders&lt;/a&gt; go by the names ABCD, CDAB, BADC and DCBA.&lt;/p&gt;

&lt;p&gt;Fixing both gives the right value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# fixed.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pymodbus.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15020&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;regs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_holding_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;registers&lt;/span&gt;
&lt;span class="n"&gt;volts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert_from_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DATATYPE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FLOAT32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;word_order&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;little&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volts:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;volts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;volts: 230.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The arithmetic behind the cancellation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;address     20    21    22    23
register  6666  4366  EB85  4247
          |  volts  |  |   Hz   |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IEEE 754 encodes 230.4 as &lt;code&gt;0x43666666&lt;/code&gt;. The meter writes the low word &lt;code&gt;6666&lt;/code&gt; first, then the high word &lt;code&gt;4366&lt;/code&gt;. A read from 21 grabs &lt;code&gt;4366 EB85&lt;/code&gt;. Decoding high word first puts the voltage's real high word in the right slot and fills the low half from the frequency.&lt;/p&gt;

&lt;p&gt;The high word of a float32 holds the sign, the exponent and the top seven mantissa bits. At this magnitude &lt;code&gt;0x4366&lt;/code&gt; pins the value between 230.0 and 231.0, and the low word picks a spot inside that volt. &lt;code&gt;0xEB85&lt;/code&gt; lands at .92.&lt;/p&gt;

&lt;p&gt;You can see how loud each bug is alone by decoding each window under all four orders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# grid.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pymodbus.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;

&lt;span class="n"&gt;ORDERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ABCD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CDAB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BADC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DCBA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;HH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ORDERS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;]))[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15020&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;
&lt;span class="n"&gt;regs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_holding_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;registers&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;addr  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ORDERS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;12.5&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ORDERS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;addr          ABCD        CDAB        BADC        DCBA
19      3.6734e-41  2.7154e+23  3.6734e-41  2.7154e+23
20      2.7185e+23       230.4  2.7201e+23  2.3069e+23
21          230.92 -3.2221e+26  2.3083e+23 -2.2137e-35
22      -3.222e+26       49.98 -2.2125e-35       49798
23           49.75  2.3776e-41       49664  2.5562e-41
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bug one alone sits at row 21 under CDAB: &lt;code&gt;-3.2221e+26&lt;/code&gt;. Bug two alone sits at row 20 under ABCD: &lt;code&gt;2.7185e+23&lt;/code&gt;. Both together give &lt;code&gt;230.92&lt;/code&gt; at row 21 under ABCD.&lt;/p&gt;

&lt;p&gt;Rows 22 and 23 repeat the pattern for frequency with &lt;code&gt;49.98&lt;/code&gt; and &lt;code&gt;49.75&lt;/code&gt;. You get two diagonals of sane-looking readings, so a range check can't choose between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell: decimals that follow the wrong quantity
&lt;/h2&gt;

&lt;p&gt;I rewrote the simulated readings from a second client and read them back with the buggy code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  true V  true Hz   read V  read Hz
  230.40    49.98   230.92    49.75
  231.70    49.98   231.92    49.75
  229.10    49.98   229.92    49.75
  230.40    50.01   230.04    50.00
  230.40    49.90   230.60    49.75
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The integer part of the voltage tracks the real voltage. The fraction stays at .92 while the voltage moves, then jumps to .04 and .60 after I change the frequency. The frequency reading snaps to steps of 0.25, because a zero register follows it in my map. On a real meter, the contents of the next register set that fraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catching it on a device
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read a window around the documented address and print the grid. Expect two plausible diagonals.&lt;/li&gt;
&lt;li&gt;Choose between them with a value whose decimals you can check: a display with one decimal place, or a setpoint you control. 230.92 and 230.4 differ in the first decimal.&lt;/li&gt;
&lt;li&gt;Change the quantity and read again. The fraction has to move with it.&lt;/li&gt;
&lt;li&gt;Store the address and word order next to the device model, and add a test with an exact expected value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a one-off check, paste two registers into this &lt;a href="https://modbusapp.com/tools/modbus-float-converter/" rel="noopener noreferrer"&gt;float converter&lt;/a&gt;, which decodes all four orders in the browser. The &lt;a href="https://modbusapp.com/tools/modbus-address-converter/" rel="noopener noreferrer"&gt;address converter&lt;/a&gt; does the 40021 arithmetic.&lt;/p&gt;

&lt;p&gt;FieldKit stores format, word order, scale and unit with each register in a saved device profile, so the next meter of that model starts from the right settings. If you do this next to a panel with a phone, the &lt;a href="https://modbusapp.com/modbus-tcp-client-ios/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=modbus_two_bugs"&gt;client page&lt;/a&gt; shows what it reads and writes.&lt;/p&gt;

&lt;p&gt;Have you hit a plausible but wrong reading on real hardware? Tell me which device in the comments.&lt;/p&gt;

</description>
      <category>modbus</category>
      <category>python</category>
      <category>iot</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Inside a 9.8 GB Xcode Simulator: Apps Took 350 MB</title>
      <dc:creator>Vladimir Chemeris</dc:creator>
      <pubDate>Tue, 27 Oct 2026 14:27:40 +0000</pubDate>
      <link>https://dev.to/chamav/inside-a-98-gb-xcode-simulator-apps-took-350-mb-22hk</link>
      <guid>https://dev.to/chamav/inside-a-98-gb-xcode-simulator-apps-took-350-mb-22hk</guid>
      <description>&lt;p&gt;I build StorageRadar, a disk space app for macOS. Everything below uses built-in commands, so you can repeat it without the app.&lt;/p&gt;

&lt;p&gt;Last week I measured &lt;a href="https://dev.to/chamav/where-51-gb-of-xcode-simulator-storage-went-on-my-mac-4a99"&gt;where 51 GB of simulator storage went on my Mac&lt;/a&gt;. One QA simulator held half of the Devices folder, and a reader asked what was inside it. Short answer: not my app.&lt;/p&gt;

&lt;h2&gt;
  
  
  One simulator, 9.8 GB
&lt;/h2&gt;

&lt;p&gt;The QA simulator is an iPhone 18 Pro on iOS 27.0. I measured it on 26 October 2026 while it was booted:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;du -k -d 4 ~/Library/Developer/CoreSimulator/Devices/&amp;lt;UDID&amp;gt;/data | sort -rn | head -20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fia4faz4hx1pxr1mlum7q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fia4faz4hx1pxr1mlum7q.png" alt="Bar chart of the QA simulator's 9.81 GB data folder. Containers with all apps take 0.34 GB, the smallest bar." width="800" height="668"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Folder inside &lt;code&gt;data&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Library/Application Support/PRBPosterExtensionDataStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.88 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;var/db/diagnostics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.07 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private/var/MobileAsset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.03 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Library/Caches/com.apple.coresymbolicationd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.13 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;var/db/uuidtext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.49 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Containers&lt;/code&gt;, all app containers&lt;/td&gt;
&lt;td&gt;0.34 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything my app and its tests wrote fits in &lt;code&gt;Containers&lt;/code&gt;: 0.34 GB of 9.81.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten simulators side by side
&lt;/h2&gt;

&lt;p&gt;I have 53 simulators. Forty-three hold about 20 MB each, the size of a device that never ran an app. The other ten:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffe3anxhmfx4g10nq11nm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffe3anxhmfx4g10nq11nm.png" alt="Stacked bar chart of ten simulators' data folders, 0.63 to 9.81 GB. App containers take 0.07 to 0.34 GB on each." width="800" height="918"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App containers stayed between 0.07 and 0.34 GB on every device.&lt;/li&gt;
&lt;li&gt;MobileAsset showed up everywhere, from 0.17 to 2.03 GB.&lt;/li&gt;
&lt;li&gt;The poster store was big only on the two booted iOS 27.0 devices: 2.88 and 1.65 GB. On iOS 18.6 and 26.5 it stayed under 0.05 GB. Two devices prove nothing about iOS 27.0, but the gap is wide.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the folders are
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PRBPosterExtensionDataStore.&lt;/strong&gt; The name, along with &lt;code&gt;GalleryCache&lt;/code&gt; and &lt;code&gt;SnapshotCache.cachedb&lt;/code&gt;, is a string in the private &lt;code&gt;PosterBoard.framework&lt;/code&gt; of the iOS 27.0 runtime. Its subfolders carry the IDs of Apple's poster extensions: &lt;code&gt;WeatherPoster&lt;/code&gt;, &lt;code&gt;EmojiPoster&lt;/code&gt;, &lt;code&gt;PhotosPosterProvider&lt;/code&gt; and others. Weather and photos are among the &lt;a href="https://support.apple.com/guide/iphone/create-a-custom-iphone-lock-screen-iph4d0e6c351/ios" rel="noopener noreferrer"&gt;Lock Screen wallpapers&lt;/a&gt; Apple lists. On my QA device, 2,911 &lt;code&gt;.atx&lt;/code&gt; snapshot files of about 1 MB each made up 2.9 GB. Apple doesn't document the folder, and I don't know what makes it grow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;var/db/diagnostics and var/db/uuidtext.&lt;/strong&gt; The simulator's own copy of the unified log. &lt;code&gt;man log&lt;/code&gt; describes a log store as a database like the one in &lt;code&gt;/var/db/diagnostics&lt;/code&gt;, with &lt;code&gt;uuidtext&lt;/code&gt; next to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;private/var/MobileAsset.&lt;/strong&gt; Asset sets the simulator downloads itself. On the QA device the biggest were named after Siri Understanding (805 MB), linguistic data (409 MB) and Siri text-to-speech (289 MB). Someone on the Apple forums &lt;a href="https://developer.apple.com/forums/thread/836263" rel="noopener noreferrer"&gt;reported&lt;/a&gt; this folder growing by tens of gigabytes on iOS 26.5. No answer there yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;com.apple.coresymbolicationd.&lt;/strong&gt; &lt;code&gt;man coresymbolicationd&lt;/code&gt; calls it a "daemon to manage debug symbols cache". 1.13 GB on the QA device, close to zero elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do about it
&lt;/h2&gt;

&lt;p&gt;I don't delete files inside these folders. None of them is documented, so I can't predict what a half-deleted log store or poster store does to a simulator. I work on whole devices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;xcrun simctl erase &amp;lt;UDID&amp;gt;&lt;/code&gt; wipes contents and settings and keeps the device.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xcrun simctl delete &amp;lt;UDID&amp;gt;&lt;/code&gt; removes the device.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xcrun simctl delete unavailable&lt;/code&gt; removes devices the current Xcode SDK no longer supports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An erase drops your installed apps and logins along with the system data, and the caches can come back once the device runs again. It buys space; it doesn't stop the growth.&lt;/p&gt;

&lt;p&gt;The full version with every number, the measurement method and an FAQ is on &lt;a href="https://storageradar.app/blog/inside-xcode-simulator-device-data/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;StorageRadar's Disk Map drills into folders like these, and its Dev Cleanup lists CoreSimulator under Review before cleaning, next to DerivedData and package caches: &lt;a href="https://storageradar.app/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=sr_simulator_data"&gt;storageradar.app&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>xcode</category>
      <category>ios</category>
      <category>macos</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Where 51 GB of Xcode simulator storage went on my Mac</title>
      <dc:creator>Vladimir Chemeris</dc:creator>
      <pubDate>Sun, 25 Oct 2026 06:23:39 +0000</pubDate>
      <link>https://dev.to/chamav/where-51-gb-of-xcode-simulator-storage-went-on-my-mac-4a99</link>
      <guid>https://dev.to/chamav/where-51-gb-of-xcode-simulator-storage-went-on-my-mac-4a99</guid>
      <description>&lt;p&gt;I build StorageRadar, a disk space app for macOS. Every step below uses built-in commands, so you can follow along without it.&lt;/p&gt;

&lt;p&gt;My Mac runs macOS 27.0.1 and Xcode 27.1, and I test several iOS apps on it. I measured what the simulators take on 25 October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two places, two owners
&lt;/h2&gt;

&lt;p&gt;Simulator storage lives in two places, and different tools manage them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;On my Mac&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtimes, one per iOS version&lt;/td&gt;
&lt;td&gt;system-managed disk images&lt;/td&gt;
&lt;td&gt;31.3 GB for 4 runtimes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device data, one folder per simulator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/Library/Developer/CoreSimulator/Devices&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20 GB for 53 simulators&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A runtime is the iOS system image. Device data is what each simulated iPhone or iPad keeps: installed apps, their containers, logs and settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtimes: about 8 GB each
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;xcrun simctl runtime list&lt;/code&gt; prints every runtime and the total:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ xcrun simctl runtime list
== Disk Images ==
-- iOS --
iOS 27.1 (24A94232) - … (Ready)
iOS 27.0 (24A434) - … (Ready)
iOS 26.5 (23F77) - … (Ready)
iOS 18.6 (22G86) - … (Ready)

Total Disk Images: 4 (31.0G)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Add &lt;code&gt;-j&lt;/code&gt; and you get each runtime's size and the last time it ran. Mine range from 7.87 GB (iOS 27.1) to 8.84 GB (iOS 18.6).&lt;/p&gt;

&lt;p&gt;Remove runtimes with simctl, not Finder. The images sit in protected system storage, and simctl shuts down booted simulators and unmounts the image first. Start with a dry run:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xcrun simctl runtime delete --notUsedSinceDays 30 --dry-run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You can also pass &lt;code&gt;--unusable&lt;/code&gt;, &lt;code&gt;--outdated&lt;/code&gt;, or one identifier from the list. Xcode's Settings window lists the same runtimes.&lt;/p&gt;

&lt;p&gt;All four of mine ran in the last day, so I keep them. If you dropped an iOS version from your deployment targets, its runtime is the biggest single thing you can remove.&lt;/p&gt;

&lt;h2&gt;
  
  
  The du number that misleads
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;du -sh /Library/Developer/CoreSimulator/Volumes&lt;/code&gt; reported 51 GB on my Mac. That folder holds mount points:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mount | grep CoreSimulator/Volumes
/dev/disk5s1 on /Library/Developer/CoreSimulator/Volumes/iOS_23F77 …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;du walks into the mounted images and counts the files inside them. The images take 31 GB on disk, and 51 GB is the size of their contents. Leave Volumes alone and use &lt;code&gt;simctl runtime delete&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Device data: one simulator held half
&lt;/h2&gt;

&lt;p&gt;The Devices folder took 20 GB, and the split surprised me:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;du -sh ~/Library/Developer/CoreSimulator/Devices/* | sort -h | tail -5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;one QA simulator: 9.6 GB&lt;/li&gt;
&lt;li&gt;the next four: 3.3, 2.5, 2.1 and 1.5 GB&lt;/li&gt;
&lt;li&gt;47 of 53 simulators: under 200 MB each&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The folder names are UDIDs. &lt;code&gt;xcrun simctl list devices&lt;/code&gt; maps them to names.&lt;/p&gt;

&lt;p&gt;Two commands handle the heavy ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;xcrun simctl erase &amp;lt;UDID&amp;gt;&lt;/code&gt; wipes the simulator's apps and data and keeps the device.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xcrun simctl delete &amp;lt;UDID&amp;gt;&lt;/code&gt; removes the device.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;xcrun simctl delete unavailable&lt;/code&gt; removes simulators your current Xcode can no longer run. I had none, but the list grows after Xcode upgrades and runtime deletions. Erasing a simulator leaves its runtime in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of ~/Library/Developer
&lt;/h2&gt;

&lt;p&gt;Two more folders sat next to the simulators:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DerivedData, 6.2 GB. Xcode rebuilds it, so you can delete it and pay with one slower build.&lt;/li&gt;
&lt;li&gt;Archives, 3.9 GB. Keep the archives of builds you shipped: they hold the dSYMs that symbolicate crash reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My checklist now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;xcrun simctl runtime list&lt;/code&gt; for runtimes I no longer test on.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;du&lt;/code&gt; on Devices to find the one or two heavy simulators, then erase them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xcrun simctl delete unavailable&lt;/code&gt; after each Xcode update.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I keep a longer guide with every path and what rebuilds it: &lt;a href="https://storageradar.app/blog/xcode-simulator-taking-up-space-on-mac/" rel="noopener noreferrer"&gt;Xcode Simulator taking up space on Mac&lt;/a&gt;. To test yourself on ten developer folders, from DerivedData to Docker data, try &lt;a href="https://storageradar.app/safe-to-delete/" rel="noopener noreferrer"&gt;this quiz&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;StorageRadar's Dev Cleanup lists &lt;code&gt;~/Library/Developer/CoreSimulator&lt;/code&gt; with a Caution label and DerivedData as Safe, next to npm, Docker and Homebrew caches, and moves files to the Trash after you confirm: &lt;a href="https://storageradar.app/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=sr_simulators"&gt;storageradar.app&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>xcode</category>
      <category>ios</category>
      <category>macos</category>
      <category>devtools</category>
    </item>
    <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>
