<?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: ssh412</title>
    <description>The latest articles on DEV Community by ssh412 (@shell412).</description>
    <link>https://dev.to/shell412</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%2F53862%2F6c6360f5-6f27-486e-a009-fdb6bb004f12.jpg</url>
      <title>DEV Community: ssh412</title>
      <link>https://dev.to/shell412</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shell412"/>
    <language>en</language>
    <item>
      <title>The ten-line binary search that balanced a four-million-item crawl</title>
      <dc:creator>ssh412</dc:creator>
      <pubDate>Mon, 27 Jul 2026 19:18:38 +0000</pubDate>
      <link>https://dev.to/shell412/the-ten-line-binary-search-that-balanced-a-four-million-item-crawl-321n</link>
      <guid>https://dev.to/shell412/the-ten-line-binary-search-that-balanced-a-four-million-item-crawl-321n</guid>
      <description>&lt;p&gt;&lt;em&gt;A short field note. I had to crawl every item in a public registry (~4 million of them) in about a day. One idea did most of the work.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Split the sorted keyspace into N ranges, run one worker per range, done. That's the plan. It falls apart because keys aren't uniformly distributed. Cut the alphabet into even chunks and one chunk swallows all the dense prefixes while the rest finish in minutes and sit idle. My first cut had a single shard holding &lt;strong&gt;71% of the work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And your crawl is only as fast as &lt;strong&gt;the slowest shard.&lt;/strong&gt; So that skew throws away almost all the parallelism: a 56-way fan-out that finishes no faster than a 3-way one. I didn't need equal alphabet width. I needed equal &lt;strong&gt;item count&lt;/strong&gt; per range.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick: your source already ranks keys
&lt;/h2&gt;

&lt;p&gt;So the whole thing reduces to one question: &lt;em&gt;which key sits at global position P in the sorted list?&lt;/em&gt; Answer that, and the balanced cut points are just the keys at ranks &lt;code&gt;total/N, 2·total/N, …&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Turns out the listing endpoint answers it for free. Ask for one row starting at any key, and it hands back that key's &lt;strong&gt;global offset&lt;/strong&gt;, its rank in the full sorted order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /list?startkey="&amp;lt;key&amp;gt;"&amp;amp;limit=1
→ { "offset": 3200000, "total_rows": 4000000, ... }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rank(key)&lt;/code&gt; is &lt;strong&gt;monotonic&lt;/strong&gt; in the sorted key. That's the whole trick. Monotonic means I can &lt;strong&gt;binary-search the keys themselves&lt;/strong&gt;, using &lt;code&gt;rank()&lt;/code&gt; as the compare, to run it backwards: give me a target rank, I'll find the key.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;key_at_offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetOffset&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&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="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\uffff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;               &lt;span class="c1"&gt;# full key range
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&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="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                 &lt;span class="c1"&gt;# ~40 iterations across millions of keys
&lt;/span&gt;        &lt;span class="n"&gt;mid&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;midpoint_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# a key lexicographically between lo and hi
&lt;/span&gt;        &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;offset_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# one cheap API call: the oracle
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;targetOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;

&lt;span class="n"&gt;boundaries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nf"&gt;key_at_offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;N&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step halves the key window, so ~40 calls land a boundary within an item or two. &lt;code&gt;N-1&lt;/code&gt; boundaries is a couple hundred calls total. I run it &lt;strong&gt;once, offline&lt;/strong&gt;, and bake the resulting table straight into the crawler.&lt;/p&gt;

&lt;p&gt;Here's what ranking buys you over guessing: even spacing on the &lt;strong&gt;rank&lt;/strong&gt; axis turns into &lt;em&gt;uneven&lt;/em&gt; spacing on the &lt;strong&gt;key&lt;/strong&gt; axis. Dense regions get boundaries packed tight, sparse regions get a few wide ones. Every slice is ~&lt;code&gt;1/N&lt;/code&gt; of the items, however wide it is in the alphabet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; rank axis  0 ──────────────────────────────────────────────► ~4M
 even cuts  │    │    │    │    │    │    │    │    │    │    │    (every ~1/N)
 keys       a0 a3 a6 a9   b    c    e   hp hp   m    r    s   w  z
            └─── dense ───┘         └ hot ┘          └── sparse: wide ──┘
            many cuts, packed       drilled          few cuts, wide ranges

 BEFORE  even alphabet, 2 shards      AFTER  offset-balanced, N shards
 one shard  ███████████████ 71%       every shard  █ ~1/N
 finish = the 71% shard               finish ≈ average  → ~1 day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;The fattest shard went from &lt;strong&gt;71% of the work to ≈ &lt;code&gt;1/N&lt;/code&gt;.&lt;/strong&gt; That's it. A few hundred offline API calls, and a one-week crawl becomes a one-day crawl.&lt;/p&gt;

&lt;p&gt;Everything else was plumbing (durable per-shard cursors so a dead worker resumes where it stopped, idempotent writes so retries cost nothing). Necessary, but not the part worth writing about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If your source hands you a rank or an offset for a key, you've got a binary-search oracle. Use it.&lt;/strong&gt; Build &lt;em&gt;balanced&lt;/em&gt; partitions, not &lt;em&gt;even-looking&lt;/em&gt; ones. Balanced sharding, "give me the item at percentile X", jump-to-position: same ten lines every time, because a monotonic &lt;code&gt;rank()&lt;/code&gt; is all bisection ever needs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Personal notes and my own views, not my employer's. This is a generic distributed-systems write-up, not a description of any specific product, service, or internal system. Names like Kubernetes are trademarks of their respective owners, used nominatively with no endorsement implied.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>kubernetes</category>
      <category>backend</category>
      <category>scaling</category>
    </item>
  </channel>
</rss>
