<?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: Daksh Gargas</title>
    <description>The latest articles on DEV Community by Daksh Gargas (@daksh-gargas).</description>
    <link>https://dev.to/daksh-gargas</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%2F81541%2F8e86e0e4-b356-4178-9fd3-9bde113d855e.jpg</url>
      <title>DEV Community: Daksh Gargas</title>
      <link>https://dev.to/daksh-gargas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daksh-gargas"/>
    <language>en</language>
    <item>
      <title>The 4% rule: picking app background colors that survive cheap phone screens</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Sun, 02 Aug 2026 20:55:42 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/the-4-rule-picking-app-background-colors-that-survive-cheap-phone-screens-346k</link>
      <guid>https://dev.to/daksh-gargas/the-4-rule-picking-app-background-colors-that-survive-cheap-phone-screens-346k</guid>
      <description>&lt;p&gt;Every design team eventually ships a beautiful off-white, off-blue, or off-anything background… and then opens the app on a $120 phone and watches it turn &lt;strong&gt;dirty gray&lt;/strong&gt;. Same hex, same build. This post explains why, and gives you a small formula to convert &lt;em&gt;any&lt;/em&gt; tint you've chosen into one that survives budget panels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why subtle tints die on cheap screens
&lt;/h2&gt;

&lt;p&gt;Four panel-level failure modes, all common in the budget tier:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Weak gamut coverage.&lt;/strong&gt; Entry-level LCDs cover only a fraction of sRGB — independent panel measurements routinely land in the 55–70% range, with large per-color error. A low-chroma tint simply doesn't have the budget to survive that compression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cold white points.&lt;/strong&gt; sRGB assumes a D65 white (6500K). Budget modules commonly ship visibly cooler — high-6000s to 9000K+ — because blue-ish whites look "brighter" in a store. That blue cast is spread across the &lt;em&gt;entire&lt;/em&gt; grayscale, and its magnitude is comparable to a subtle warm tint. Net result: the panel can cancel your background color outright.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Stretched gamuts on budget AMOLED.&lt;/strong&gt; The opposite failure: "vivid" default modes stretch sRGB content across the panel's wider native gamut. Your quiet tint renders at roughly double saturation and suddenly has an opinion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Banding.&lt;/strong&gt; Many cheap panels are 6-bit + FRC. Soft near-white gradients develop visible steps, which makes barely-different surface colors look like rendering bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4% rule
&lt;/h2&gt;

&lt;p&gt;You don't need a colorimeter to know if you're at risk. Use &lt;strong&gt;channel spread&lt;/strong&gt; — the distance between your highest and lowest RGB channel — as a chroma proxy:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;spread = max(R, G, B) − min(R, G, B)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If spread is &lt;strong&gt;under ~10 of 255 (≈4%)&lt;/strong&gt;, your tint is inside a cheap panel's error bar. It may render as intended, as gray, or as tinted the other direction — you don't get a vote. (Quick check on any hex: two outer pairs of digits within ~0x0A of each other = you're in the danger zone.)&lt;/p&gt;

&lt;p&gt;Why 4%? Because that's the same order of magnitude as the grayscale tint produced by a few-hundred-kelvin white-point error — the most common budget-panel defect. Your color and the panel's error are the same size, so the panel wins half the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting your color: the budget-tier formula
&lt;/h2&gt;

&lt;p&gt;Don't pick a &lt;em&gt;different&lt;/em&gt; color for cheap devices — that forks your brand. Derive the &lt;strong&gt;same hue at higher chroma&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take your canvas color. Find &lt;em&gt;maxC&lt;/em&gt; (highest channel), and each channel's distance from it: &lt;em&gt;d = maxC − channel&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Pick a spread multiplier &lt;em&gt;k&lt;/em&gt; — 2 if your spread is ≤4%, 1.2–1.5 if you're already near 5%.&lt;/li&gt;
&lt;li&gt;Drop the ceiling a touch so cards can sit above the canvas in pure white: &lt;em&gt;maxNew = maxC − 4&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Each new channel = &lt;em&gt;maxNew − d × k&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Worked example&lt;/strong&gt; — say your designer chose a cool "paper blue" &lt;code&gt;#F2F5FA&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Channels 242 / 245 / 250 → spread 8 (3.1%): danger zone.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;k&lt;/em&gt; = 2, &lt;em&gt;maxNew&lt;/em&gt; = 246.&lt;/li&gt;
&lt;li&gt;R = 246 − (8×2) = 230 → &lt;code&gt;E6&lt;/code&gt; · G = 246 − (5×2) = 236 → &lt;code&gt;EC&lt;/code&gt; · B = 246 → &lt;code&gt;F6&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget variant: &lt;code&gt;#E6ECF6&lt;/code&gt;&lt;/strong&gt; — spread 16 (6.3%), same hue, twice the presence, and pure &lt;code&gt;#FFFFFF&lt;/code&gt; cards now sit a clear step above it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same math works on warm creams, mint-greens, blush-pinks — hue never changes, only its insistence. If you prefer perceptual tooling: convert to &lt;strong&gt;OKLCH&lt;/strong&gt;, multiply C by ~2, subtract ~0.01 from L, keep H — same result with cleaner math. (Avoid HSL for this: its S value is wildly misleading near white.) Either way, your background lightness barely moves, so text contrast ratios are essentially unchanged — but re-run your AA checks anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship it as tiers, not themes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — color-managed panels&lt;/strong&gt; (iPhones, flagship Androids): your original tint, near-white cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — budget panels&lt;/strong&gt;: the converted tint, pure-white cards, hairlines nudged one step darker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else — ink, accent, type — stays identical. Users never see "a different theme"; they see the same brand, legible. Platform low-RAM/entry-device flags are a good-enough proxy for panel class to pick a default, and a small &lt;em&gt;Paper tone: Auto · Soft · Rich&lt;/em&gt; setting covers the misfires (and the users who simply prefer one).&lt;/p&gt;

&lt;h2&gt;
  
  
  Test on glass, not in Figma
&lt;/h2&gt;

&lt;p&gt;Emulators show you your monitor's opinion. On the real device, at full brightness &lt;em&gt;and&lt;/em&gt; ~40%:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the canvas read as &lt;strong&gt;a color&lt;/strong&gt; — or as a white that needs cleaning?&lt;/li&gt;
&lt;li&gt;Can you see where a card ends &lt;strong&gt;without tilting the phone&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt;Is a canvas→white gradient &lt;strong&gt;smooth&lt;/strong&gt;, no steps?&lt;/li&gt;
&lt;li&gt;Is your content (photos especially) still the warmest, richest thing on screen?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Four yeses on your cheapest test device and you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Subtle off-whites are a flagship luxury; under ~4% channel spread you're inside the error bar of a cheap panel.&lt;/li&gt;
&lt;li&gt;Never fork the color — scale its chroma: &lt;em&gt;maxNew − d × k&lt;/em&gt;, hue untouched.&lt;/li&gt;
&lt;li&gt;Pure-white cards above a slightly stronger canvas beat shadow-only hierarchy on washed panels.&lt;/li&gt;
&lt;li&gt;Tier by device class with a user override; test with eyes on real glass.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Panel measurement &amp;amp; gamut coverage&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.notebookcheck.net/How-does-Notebookcheck-test-laptops-and-smartphones-A-behind-the-scenes-look-into-our-review-process.15394.0.html" rel="noopener noreferrer"&gt;How Notebookcheck tests laptops and smartphones&lt;/a&gt; — the measurement methodology behind most published sRGB-coverage and DeltaE numbers; a good primer on what "coverage" and "ΔE &amp;lt; 3" actually mean.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.notebookcheck.net/Lenovo-IdeaPad-330S-15IKB-i5-8250U-UHD620-Laptop-Review.332226.0.html" rel="noopener noreferrer"&gt;Lenovo IdeaPad 330S display review&lt;/a&gt; — a real measured example of a budget panel at ~58% sRGB (reds render orange; subtle tints don't stand a chance).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://screentester.io/srgb-test/" rel="noopener noreferrer"&gt;sRGB coverage explained + NTSC conversion&lt;/a&gt; — why "45% NTSC" marketing ≈ 63% sRGB, typical coverage tiers by panel class, and why wide-gamut panels oversaturate sRGB content in native mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;White points&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.xrite.com/service-support/monitor_calibration__which_white_point_is_correct" rel="noopener noreferrer"&gt;X-Rite: which white point is correct?&lt;/a&gt; — D65/6500K as the reference illuminant.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.barco.com/en/inspiration/news-insights/2019-06-19-what-is-a-displays-white-point" rel="noopener noreferrer"&gt;Barco: what is a display's white point?&lt;/a&gt; — native panel hue, and sRGB's 6500K default.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.phonearena.com/howdowerate/display" rel="noopener noreferrer"&gt;PhoneArena: how we test displays&lt;/a&gt; — phone color temperature measured against the 6500K reference; higher = colder/bluer whites.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://us.ktcplay.com/blogs/technology-hub/6500k-vs-5000k-monitor-color-temperature" rel="noopener noreferrer"&gt;6500K vs 5000K in practice&lt;/a&gt; — includes real-world reports of uncalibrated displays landing in the low-to-mid 7000K range.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bit depth &amp;amp; banding&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://evezone.evetech.co.za/deep-dives/8-bit-vs-6-bit-frc-monitors" rel="noopener noreferrer"&gt;8-bit vs 6-bit+FRC panels&lt;/a&gt; — why cheap panels band in soft gradients (262k native colors, dithering artifacts).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Color math &amp;amp; platform APIs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://bottosson.github.io/posts/oklab/" rel="noopener noreferrer"&gt;Björn Ottosson: Oklab/OKLCH&lt;/a&gt; — the perceptual color space behind the "C × 2, L − 0.01, H constant" conversion; interactive picker at &lt;a href="https://oklch.com" rel="noopener noreferrer"&gt;oklch.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/reference/android/app/ActivityManager#isLowRamDevice()" rel="noopener noreferrer"&gt;Android &lt;code&gt;isLowRamDevice&lt;/code&gt;&lt;/a&gt; — the platform flag that best proxies device (and panel) class.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Most measurement sources above are monitor/laptop reviews — the panel physics is identical, and phone-specific measurement lives in the PhoneArena and Notebookcheck methodologies.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>ux</category>
      <category>design</category>
      <category>color</category>
    </item>
    <item>
      <title>One Brain, Two Wallets: Two Claude Code Accounts on One Machine</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Mon, 20 Jul 2026 18:56:18 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/one-brain-two-wallets-two-claude-code-accounts-on-one-machine-5ejb</link>
      <guid>https://dev.to/daksh-gargas/one-brain-two-wallets-two-claude-code-accounts-on-one-machine-5ejb</guid>
      <description>&lt;p&gt;I hit my Claude Code usage limit mid-week. I have a second subscription, but switching accounts the naive way — &lt;code&gt;/logout&lt;/code&gt;, &lt;code&gt;/login&lt;/code&gt;, repeat — is miserable, and it turns out "switching accounts" in Claude Code silently means switching &lt;em&gt;everything&lt;/em&gt;: skills, MCP servers, settings, hooks, memory, session history. All of it.&lt;/p&gt;

&lt;p&gt;What I actually wanted: &lt;strong&gt;one brain, two wallets.&lt;/strong&gt; Same skills, same MCP servers, same muscle memory — just a different subscription getting billed.&lt;/p&gt;

&lt;p&gt;Here's the setup that got me there. ~20 minutes, fully reversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Two config dirs, two aliases
&lt;/h2&gt;

&lt;p&gt;Claude Code keeps all its state in a config directory — &lt;code&gt;~/.claude&lt;/code&gt; by default — and honors a &lt;code&gt;CLAUDE_CONFIG_DIR&lt;/code&gt; env var to point elsewhere. That's the whole trick:&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;# ~/.zshrc&lt;/span&gt;
&lt;span class="c"&gt;# Claude Code multi-account aliases&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;claude-work&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'CLAUDE_CONFIG_DIR=~/.claude claude'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;claude-personal&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'CLAUDE_CONFIG_DIR=~/.claude-personal claude'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; ~/.claude-personal
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
claude-personal   &lt;span class="c"&gt;# prompts OAuth login for the second account — one time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Credentials land in whichever dir is active. Bare &lt;code&gt;claude&lt;/code&gt; still defaults to &lt;code&gt;~/.claude&lt;/code&gt;, so nothing about your existing workflow changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #1: &lt;code&gt;.claude.json&lt;/code&gt; moves when you set the env var
&lt;/h2&gt;

&lt;p&gt;Claude Code's user-level config file (&lt;code&gt;~/.claude.json&lt;/code&gt; — MCP servers, folder trust, onboarding state) lives &lt;em&gt;next to&lt;/em&gt; &lt;code&gt;~/.claude&lt;/code&gt;, not inside it. But when &lt;code&gt;CLAUDE_CONFIG_DIR&lt;/code&gt; is set, it's read from &lt;em&gt;inside&lt;/em&gt; the config dir instead.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;claude-work&lt;/code&gt; — even though it points at the same &lt;code&gt;~/.claude&lt;/code&gt; you've always used — creates a fresh, empty &lt;code&gt;~/.claude/.claude.json&lt;/code&gt; and suddenly has zero MCP servers. Fix: symlink it to the master, since it's the same account:&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;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; ~/.claude.json ~/.claude/.claude.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now bare &lt;code&gt;claude&lt;/code&gt; and &lt;code&gt;claude-work&lt;/code&gt; are byte-for-byte identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Share the brain
&lt;/h2&gt;

&lt;p&gt;The second account starts as a blank slate. Since only billing should differ, symlink everything shareable from the main dir:&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;cd&lt;/span&gt; ~/.claude-personal
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; ~/.claude/skills skills
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; ~/.claude/CLAUDE.md CLAUDE.md
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; ~/.claude/settings.json settings.json        &lt;span class="c"&gt;# hooks, permissions, env&lt;/span&gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; ~/.claude/settings.local.json settings.local.json
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; ~/.claude/plugins plugins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One source of truth: add a skill or tweak a hook once, both accounts see it.&lt;/p&gt;

&lt;p&gt;MCP servers need more care. They live in &lt;code&gt;.claude.json&lt;/code&gt;, which &lt;em&gt;also&lt;/em&gt; holds the account identity (&lt;code&gt;oauthAccount&lt;/code&gt;) — so you can't symlink the whole file across accounts. Merge just the &lt;code&gt;mcpServers&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;--slurpfile&lt;/span&gt; mcp &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;jq &lt;span class="s1"&gt;'.mcpServers'&lt;/span&gt; ~/.claude.json&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="s1"&gt;'.mcpServers = $mcp[0]'&lt;/span&gt; ~/.claude-personal/.claude.json &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/merged.json &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/merged.json ~/.claude-personal/.claude.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotcha #2: &lt;code&gt;--resume&lt;/code&gt; doesn't know about accounts
&lt;/h2&gt;

&lt;p&gt;Session transcripts live at &lt;code&gt;&amp;lt;config-dir&amp;gt;/projects/&amp;lt;project&amp;gt;/&amp;lt;session-id&amp;gt;.jsonl&lt;/code&gt;. Resume a personal session under the work account and Claude just says the session doesn't exist. Worse, anything that launches bare &lt;code&gt;claude&lt;/code&gt; for you — crash-restore scripts, launchd jobs — always resumes against the default dir.&lt;/p&gt;

&lt;p&gt;The fix is a tiny shim earlier in &lt;code&gt;$PATH&lt;/code&gt; than the real binary. Every session ID exists in exactly one account's tree, so ownership &lt;em&gt;is&lt;/em&gt; the routing signal:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# ~/.local/bin/claude — routes --resume to the account that owns the session&lt;/span&gt;
&lt;span class="nv"&gt;REAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/homebrew/bin/claude   &lt;span class="c"&gt;# wherever `command -v claude` pointed before&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE_CONFIG_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="nv"&gt;prev&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;arg &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$prev&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"--resume"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$prev&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"-r"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$arg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
    if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$arg&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nt"&gt;--resume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;#--resume=&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
    &lt;/span&gt;&lt;span class="nv"&gt;prev&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$arg&lt;/span&gt;
  &lt;span class="k"&gt;done
  if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;compgen&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude-personal/projects/*/&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;.jsonl"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_CONFIG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude-personal"&lt;/span&gt;
  &lt;span class="k"&gt;fi
fi
&lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REAL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;claude --resume &amp;lt;id&amp;gt;&lt;/code&gt; just works, from anywhere, for either account. Explicit &lt;code&gt;CLAUDE_CONFIG_DIR&lt;/code&gt; (your aliases) passes through untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: moving a conversation between wallets
&lt;/h2&gt;

&lt;p&gt;The whole reason I did this was running out of credits &lt;em&gt;mid-conversation&lt;/em&gt;. Since a session is just a transcript file, "continue this work conversation on my personal account" is a copy + resume:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# ~/.local/bin/claude-pickup &amp;lt;session-id&amp;gt; — continue a work session as personal&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?usage:&lt;span class="p"&gt; claude-pickup &amp;lt;session-id&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; ~/.claude/projects/&lt;span class="k"&gt;*&lt;/span&gt;/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;.jsonl 2&amp;gt;/dev/null | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$src&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"session &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt; not found"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;proj&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.claude-personal/projects/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$proj&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ~/.claude-personal/projects/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$proj&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/
&lt;span class="nv"&gt;CLAUDE_CONFIG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.claude-personal &lt;span class="nb"&gt;exec &lt;/span&gt;claude &lt;span class="nt"&gt;--resume&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The work account keeps its copy as history; new turns land only in the personal copy. And since the shim checks the personal tree first, future bare &lt;code&gt;claude --resume&lt;/code&gt; of that ID follows the conversation to its new wallet automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays separate (and should)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credentials and billing&lt;/strong&gt; — the entire point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session transcripts&lt;/strong&gt; — the shim depends on separate trees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Folder trust&lt;/strong&gt; — each account re-asks "do you trust this folder?" once per directory. Expected, answer once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth'd MCP servers&lt;/strong&gt; (Linear, Notion, …) — tokens are per config dir; run &lt;code&gt;/mcp&lt;/code&gt; once in the new account to re-auth. CLI-based MCP servers that use machine credentials work immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude              &lt;span class="c"&gt;# work (default, unchanged)&lt;/span&gt;
claude-personal     &lt;span class="c"&gt;# second subscription, same brain&lt;/span&gt;
claude &lt;span class="nt"&gt;--resume&lt;/span&gt; X   &lt;span class="c"&gt;# figures out the account itself&lt;/span&gt;
claude-pickup X     &lt;span class="c"&gt;# mid-conversation wallet swap&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same skills, same MCP servers, same hooks, same memory. The only thing that changes is who gets the bill.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>cli</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why Redis Splits Into Exactly 16,384 Slots: A Deep Dive Into Distributed Systems Design</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Sat, 18 Jul 2026 20:22:13 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/why-redis-splits-into-exactly-16384-slots-a-deep-dive-into-distributed-systems-design-4bgh</link>
      <guid>https://dev.to/daksh-gargas/why-redis-splits-into-exactly-16384-slots-a-deep-dive-into-distributed-systems-design-4bgh</guid>
      <description>&lt;p&gt;If you have ever spun up a Redis Cluster, you probably ran into a strange, highly specific hardcoded number: &lt;strong&gt;16,384&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;No matter if you configure a tiny cluster of 3 nodes or a massive enterprise deployment of 500 nodes, the total number of Redis hash slots remains exactly the same. &lt;/p&gt;

&lt;p&gt;Why 16,384? Why not 65,536 (2¹⁶), which is the standard boundary for almost every other distributed database, network protocol, and sharding architecture on earth?&lt;/p&gt;

&lt;p&gt;To understand why Salvatore Sanfilippo (antirez), the creator of Redis, chose this specific number, we have to look past the surface-level documentation. We have to map out a technical journey from a single server to a decentralized mesh network, looking directly at the math, the memory structs, and the harsh realities of network physics.&lt;/p&gt;




&lt;h2&gt;
  
  
  Act 1: The Humble Beginning (The Single Instance)
&lt;/h2&gt;

&lt;p&gt;Imagine you are building a new application. You need a fast database, so you spin up a single instance of Redis on a single cloud server. &lt;/p&gt;

&lt;p&gt;At this stage, life is simple. To your application, Redis is a giant, super-fast dictionary in memory. You connect to that one server, and you start saving data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Your App]  ----&amp;gt;  ( Redis Server: 10.0.0.1 )
                      └── "user:101" -&amp;gt; { name: "Alice" }
                      └── "user:102" -&amp;gt; { name: "Bob" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time you write a key-value pair, it lands in the exact same memory pool. When you ask for &lt;code&gt;"user:101"&lt;/code&gt;, Redis looks inside its internal memory table, grabs the value, and hands it right back.&lt;/p&gt;




&lt;h2&gt;
  
  
  Act 2: The Success Wall (When One Server Isn't Enough)
&lt;/h2&gt;

&lt;p&gt;Your application goes viral. Suddenly, you aren't storing thousands of keys; you are storing &lt;strong&gt;hundreds of millions&lt;/strong&gt; of keys. &lt;/p&gt;

&lt;p&gt;You hit a physical wall. Your Redis server runs out of RAM. You upgrade the server to a bigger instance (Vertical Scaling), but eventually, you hit the ultimate ceiling: you cannot buy a single machine with enough RAM or CPU power to handle your traffic. &lt;/p&gt;

&lt;p&gt;You have to &lt;strong&gt;Scale Horizontally&lt;/strong&gt;. You need to split your massive dictionary across multiple independent Redis servers (Nodes).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               ┌──&amp;gt; [ Node 1 (10.0.0.1) ] -&amp;gt; Stores keys A to I
[Your App] ────┼──&amp;gt; [ Node 2 (10.0.0.2) ] -&amp;gt; Stores keys J to R
               └──&amp;gt; [ Node 3 (10.0.0.3) ] -&amp;gt; Stores keys S to Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where things get messy. If you have 3 different servers, &lt;strong&gt;how does your app know which server holds &lt;code&gt;"user:101"&lt;/code&gt;?&lt;/strong&gt; If you guess wrong, you waste time jumping from server to server. We need a predictable way to evenly distribute keys across any number of servers. &lt;/p&gt;




&lt;h2&gt;
  
  
  Act 3: The Naive Solution (Modulo Hashing)
&lt;/h2&gt;

&lt;p&gt;To solve the distribution problem with technical precision, your first instinct might be to use traditional &lt;strong&gt;Modulo Hashing&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;When your application wants to store a key, it passes the string through a hashing algorithm to turn it into a consistent, predictable integer. Then, you use the modulo operator (&lt;code&gt;%&lt;/code&gt;) against the total number of Redis nodes you currently own ($N$).&lt;/p&gt;

&lt;p&gt;$$\text{Node Index} = \text{Hash}(\text{key}) \pmod N$$&lt;/p&gt;

&lt;p&gt;If you have $N = 3$ nodes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$\text{Hash}(\text{"user:101"}) = 125,482 \implies 125,482 \pmod 3 = 1 \longrightarrow \textbf{Node 1}$&lt;/li&gt;
&lt;li&gt;$\text{Hash}(\text{"user:102"}) = 984,311 \implies 984,311 \pmod 3 = 2 \longrightarrow \textbf{Node 2}$&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Fatal Flaw: The Resharding Catastrophe
&lt;/h3&gt;

&lt;p&gt;This math works flawlessly—until your traffic doubles again, and you need to add a 4th node ($N = 4$). &lt;/p&gt;

&lt;p&gt;Suddenly, the denominator in your modulo operation changes from 3 to 4. Let's recalculate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$\text{Hash}(\text{"user:101"}) = 125,482 \implies 125,482 \pmod 4 = 2 \longrightarrow \textbf{Node 2}$ &lt;em&gt;(Was Node 1)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;$\text{Hash}(\text{"user:102"}) = 984,311 \implies 984,311 \pmod 4 = 3 \longrightarrow \textbf{Node 3}$ &lt;em&gt;(Was Node 2)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By changing $N$ from 3 to 4, &lt;strong&gt;up to 75% of your existing keys instantly map to the wrong node.&lt;/strong&gt; Your application will suffer a massive cache miss storm. To fix this, you would have to take the database offline and move terabytes of data around just to align them with the new math. This is a production nightmare.&lt;/p&gt;




&lt;h2&gt;
  
  
  Act 4: Abstracting the Infrastructure (The Invention of Hash Slots)
&lt;/h2&gt;

&lt;p&gt;Redis avoids this catastrophic remapping by decoupling your keys from the physical servers. It introduces a mathematical abstraction layer called &lt;strong&gt;Hash Slots&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of hashing a key directly to a node, Redis hashes every key into a fixed pool of exactly &lt;strong&gt;16,384 logical slots&lt;/strong&gt;. This mapping &lt;em&gt;never&lt;/em&gt; changes, no matter how many physical servers you add or remove.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Any Key ---&amp;gt; CRC16(Key) % 16384 ---&amp;gt; Hash Slot (0 to 16383)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The physical nodes do not own keys anymore; &lt;strong&gt;they own blocks of slots&lt;/strong&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  How the Fixed Pie is Sliced
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Scenario A: The 4-Node Cluster
&lt;/h4&gt;

&lt;p&gt;If you deploy a cluster with 4 master nodes, Redis divides the 16,384 slots evenly among them. Each node gets a massive chunk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node 1:&lt;/strong&gt; Owns Slots 0 to 4,095 &lt;em&gt;(4,096 slots)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node 2:&lt;/strong&gt; Owns Slots 4,096 to 8,191 &lt;em&gt;(4,096 slots)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node 3:&lt;/strong&gt; Owns Slots 8,192 to 12,287 &lt;em&gt;(4,096 slots)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node 4:&lt;/strong&gt; Owns Slots 12,288 to 16,383 &lt;em&gt;(4,096 slots)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Scenario B: The 100-Node Cluster
&lt;/h4&gt;

&lt;p&gt;If your application grows massively and you spin up 100 master nodes, the total number of slots is &lt;em&gt;still&lt;/em&gt; 16,384. Redis divides the pie into much smaller pieces:&lt;/p&gt;

&lt;p&gt;[16,384 \text{ slots} \div 100 \text{ nodes} \approx 163 \text{ slots per node}]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node 1:&lt;/strong&gt; Owns Slots 0 to 162&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node 2:&lt;/strong&gt; Owns Slots 163 to 325&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node 100:&lt;/strong&gt; Owns Slots 16,221 to 16,383&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, when you add &lt;strong&gt;Node 101&lt;/strong&gt;, you don't rewrite the hashing math. You simply instruct the existing 100 nodes to hand over 1 or 2 slots to the newcomer. Only the keys residing within those specific moved slots need to be transferred across the network. The remaining 99% of your cluster’s data remains completely untouched and online.&lt;/p&gt;




&lt;h2&gt;
  
  
  Act 5: What Exactly is inside a Hash Slot?
&lt;/h2&gt;

&lt;p&gt;A common point of confusion is what a slot actually looks like under the hood. A slot does not contain your data, your text, or your values. Instead, a slot contains &lt;strong&gt;a list of references (pointers) to the memory locations of your keys&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Inside a single Redis node, there are two distinct layers of dictionaries (hash tables) holding everything together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Global Keyspace Dictionary (&lt;code&gt;db-&amp;gt;dict&lt;/code&gt;):&lt;/strong&gt; This is the master dictionary for the node. The key is your actual string (like &lt;code&gt;"user:101"&lt;/code&gt;), and the value is the actual data object stored in RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Cluster Slots Dictionary (&lt;code&gt;cluster-&amp;gt;slots&lt;/code&gt;):&lt;/strong&gt; This is an array of exactly 16,384 entries managed by the node. Each active slot in this array points to a smaller, dedicated sub-dictionary tracking the keys belonging to it.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Slot 4500] ───&amp;gt; Points to a list of keys: ["user:101", "user:999", "session:abc"]
                        │
                        └─► (Redis then looks up "user:101" in the master table to find the actual data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because you have millions of unique keys but only 16,384 slots, &lt;strong&gt;multiple keys are mathematically guaranteed to land in the same slot.&lt;/strong&gt; &lt;/p&gt;

&lt;h3&gt;
  
  
  Exploding Slots and Hash Tags
&lt;/h3&gt;

&lt;p&gt;While Redis's internal hash tables handle millions of keys per slot effortlessly, you can run into a &lt;strong&gt;Hot Slot&lt;/strong&gt; bottleneck if you aren't careful. Since all keys in a slot live on one physical node, a sudden spike in traffic to keys sharing a slot can peg that single node’s CPU to 100% while the rest of your cluster sits idle. &lt;/p&gt;

&lt;p&gt;However, you can also leverage this behavior to your advantage using &lt;strong&gt;Hash Tags&lt;/strong&gt;. If you put part of a key inside curly braces &lt;code&gt;{ }&lt;/code&gt;, Redis will only hash what is inside the braces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user:{101}:profile&lt;/code&gt; $\longrightarrow$ Redis only hashes &lt;code&gt;"101"&lt;/code&gt; $\longrightarrow$ &lt;strong&gt;Slot 7312&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user:{101}:orders&lt;/code&gt;  $\longrightarrow$ Redis only hashes &lt;code&gt;"101"&lt;/code&gt; $\longrightarrow$ &lt;strong&gt;Slot 7312&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By forcing these different keys into the exact same slot, you guarantee they will live on the &lt;strong&gt;exact same physical node&lt;/strong&gt;. This is the only way Redis allows you to run multi-key operations (like transactions) in a clustered environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Act 6: The Smart Client (Navigating the Slot Map)
&lt;/h2&gt;

&lt;p&gt;How does your application actually talk to this setup without wasting time? If your application had to query a random node every time just to ask where a slot lives, your network latency would double. Redis Cluster solves this by pushing the intelligence out to the client library (like Jedis, redis-py, or ioredis).&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Initial Handshake
&lt;/h3&gt;

&lt;p&gt;When your application boots up, the Redis client library connects to just &lt;strong&gt;one&lt;/strong&gt; known node in the cluster and executes a special command: &lt;code&gt;CLUSTER SLOTS&lt;/code&gt;. The node responds with the complete, current routing table of the cluster, which the client caches locally in your application's memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Zero-Latency Routing
&lt;/h3&gt;

&lt;p&gt;When your code executes &lt;code&gt;redis.get("user:101")&lt;/code&gt;, the client library intercepts the call locally inside your app's process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It runs the key through the hashing math: &lt;code&gt;CRC16("user:101") % 16384&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The result is &lt;strong&gt;Slot 4500&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It checks its local cache, sees that Slot 4500 belongs to Node A (&lt;code&gt;10.0.0.1&lt;/code&gt;), and routes the TCP request directly to Node A. Under normal operations, your app achieves &lt;strong&gt;O(1) routing lookup&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Handling the &lt;code&gt;MOVED&lt;/code&gt; Redirection
&lt;/h3&gt;

&lt;p&gt;What happens if an administrator is actively moving Slot 4500 from Node A to Node B while your app is running? Your client’s local cache becomes outdated.&lt;/p&gt;

&lt;p&gt;Your client sends the request for &lt;code&gt;"user:101"&lt;/code&gt; to Node A. Node A looks at its internal memory and realizes it just transferred that slot to Node B. Instead of processing the command, Node A fires back a specific error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-MOVED 4500 10.0.0.2:6379
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client library automatically catches this &lt;code&gt;-MOVED&lt;/code&gt; error behind the scenes, updates its local memory cache to note that Slot 4500 now lives at &lt;code&gt;10.0.0.2&lt;/code&gt;, and transparently re-runs the request against Node B.&lt;/p&gt;




&lt;h2&gt;
  
  
  Act 7: The Architect's Compromise (Why 16,384?)
&lt;/h2&gt;

&lt;p&gt;This brings us to our ultimate architectural riddle: &lt;strong&gt;If the total number of slots is a fixed pool, why choose 16,384? Why not make the fixed pool 65,536 (2¹⁶)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer lies in the harsh realities of decentralized networks and the internal mechanics of the Redis &lt;strong&gt;Gossip Protocol&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Cost of the Wire (The 2KB vs 8KB Bitmap)
&lt;/h3&gt;

&lt;p&gt;Redis Cluster operates without a centralized orchestrator (like ZooKeeper). To detect dead nodes and handle failovers, every node must constantly broadcast its state to other nodes via heartbeat packets.&lt;/p&gt;

&lt;p&gt;Inside every single heartbeat packet, a node must tell the rest of the cluster exactly which slots it currently owns out of that fixed pool. It does this using a raw binary checklist (a bitmap), where a &lt;code&gt;1&lt;/code&gt; means "I own this slot" and a &lt;code&gt;0&lt;/code&gt; means "I don't."&lt;/p&gt;

&lt;p&gt;Inside the Redis source code, this state tracking looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;clusterNode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CLUSTER_SLOTS&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// The raw configuration bitmap&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other node metadata like IP, port, flags&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's look at how the math changes based on the size of our fixed pool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;With a 16,384 fixed pool:&lt;/strong&gt; Each node sends a checklist that is 16,384 bits long. This requires exactly $16,384 \div 8 \text{ bits/byte} = \mathbf{2,048\text{ bytes (2KB)}}$ of data inside the header of every single packet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With a 65,536 fixed pool:&lt;/strong&gt; Each node would have to send a checklist that is 65,536 bits long. The packet header size for every single heartbeat immediately boots to $65,536 \div 8 \text{ bits/byte} = \mathbf{8,192\text{ bytes (8KB)}}$.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The Fallacy of Compression
&lt;/h3&gt;

&lt;p&gt;You might ask: &lt;em&gt;“Why not just use 65,536 slots and compress the 8KB bitmap before sending it?”&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;In a pristine, newly launched cluster, a node's slots are perfectly contiguous (e.g., Node 1 owns slots 0 to 655). A bitmap with a solid block of &lt;code&gt;1&lt;/code&gt;s followed by all &lt;code&gt;0&lt;/code&gt;s compresses beautifully down to a few bytes. &lt;/p&gt;

&lt;p&gt;However, in a mature production cluster that has undergone months of scaling, sharding, and node failovers, slots get heavily fragmented. Node 1 might end up owning slots 5, 22, 109, 455, 1200, and so on. Mathematically, a highly fragmented bit array behaves like random noise and &lt;strong&gt;cannot be compressed efficiently&lt;/strong&gt;. The CPU overhead required to constantly compress and decompress this data on Redis's single-threaded architecture would severely bottleneck throughput, and the packet would still hit the network at close to its full 8KB size.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Math of a Gossip Storm
&lt;/h3&gt;

&lt;p&gt;A 6KB difference feels trivial on a single web request. But Redis nodes execute their cluster cron loop every &lt;strong&gt;100 milliseconds&lt;/strong&gt; (10 times a second) to shuffle gossip packets across the mesh. &lt;/p&gt;

&lt;p&gt;Let's calculate the network traffic generated &lt;em&gt;purely by background heartbeats&lt;/em&gt; in a standard cluster of &lt;strong&gt;200 nodes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total Packets:&lt;/strong&gt; If each of the 200 nodes sends just 1 baseline ping per 100ms, that is 10 pings per node per second. That's $200 \times 10 = 2,000$ PING packets per second sent cluster-wide. Since every PING requires a corresponding PONG response, the total volume hits &lt;strong&gt;4,000 packets per second&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 2KB Setup (16,384 slots):&lt;/strong&gt; Including gossip metadata, a heartbeat packet sits at roughly 2.5KB. 
$$4,000 \text{ packets/sec} \times 2.5\text{KB} = \mathbf{\sim 10 \text{ MB/s of background noise}}$$&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 8KB Setup (65,536 slots):&lt;/strong&gt; If we change the bitmap to 8KB, the packet size swells to roughly 8.5KB.
$$4,000 \text{ packets/sec} \times 8.5\text{KB} = \mathbf{\sim 34 \text{ MB/s of background noise}}$$&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a healthy cluster, 34 MB/s is manageable. But networks are never perfectly healthy. Imagine a minor network switch blip occurs, causing packets to drop for a few seconds. &lt;/p&gt;

&lt;p&gt;Suddenly, all 200 nodes realize they haven't heard from their peers. Their "Urgent Check" loops trigger. Instead of pinging 1 node every 100ms, &lt;strong&gt;every node aggressively flings forced PINGs to all 199 other nodes simultaneously&lt;/strong&gt; to figure out who is alive. &lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;2KB&lt;/strong&gt;, the network spike hits &lt;strong&gt;~40 MB/s&lt;/strong&gt;. The infrastructure absorbs it effortlessly, nodes hear back from each other, and the cluster stabilizes.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;8KB&lt;/strong&gt;, the network spike violently jumps to &lt;strong&gt;Over 160 MB/s (1.3 Gbps)&lt;/strong&gt; of pure background chatter. This background surge can completely saturate the network interface cards (NICs) of smaller VM instances. Because the network is saturated with heartbeat bitmaps, your actual client requests (&lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;SET&lt;/code&gt; commands) get queued up, causing massive latency spikes. Worse, because the heartbeats themselves get dropped in the traffic jam, nodes will falsely assume their healthy peers are dead, triggering a chaotic chain-reaction of accidental master failovers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The 1,000-Node Pragmatic Limit
&lt;/h3&gt;

&lt;p&gt;Sanfilippo designed Redis Cluster with a strict architectural ceiling: &lt;strong&gt;the system is not intended to scale beyond 1,000 master nodes.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Let's look at the math at that extreme limit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At 1,000 master nodes, a &lt;strong&gt;16,384&lt;/strong&gt; slot pool means each node manages roughly 16 slots. This provides more than enough granularity for smooth, incremental data rebalancing. &lt;/li&gt;
&lt;li&gt;You would only &lt;em&gt;need&lt;/em&gt; a &lt;strong&gt;65,536&lt;/strong&gt; slot pool if you planned to scale to 10,000+ master nodes (so each node could have at least a few slots). But at 10,000 nodes, the $O(N^2)$ Gossip protocol would completely collapse the network under a tidal wave of heartbeat packets anyway. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Choosing 16,384 was the ultimate engineering compromise. It provided enough slots to cleanly distribute data across a realistic maximum of 1,000 nodes, while ensuring that the mandatory, uncompressible gossip bitmaps stayed at a lightweight 2KB—protecting the cluster from killing its own network bandwidth when things go wrong.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>backend</category>
      <category>redis</category>
      <category>performance</category>
    </item>
    <item>
      <title>How Meta Turns Old RAM Into Cheap RAM (and the Ideas That Make It Work)</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Fri, 10 Jul 2026 16:04:20 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/how-meta-turns-old-ram-into-cheap-ram-and-the-ideas-that-make-it-work-4b2i</link>
      <guid>https://dev.to/daksh-gargas/how-meta-turns-old-ram-into-cheap-ram-and-the-ideas-that-make-it-work-4b2i</guid>
      <description>&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%2Fw9d1owqmny0qn7eltacs.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%2Fw9d1owqmny0qn7eltacs.png" alt="Cover" width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a DRAM shortage, memory is expensive, and hyperscalers own mountains of it. So when Meta retires a server, throwing away its perfectly good memory sticks is real money on fire. The problem: old sticks don't slot cleanly into new machines. Meta's fix is a small bridge chip plus a clever reuse of ideas the operating system has had for years. Three ideas do the heavy lifting — NUMA, hot/cold tiering, and CXL — and they stack together nicely.&lt;/p&gt;

&lt;h2&gt;
  
  
  NUMA: not all RAM is equally close
&lt;/h2&gt;

&lt;p&gt;A modern server usually has more than one CPU chip on the motherboard. Each chip has its own bank of RAM wired directly to it — its &lt;em&gt;local&lt;/em&gt; memory. The chips are also linked to each other, so CPU 0 can reach CPU 1's RAM if it needs to. It just has to hop across the link between them first, which costs time.&lt;/p&gt;

&lt;p&gt;So access speed depends on &lt;em&gt;where&lt;/em&gt; the memory physically sits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU 0 reaching its own local RAM: fast (~80ns)&lt;/li&gt;
&lt;li&gt;CPU 0 reaching the other chip's RAM: slower (~140ns), because the request crosses the inter-chip link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Those numbers are ballpark and vary by hardware.) That's the "non-uniform" in &lt;strong&gt;NUMA — Non-Uniform Memory Access.&lt;/strong&gt; Same request, different latency depending on distance.&lt;/p&gt;

&lt;p&gt;Linux has long known about this and tries to help. A mechanism called AutoNUMA watches which CPU actually uses a given chunk of memory, and if a process's data ended up on the far chip, it quietly migrates that data over to the local one to cut the latency. The key pattern to hold onto: &lt;strong&gt;watch what gets accessed, then move it somewhere better.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hot and cold: sort data by how often you touch it
&lt;/h2&gt;

&lt;p&gt;Most workloads don't touch all their memory evenly. A small slice gets hammered constantly; the rest mostly sits idle. Call the busy stuff &lt;em&gt;hot&lt;/em&gt; and the idle stuff &lt;em&gt;cold&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Once you look at memory this way, an obvious strategy appears: keep hot data in your fast-but-small memory, and shove cold data into a slow-but-large tier. Because the slow tier only ever holds things nobody's really asking for, you rarely pay its latency penalty. This is the same idea behind CPU caches, the OS page cache, and RAM-versus-SSD — a fast small tier backed by a slow big one, with a placement policy keeping the right things in front.&lt;/p&gt;

&lt;h2&gt;
  
  
  CXL: a slower doorway for a lot more memory
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CXL — Compute Express Link&lt;/strong&gt; — lets memory talk to the CPU over PCIe, the same general-purpose high-speed lanes your graphics card uses, instead of the dedicated memory channels wired right next to the CPU. That extra hop makes CXL memory slower than native RAM, but far faster than an SSD or the network. And critically, it means memory no longer has to be a tightly-matched neighbor of the CPU — which is exactly why an old stick can join a new server through a bridge chip.&lt;/p&gt;

&lt;p&gt;On its own, slower memory sounds like a downgrade. Here's where the three ideas click together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff: reuse the NUMA plumbing for tiering
&lt;/h2&gt;

&lt;p&gt;CXL memory shows up to Linux as a NUMA node — just a "far" region, like the other chip's RAM. So Meta didn't need a new operating system. They took the AutoNUMA machinery that already does &lt;em&gt;watch access, then migrate&lt;/em&gt; and pointed it at a new decision: not "which chip is closer," but "is this page hot or cold?" Hot pages stay in fast local RAM; cold pages get demoted to the slow CXL pool. Meta's version of this is called &lt;strong&gt;TPP — Transparent Page Placement&lt;/strong&gt; — and it was merged into mainline Linux, so it's not a private fork.&lt;/p&gt;

&lt;p&gt;The result: old DIMMs that would have been scrapped become a big, cheap, slower memory tier — used a bit like fast swap space — and the OS keeps its latency off the critical path by making sure only cold data lives there. Meta reports up to a 25% reduction in server count for some workloads.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;A caveat worth keeping in mind: detecting "hot" isn't free. It leans on things like minor page faults and access-bit scanning, which themselves burn CPU cycles — so part of the real engineering is making the detection cheap enough that tiering doesn't eat its own savings. And the 25% figure comes from Meta's own paper, not independent benchmarks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cxl</category>
      <category>numa</category>
      <category>memory</category>
      <category>linux</category>
    </item>
    <item>
      <title>Snapshot your terminal state, restore it after a crash — Claude Code sessions included</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Tue, 07 Jul 2026 16:17:05 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/snapshot-your-terminal-state-restore-it-after-a-crash-claude-code-sessions-included-32pj</link>
      <guid>https://dev.to/daksh-gargas/snapshot-your-terminal-state-restore-it-after-a-crash-claude-code-sessions-included-32pj</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Last time I built &lt;a href="https://github.com/DenverLifeSciences/claude-sessions" rel="noopener noreferrer"&gt;&lt;code&gt;claude-sessions&lt;/code&gt;&lt;/a&gt;: one picker to resume any Claude Code session across projects. But a machine crash takes more than Claude with it — it takes your whole terminal &lt;em&gt;layout&lt;/em&gt;. Which tabs were open, which directory each one was in, which ones had a Claude session running.&lt;/p&gt;

&lt;p&gt;macOS window restoration exists, but it fails this job twice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It restores &lt;strong&gt;windows, not processes&lt;/strong&gt; — your &lt;code&gt;claude&lt;/code&gt; sessions don't come back.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;can't tell a crash from a quit&lt;/strong&gt; — close your terminal on purpose for a fresh start, and it cheerfully brings back all the old tabs anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;A snapshot daemon plus an &lt;em&gt;explicit&lt;/em&gt; restore — no auto-restore, ever.&lt;/p&gt;

&lt;p&gt;Every 5 minutes, a launchd job walks every iTerm window and tab via AppleScript, and for each tab resolves the tty → the processes on it (&lt;code&gt;ps&lt;/code&gt;) → the working directory (&lt;code&gt;lsof -d cwd&lt;/code&gt;) → and whether a &lt;code&gt;claude&lt;/code&gt; process is running there, including &lt;strong&gt;which session&lt;/strong&gt;: from its &lt;code&gt;--resume&lt;/code&gt; argument, from the transcript file it holds open, or from the newest transcript for that directory. The result is a small JSON file.&lt;/p&gt;

&lt;p&gt;Snapshots accumulate as &lt;strong&gt;history&lt;/strong&gt; — the last 100 &lt;em&gt;distinct&lt;/em&gt; states, consecutive duplicates skipped, and an empty terminal never overwrites anything.&lt;/p&gt;

&lt;p&gt;After a crash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude-sessions &lt;span class="nt"&gt;--restore-crash&lt;/span&gt;   &lt;span class="c"&gt;# newest snapshot&lt;/span&gt;
claude-sessions &lt;span class="nt"&gt;--restore-pick&lt;/span&gt;    &lt;span class="c"&gt;# or: fzf through history, pick the right one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every tab comes back — as tabs of the window you're standing in — &lt;code&gt;cd&lt;/code&gt;'d to its old directory, with &lt;code&gt;claude --resume &amp;lt;session-id&amp;gt;&lt;/code&gt; rerun wherever Claude was live. Intentional quit? Just don't run it. You are the crash detector; that's the feature.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three gotchas that cost me an evening
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;launchd can't read iCloud paths.&lt;/strong&gt; If your script lives in iCloud Drive (mine syncs via &lt;code&gt;~/.claude/bin&lt;/code&gt; → iCloud), the timer dies with &lt;code&gt;Operation not permitted&lt;/code&gt; — macOS TCC silently denies background jobs access to &lt;code&gt;~/Library/Mobile Documents&lt;/code&gt;. Point the plist at a local copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single snapshot file is a footgun.&lt;/strong&gt; After a crash you reopen a near-empty terminal, the timer fires, and your pre-crash snapshot is gone — replaced by three sad tabs. That's why history, not one file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't restore the original window grouping.&lt;/strong&gt; My first version faithfully recreated every window — including a junk window the snapshot had captured. Flattening everything into tabs of the current window is what you actually want.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Script, launchd template, and docs:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/DenverLifeSciences" rel="noopener noreferrer"&gt;
        DenverLifeSciences
      &lt;/a&gt; / &lt;a href="https://github.com/DenverLifeSciences/claude-sessions" rel="noopener noreferrer"&gt;
        claude-sessions
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Cross-project session picker for Claude Code CLI — fzf over all your sessions, Enter resumes each in a new tab
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;claude-sessions&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Find and resume any Claude Code session from any project — and rebuild your whole
terminal after a reboot — from one &lt;a href="https://github.com/junegunn/fzf" rel="noopener noreferrer"&gt;fzf&lt;/a&gt; picker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-project picker.&lt;/strong&gt; &lt;code&gt;claude --resume&lt;/code&gt; only lists sessions for the directory
you run it from. &lt;code&gt;claude-sessions&lt;/code&gt; lists &lt;em&gt;every&lt;/em&gt; session from &lt;em&gt;every&lt;/em&gt; project in
one picker, newest first. Enter opens the highlighted session in a new iTerm tab
(or tmux window) running &lt;code&gt;claude --resume &amp;lt;id&amp;gt;&lt;/code&gt; in the right directory — and the
picker stays open for the next one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crash recovery.&lt;/strong&gt; A timer snapshots your full iTerm layout every 5 minutes —
every window/tab, its working directory, and the Claude session running inside it
After a reboot (or an unattended OS update), one command rebuilds the whole set.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; Recovering by hand means remembering every repo you had open, &lt;code&gt;cd&lt;/code&gt;-ing into
each, and running &lt;code&gt;claude --resume&lt;/code&gt; there — once per session. This collapses…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/DenverLifeSciences/claude-sessions" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>claudecode</category>
      <category>cli</category>
      <category>productivity</category>
      <category>macos</category>
    </item>
    <item>
      <title>Every Claude Code session across all your projects, in one list — hit Enter to resume</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Mon, 06 Jul 2026 22:40:49 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/every-claude-code-session-across-all-your-projects-in-one-list-hit-enter-to-resume-3864</link>
      <guid>https://dev.to/daksh-gargas/every-claude-code-session-across-all-your-projects-in-one-list-hit-enter-to-resume-3864</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;My Mac hard-crashed with seven Claude Code CLI sessions open, across five repos. All gone.&lt;/p&gt;

&lt;p&gt;The built-in recovery is &lt;code&gt;claude --resume&lt;/code&gt;, and it doesn't scale to seven:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It only lists sessions for the &lt;strong&gt;directory you run it from&lt;/strong&gt; — you have to remember which repos you were even in, then &lt;code&gt;cd&lt;/code&gt; into each one.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;takes over the shell&lt;/strong&gt; you run it from — so you're spawning windows and retyping paths anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&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%2Fcub8flqa6kmkoos4l0sb.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%2Fcub8flqa6kmkoos4l0sb.png" alt="Solution" width="799" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code already stores everything you need as plain JSONL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/projects/&amp;lt;encoded-project-path&amp;gt;/&amp;lt;session-id&amp;gt;.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The first lines of each file carry the session id, working directory, git branch, and your opening message. That's a complete cross-project session index sitting on disk — it just needs &lt;a href="https://github.com/junegunn/fzf" rel="noopener noreferrer"&gt;fzf&lt;/a&gt; pointed at it.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;&lt;code&gt;claude-sessions&lt;/code&gt;&lt;/strong&gt;, one script.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every session from &lt;strong&gt;every project&lt;/strong&gt; in one list, newest first, with a live preview of the conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enter&lt;/strong&gt; opens the highlighted session in a new iTerm tab (or tmux window): &lt;code&gt;cd &amp;lt;project&amp;gt; &amp;amp;&amp;amp; claude --resume &amp;lt;id&amp;gt;&lt;/code&gt; — in the right directory, with Claude's own "summary or full session?" prompt appearing in the new tab. The picker stays open for the next one.&lt;/li&gt;
&lt;li&gt;Sessions spawned by &lt;strong&gt;agent teams&lt;/strong&gt; are tagged &lt;code&gt;⛭ agent-name&lt;/code&gt;; &lt;strong&gt;ctrl-a&lt;/strong&gt; hides them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Restoring my seven sessions: Enter, seven times. ~10 seconds.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;Needs &lt;code&gt;fzf&lt;/code&gt; and &lt;code&gt;python3&lt;/code&gt;. Tab-opening is macOS (iTerm2/Terminal); inside tmux it works on Linux too.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/DenverLifeSciences/claude-sessions/main/claude-sessions &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/local/bin/claude-sessions
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/claude-sessions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Script, docs, and caveats (the storage format is undocumented — this only ever &lt;em&gt;reads&lt;/em&gt; it):&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/DenverLifeSciences" rel="noopener noreferrer"&gt;
        DenverLifeSciences
      &lt;/a&gt; / &lt;a href="https://github.com/DenverLifeSciences/claude-sessions" rel="noopener noreferrer"&gt;
        claude-sessions
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Cross-project session picker for Claude Code CLI — fzf over all your sessions, Enter resumes each in a new tab
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;claude-sessions&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Find and resume any Claude Code session from any project — and rebuild your whole
terminal after a reboot — from one &lt;a href="https://github.com/junegunn/fzf" rel="noopener noreferrer"&gt;fzf&lt;/a&gt; picker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-project picker.&lt;/strong&gt; &lt;code&gt;claude --resume&lt;/code&gt; only lists sessions for the directory
you run it from. &lt;code&gt;claude-sessions&lt;/code&gt; lists &lt;em&gt;every&lt;/em&gt; session from &lt;em&gt;every&lt;/em&gt; project in
one picker, newest first. Enter opens the highlighted session in a new iTerm tab
(or tmux window) running &lt;code&gt;claude --resume &amp;lt;id&amp;gt;&lt;/code&gt; in the right directory — and the
picker stays open for the next one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crash recovery.&lt;/strong&gt; A timer snapshots your full iTerm layout every 5 minutes —
every window/tab, its working directory, and the Claude session running inside it
After a reboot (or an unattended OS update), one command rebuilds the whole set.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; Recovering by hand means remembering every repo you had open, &lt;code&gt;cd&lt;/code&gt;-ing into
each, and running &lt;code&gt;claude --resume&lt;/code&gt; there — once per session. This collapses…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/DenverLifeSciences/claude-sessions" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>claudecode</category>
      <category>cli</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Database Rate Limiting: The Missing Piece After a Circuit Breaker</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Sat, 27 Jun 2026 20:38:27 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/database-rate-limiting-the-missing-piece-after-a-circuit-breaker-2bp7</link>
      <guid>https://dev.to/daksh-gargas/database-rate-limiting-the-missing-piece-after-a-circuit-breaker-2bp7</guid>
      <description>&lt;p&gt;This is an extension to my previous post on &lt;a href="https://dev.to/daksh-gargas/a-circuit-breaker-alone-wont-save-your-database-3d0i"&gt;Circuit Breakers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A circuit breaker only decides:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Should I even try calling Redis?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When the circuit is open, your application executes the fallback.&lt;/p&gt;

&lt;p&gt;For many systems, that fallback is the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
   │
Circuit Breaker (Open)
   │
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's where the real problem begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Database Becomes the New Bottleneck
&lt;/h2&gt;

&lt;p&gt;Imagine your system normally handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;100K requests/sec&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Redis serves &lt;strong&gt;99K&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Database serves &lt;strong&gt;1K&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Redis suddenly goes down.&lt;/p&gt;

&lt;p&gt;Without any protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100K Requests

↓

Database 💥
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your database was never designed to handle the entire production workload.&lt;/p&gt;

&lt;p&gt;A circuit breaker saved you from Redis timeouts—but it didn't save your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: A DB Rate Limiter
&lt;/h2&gt;

&lt;p&gt;Instead of letting every request reach the database, the application enforces a limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;circuitBreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsOpen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;dbRateLimiter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Allow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;503&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;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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;If your database can safely handle &lt;strong&gt;5,000 QPS&lt;/strong&gt;, only those 5,000 requests are allowed through.&lt;/p&gt;

&lt;p&gt;The rest fail fast (or can be served from a local cache or stale data if available).&lt;/p&gt;

&lt;p&gt;The goal isn't to serve every request.&lt;/p&gt;

&lt;p&gt;The goal is to keep the database alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Does the Rate Limiter Live?
&lt;/h2&gt;

&lt;p&gt;One question I had while learning this was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Can't the API Gateway do this?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not really.&lt;/p&gt;

&lt;p&gt;The gateway only sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /users/123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has no idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is Redis down?&lt;/li&gt;
&lt;li&gt;Is this endpoint going to Postgres?&lt;/li&gt;
&lt;li&gt;Is it calling Elasticsearch?&lt;/li&gt;
&lt;li&gt;Is it reading Kafka?&lt;/li&gt;
&lt;li&gt;Does this endpoint even touch the database?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the service knows when it's about to hit the database.&lt;/p&gt;

&lt;p&gt;That's why the rate limiter is typically implemented &lt;strong&gt;inside the application&lt;/strong&gt;, immediately before the database call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP Handler
      │
Business Logic
      │
Circuit Breaker
      │
DB Rate Limiter   ← Here
      │
Repository
      │
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Simple Implementation
&lt;/h2&gt;

&lt;p&gt;At its core, it's just a token bucket.&lt;/p&gt;

&lt;p&gt;Suppose the database can safely handle &lt;strong&gt;500 QPS&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;redisCircuitBreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsOpen&lt;/span&gt;&lt;span class="p"&gt;()&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;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;localCache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&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;localCache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;dbRateLimiter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Allow&lt;/span&gt;&lt;span class="p"&gt;()&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;Error503&lt;/span&gt;&lt;span class="p"&gt;()&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;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;If there are no tokens left, the request is rejected immediately.&lt;/p&gt;

&lt;p&gt;No waiting.&lt;/p&gt;

&lt;p&gt;No overwhelming the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  But What About Multiple Application Servers?
&lt;/h2&gt;

&lt;p&gt;This was another question that came to mind.&lt;/p&gt;

&lt;p&gt;Suppose you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 App Servers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each server allows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 QPS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the database receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 × 500 = 5,000 QPS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect.&lt;/p&gt;

&lt;p&gt;In practice, each instance is usually allocated a portion of the database's total capacity.&lt;/p&gt;

&lt;p&gt;Larger systems may use distributed rate limiters or adaptive concurrency limits, but the idea remains the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never allow your fallback path to exceed what your database can safely handle.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Circuit Breaker&lt;/strong&gt; prevents wasted calls to a failing dependency.&lt;/li&gt;
&lt;li&gt;It does &lt;strong&gt;not&lt;/strong&gt; protect your database.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;DB Rate Limiter&lt;/strong&gt; sits inside the application, just before the database call.&lt;/li&gt;
&lt;li&gt;It intentionally rejects excess requests to keep the database healthy.&lt;/li&gt;
&lt;li&gt;During outages, preserving the system is often more important than serving every request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A resilient system isn't one that never fails—it's one that fails without taking everything else down.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>backend</category>
      <category>redis</category>
      <category>performance</category>
    </item>
    <item>
      <title>A Circuit Breaker Alone Won't Save Your Database</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Sat, 27 Jun 2026 20:33:58 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/a-circuit-breaker-alone-wont-save-your-database-3d0i</link>
      <guid>https://dev.to/daksh-gargas/a-circuit-breaker-alone-wont-save-your-database-3d0i</guid>
      <description>&lt;p&gt;A common misconception is that a &lt;strong&gt;circuit breaker protects your database&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;p&gt;It only protects your application from repeatedly calling a dependency that is already known to be unhealthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when Redis goes down?
&lt;/h2&gt;

&lt;p&gt;Without a circuit breaker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
   │
Redis (500ms timeout)
   │
DB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Waits for Redis to time out.&lt;/li&gt;
&lt;li&gt;Wastes network calls.&lt;/li&gt;
&lt;li&gt;Eventually hits the database anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Enter the Circuit Breaker
&lt;/h2&gt;

&lt;p&gt;After Redis fails repeatedly, the circuit opens.&lt;/p&gt;

&lt;p&gt;Now requests become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
   │
Circuit Breaker
   │
Fallback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice something?&lt;/p&gt;

&lt;p&gt;The circuit breaker &lt;strong&gt;doesn't decide the fallback&lt;/strong&gt;. It only decides:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Should I even try Redis?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  So what is the fallback?
&lt;/h2&gt;

&lt;p&gt;A production system usually follows this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
   │
Circuit Open?
   │
Yes
   │
Local Cache?
   │
Hit ─────────► Return
   │
Miss
   │
DB Rate Limiter
   │
Allowed?
   │
Yes ─────────► Database
   │
No
   │
Return 503
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why a DB Rate Limiter?
&lt;/h2&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normal traffic: &lt;strong&gt;100K RPS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Redis serves &lt;strong&gt;99K RPS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Database handles &lt;strong&gt;1K RPS&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Redis crashes and every request falls back to the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100K RPS

↓

Database 💥
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, protect the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;circuitBreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsOpen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;localCache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&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;localCache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;dbRateLimiter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Allow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;503&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;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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;Only a limited number of requests are allowed to reach the database. The rest fail fast, keeping the system alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where is the DB Rate Limiter?
&lt;/h2&gt;

&lt;p&gt;Not in the API Gateway.&lt;/p&gt;

&lt;p&gt;The gateway only sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /users/123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has no idea whether your service will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query Redis&lt;/li&gt;
&lt;li&gt;Query Postgres&lt;/li&gt;
&lt;li&gt;Call Elasticsearch&lt;/li&gt;
&lt;li&gt;Read Kafka&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;service itself&lt;/strong&gt; knows when it's about to hit the database, so that's where the dependency-specific rate limiter belongs.&lt;/p&gt;

&lt;p&gt;👉 Read: &lt;a href="https://dev.to/daksh-gargas/database-rate-limiting-the-missing-piece-after-a-circuit-breaker-2bp7"&gt;Database Rate Limiting: The Missing Piece After a Circuit Breaker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A circuit breaker and a rate limiter solve different problems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Circuit Breaker:&lt;/strong&gt; Stop calling a dependency that is already failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB Rate Limiter:&lt;/strong&gt; Protect the database from being overwhelmed when fallbacks occur.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production, they almost always work together.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>systemdesign</category>
      <category>backend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why Redis Doesn't Implement "True" LRU</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Sat, 27 Jun 2026 20:15:57 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/why-redis-doesnt-implement-true-lru-1d4n</link>
      <guid>https://dev.to/daksh-gargas/why-redis-doesnt-implement-true-lru-1d4n</guid>
      <description>&lt;p&gt;One question that recently made me rethink cache eviction was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If Redis uses LRU, why doesn't it maintain a heap (or a perfectly sorted list) of keys?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer comes down to optimizing the common case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Imagine a Redis instance with &lt;strong&gt;10 million keys&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If Redis maintained a perfect LRU structure, every cache hit would need to update it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET user:123

↓

Update LRU ordering

↓

Return value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though the lookup is O(1), updating a heap would be O(log n), and maintaining a doubly-linked LRU list would still require modifying shared metadata on &lt;strong&gt;every single read&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a cache serving millions of requests per second, that's expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis's Approach: Approximate LRU
&lt;/h2&gt;

&lt;p&gt;Instead of maintaining an exact ordering, Redis uses &lt;strong&gt;random sampling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When memory is full and a write arrives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Randomly sample a small number of keys (default: &lt;strong&gt;5&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Find the least recently used among them.&lt;/li&gt;
&lt;li&gt;Evict that key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At first glance, this seems inaccurate.&lt;/p&gt;

&lt;p&gt;What if all 5 sampled keys are hot?&lt;/p&gt;

&lt;p&gt;It's possible—but statistically very unlikely for most real-world workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Clever Optimization: Eviction Pool
&lt;/h2&gt;

&lt;p&gt;Redis goes one step further.&lt;/p&gt;

&lt;p&gt;Instead of discarding the remaining sampled keys after each eviction, it keeps the best eviction candidates in a small &lt;strong&gt;eviction pool&lt;/strong&gt; (16 entries internally).&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Iteration 1
------------
Sample: A B C D E

Pool:
A B C D E

Evict A

Remaining Pool:
B C D E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need more memory?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Iteration 2
------------
Sample:
F G H I J

Merge:
B C D E F G H I J

Keep only the best candidates

Evict the worst one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pool gradually accumulates better eviction candidates while still sampling only a handful of random keys each iteration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;Redis optimizes for the common case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Millions of &lt;strong&gt;GETs&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Relatively few &lt;strong&gt;evictions&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of paying a maintenance cost on every read, Redis does a small amount of work only when memory is exhausted.&lt;/p&gt;

&lt;p&gt;This is a classic systems engineering trade-off:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Accept a near-perfect approximation during rare events to keep the hot path extremely fast.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's one of the reasons Redis continues to scale so well while delivering cache hit rates that are remarkably close to a true LRU implementation.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>systemdesign</category>
      <category>backend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Bloom Filters, Explained Properly</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Wed, 24 Jun 2026 20:03:13 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/bloom-filters-explained-properly-1i6i</link>
      <guid>https://dev.to/daksh-gargas/bloom-filters-explained-properly-1i6i</guid>
      <description>&lt;p&gt;Most explanations throw a wall of theory at you and leave out the one thing that actually makes it click. This post fixes that.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one question a bloom filter answers
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this item &lt;strong&gt;definitely not&lt;/strong&gt; in the set, or &lt;strong&gt;possibly&lt;/strong&gt; in it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It can tell you "no" with total certainty, but only ever says "maybe" for yes. It never gives a false negative; it can give a false positive. That asymmetry is the whole point — everything falls out of it. In exchange for the fuzziness, you get tiny memory and blazing speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it's made of
&lt;/h2&gt;

&lt;p&gt;Two things: a &lt;strong&gt;bit array&lt;/strong&gt; (a row of light switches, all starting at 0) and a few &lt;strong&gt;hash functions&lt;/strong&gt; (machines that take an item and spit out a position in the array). No stored words. No list of items. Just switches and hash functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many bits does one item take?
&lt;/h2&gt;

&lt;p&gt;This is the question that trips everyone up. When you add &lt;code&gt;cat&lt;/code&gt;, how much room does it take?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exactly &lt;code&gt;k&lt;/code&gt; bits — and &lt;code&gt;k&lt;/code&gt; is a number &lt;em&gt;you&lt;/em&gt; pick in advance, not something the word decides.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A word has no natural size. You decide up front, "every item sets exactly 3 bits." That's &lt;code&gt;k&lt;/code&gt;, the same for everything. &lt;code&gt;cat&lt;/code&gt; sets 3 bits, &lt;code&gt;dog&lt;/code&gt; sets 3 bits, &lt;code&gt;elephant&lt;/code&gt; sets 3 bits. Word length is irrelevant — &lt;code&gt;elephant&lt;/code&gt; takes no more room than &lt;code&gt;cat&lt;/code&gt;. &lt;em&gt;Which&lt;/em&gt; bits? That's what the hash functions decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the array
&lt;/h2&gt;

&lt;p&gt;Tiny array of 10 slots, &lt;code&gt;k = 3&lt;/code&gt;. Add &lt;code&gt;cat&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hashA("cat") -&amp;gt; 4
hashB("cat") -&amp;gt; 4    &amp;lt;- two hashes can land on the same spot
hashC("cat") -&amp;gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flip those bits on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index:  0  1  2  3  4  5  6  7  8  9
        .  ■  .  .  ■  .  .  .  .  .
           ^cat        ^cat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cat&lt;/code&gt; lit only &lt;strong&gt;2&lt;/strong&gt; distinct bits even though &lt;code&gt;k = 3&lt;/code&gt;, because two hashes both said 4. Now add &lt;code&gt;dog&lt;/code&gt; (hashes to 7, 1, 9):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index:  0  1  2  3  4  5  6  7  8  9
        .  ■  .  .  ■  .  .  ■  .  ■
           ^            ^  ^     ^
        cat+dog        cat dog  dog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bit 1 was already lit by &lt;code&gt;cat&lt;/code&gt;; &lt;code&gt;dog&lt;/code&gt; setting it again does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The insight: you can't read the array
&lt;/h2&gt;

&lt;p&gt;Here's the part that sticks. Look at that final array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        .  ■  .  .  ■  .  .  ■  .  ■
        0  1  2  3  4  5  6  7  8  9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If I handed you this array cold, you could not tell me what's in it.&lt;/strong&gt; You couldn't say "cat and dog are here." All you could say is "switches 1, 4, 7, 9 are on." The array keeps &lt;em&gt;no record of who lit each bit&lt;/em&gt;. A lit bit just means &lt;em&gt;somebody&lt;/em&gt; hashed there — it doesn't remember who, or how many.&lt;/p&gt;

&lt;p&gt;That forgetfulness is the source of everything. It's why the filter is tiny (it's not storing your words, just their smeared fingerprints), and it's why "maybe" can never become "yes."&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%2F7u8aescrtmbosb08qotv.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%2F7u8aescrtmbosb08qotv.png" alt="Bloom filter: array forming across inserts and a false positive" width="800" height="690"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Querying: same hashes, then an AND
&lt;/h2&gt;

&lt;p&gt;To check &lt;code&gt;cat&lt;/code&gt;, hash it again — same outputs, 4, 4, 1 — and check whether &lt;em&gt;those&lt;/em&gt; bits are all 1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Are bits 4 AND 1 both 1?  -&amp;gt; yes -&amp;gt; "possibly present"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's an AND across the item's own target bits. One 0 anywhere is an instant "definitely not" — it doesn't even check the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "No" is certain and "Maybe" isn't
&lt;/h2&gt;

&lt;p&gt;It comes from one fact: &lt;strong&gt;bits only ever turn on, never off.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a 0 bit -&amp;gt; "this slot was never touched" -&amp;gt; CERTAIN, nothing erases
a 1 bit -&amp;gt; "someone touched this slot"   -&amp;gt; AMBIGUOUS, the array forgot who
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;0 is proof of absence&lt;/strong&gt;: if a position is still 0, nothing ever hashed there — so any item needing that bit was definitely never added. A &lt;strong&gt;1 is only circumstantial&lt;/strong&gt;: it could be your item, or other items that happened to share that slot. When all an item's bits are 1, you can't tell those apart, so the only honest answer is "maybe."&lt;/p&gt;

&lt;p&gt;This is also why &lt;strong&gt;false negatives are impossible&lt;/strong&gt; — a real member always finds its own bits lit, because it set them and nothing erases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The false positive
&lt;/h2&gt;

&lt;p&gt;Query &lt;code&gt;wolf&lt;/code&gt;, never added, but it happens to hash to bits 4 and 1 — both already lit by &lt;code&gt;cat&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wolf -&amp;gt; bits 4, 1 -&amp;gt; both 1 -&amp;gt; "maybe present"  -&amp;gt; WRONG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filter isn't lying. Its real claim is "every bit &lt;code&gt;wolf&lt;/code&gt; needs is lit" — which is true. It never promised &lt;code&gt;wolf&lt;/code&gt; is present, only that it &lt;em&gt;can't rule it out&lt;/em&gt;. And it genuinely can't: "wolf was added" and "cat was added and wolf collides" produce the identical pattern. The array forgot who lit those bits, remember? So it cannot distinguish them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's useful anyway
&lt;/h2&gt;

&lt;p&gt;A trustworthy "no" lets you skip expensive work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query the filter
  -&amp;gt; "definitely not"  -&amp;gt; skip the expensive lookup (the common case)
  -&amp;gt; "maybe"           -&amp;gt; do the real lookup to confirm (the rare case)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A false positive costs you one wasted lookup, never a wrong final answer — you always verify a "maybe" against the real source. Real uses: &lt;strong&gt;LSM-tree databases&lt;/strong&gt; (Cassandra, RocksDB) put one in front of each on-disk table to skip reads for keys that definitely aren't there; &lt;strong&gt;browser safe-browsing&lt;/strong&gt; keeps a local filter of bad URLs and only phones home on a "maybe."&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff, briefly
&lt;/h2&gt;

&lt;p&gt;More bits and more hash functions lower the false-positive rate, at the cost of memory. You pick how many items you'll store and what error rate you'll tolerate; the array size and number of hash functions fall out of that (the sweet spot lands when the array is about half-full of 1s). And you &lt;strong&gt;can't delete&lt;/strong&gt; from a basic bloom filter — clearing a bit might break other items sharing it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Going deeper.&lt;/strong&gt; The exact sizing math — deriving the false-positive rate &lt;code&gt;(1 − e^(−kn/m))^k&lt;/code&gt;, the optimal &lt;code&gt;m = −(n ln p)/(ln 2)²&lt;/code&gt;, the optimal &lt;code&gt;k = (m/n) ln 2&lt;/code&gt;, and why &lt;code&gt;ln 2&lt;/code&gt; keeps appearing — is laid out clearly in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Bloom_filter" rel="noopener noreferrer"&gt;Wikipedia: Bloom filter&lt;/a&gt; — the standard reference, full derivations&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://llimllib.github.io/bloomfilter-tutorial/" rel="noopener noreferrer"&gt;Bloom Filters by Example&lt;/a&gt; — interactive, lets you watch the array fill&lt;/li&gt;
&lt;li&gt;The original 1970 paper: Burton H. Bloom, &lt;em&gt;Space/Time Trade-offs in Hash Coding with Allowable Errors&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The one-paragraph version
&lt;/h2&gt;

&lt;p&gt;A bloom filter is a bit array plus a few hash functions. Adding an item flips the &lt;code&gt;k&lt;/code&gt; bits it hashes to; checking looks at those same bits. Any 0 means definitely-not; all 1s mean probably-yes. The key: you can't read the array — a lit bit doesn't remember who lit it, so a 0 proves absence but a 1 is only circumstantial. That one-way guarantee makes it a perfect cheap gate in front of an expensive lookup: trust the "no," verify the "maybe."&lt;/p&gt;

</description>
      <category>datastructures</category>
      <category>algorithms</category>
      <category>database</category>
      <category>programming</category>
    </item>
    <item>
      <title>Beyond Sub-Agents: One Claude Orchestrating Another (with tmux)</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Wed, 17 Jun 2026 17:46:47 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/beyond-sub-agents-one-claude-orchestrating-another-with-tmux-e1o</link>
      <guid>https://dev.to/daksh-gargas/beyond-sub-agents-one-claude-orchestrating-another-with-tmux-e1o</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2F8uibnwezc5qobbu1wrtu.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.amazonaws.com%2Fuploads%2Farticles%2F8uibnwezc5qobbu1wrtu.png" alt="Summary" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A detached tmux session can run completely independently of any visible terminal window. You can send keystrokes into it, capture everything displayed on screen, and attach a human observer at any time.&lt;/p&gt;

&lt;p&gt;In other words, tmux lets you automate an interactive terminal session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Suppose you have a detached tmux session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux new-session &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can send input into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; worker &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"echo hello"&lt;/span&gt;
tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; worker Enter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you can read what's on screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux capture-pane &lt;span class="nt"&gt;-t&lt;/span&gt; worker &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's essentially it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write → Terminal
Read  ← Terminal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you can write to a terminal and read from it, the terminal becomes programmable.&lt;/p&gt;

&lt;p&gt;Anything that runs inside a terminal can now be controlled programmatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Claude Orchestrating Another
&lt;/h2&gt;

&lt;p&gt;Once you realize a tmux session is programmable, an interesting possibility appears.&lt;/p&gt;

&lt;p&gt;Instead of asking Claude to spawn a sub-agent, you can ask it to create and manage an entirely separate Claude Code session.&lt;/p&gt;

&lt;p&gt;That worker Claude runs independently in its own terminal, while the primary Claude retains the conversation context, understands the broader objective, and remains responsible for planning and decision-making.&lt;/p&gt;

&lt;p&gt;From your perspective, you're still talking to a single Claude.&lt;/p&gt;

&lt;p&gt;Behind the scenes, that Claude is coordinating another fully capable Claude.&lt;/p&gt;

&lt;p&gt;Your primary Claude can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a detached tmux session&lt;/li&gt;
&lt;li&gt;Launch a worker Claude inside it&lt;/li&gt;
&lt;li&gt;Send prompts&lt;/li&gt;
&lt;li&gt;Read responses&lt;/li&gt;
&lt;li&gt;Review results&lt;/li&gt;
&lt;li&gt;Decide what to do next&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worker Claude isn't a sub-agent.&lt;/p&gt;

&lt;p&gt;It's a completely separate Claude Code process running in its own terminal.&lt;/p&gt;

&lt;p&gt;Meanwhile, the primary Claude retains all of the context, planning, and decision-making.&lt;/p&gt;

&lt;p&gt;You end up with one Claude orchestrating another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use a Sub-Agent?
&lt;/h2&gt;

&lt;p&gt;Sub-agents are great for bounded tasks.&lt;/p&gt;

&lt;p&gt;But sometimes what you really want isn't a sub-agent.&lt;/p&gt;

&lt;p&gt;You want another fully capable Claude Code session.&lt;/p&gt;

&lt;p&gt;The challenge is that you don't want to give up the context, planning, and decision-making already accumulated by your current Claude.&lt;/p&gt;

&lt;p&gt;You want your existing Claude to stay in charge while delegating work to a fully capable worker.&lt;/p&gt;

&lt;p&gt;A tmux-backed Claude worker gives you exactly that.&lt;/p&gt;

&lt;p&gt;The primary Claude remains the orchestrator.&lt;/p&gt;

&lt;p&gt;The worker Claude remains fully capable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create worker session&lt;/span&gt;
tmux new-session &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; worker

&lt;span class="c"&gt;# Launch Claude&lt;/span&gt;
tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; worker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'env -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT -u CLAUDE_CODE_SSE_PORT claude'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  Enter

&lt;span class="c"&gt;# Send prompt&lt;/span&gt;
tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; worker &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"Review this repository"&lt;/span&gt;
tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; worker Enter

&lt;span class="c"&gt;# Read output&lt;/span&gt;
tmux capture-pane &lt;span class="nt"&gt;-t&lt;/span&gt; worker &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nt"&gt;-1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're not talking to Claude through an API.&lt;/p&gt;

&lt;p&gt;You're automating a terminal exactly like a human would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha: Claude Doesn't Launch (and Prompts Don't Submit)
&lt;/h2&gt;

&lt;p&gt;There are two things that will almost certainly trip you up the first time you try this.&lt;/p&gt;

&lt;h3&gt;
  
  
  #1 Nested Claude Sessions Exit Immediately
&lt;/h3&gt;

&lt;p&gt;If you launch Claude from inside an existing Claude Code session, the child process inherits Claude-specific environment variables and immediately exits.&lt;/p&gt;

&lt;p&gt;The fix is to clear the inherited variables before launching the worker:&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;env&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; CLAUDECODE &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-u&lt;/span&gt; CLAUDE_CODE_ENTRYPOINT &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-u&lt;/span&gt; CLAUDE_CODE_SSE_PORT &lt;span class="se"&gt;\&lt;/span&gt;
    claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  #2 &lt;code&gt;send-keys&lt;/code&gt; Doesn't Press Enter
&lt;/h3&gt;

&lt;p&gt;My first attempt looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; worker &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"Review this repository"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing happened.&lt;/p&gt;

&lt;p&gt;That's because &lt;code&gt;send-keys&lt;/code&gt; only types text into the terminal. It doesn't actually submit the prompt.&lt;/p&gt;

&lt;p&gt;You need to explicitly send the Enter key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; worker &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"Review this repository"&lt;/span&gt;
tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; worker Enter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you realize that, the interaction model becomes surprisingly simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;send-keys -l&lt;/code&gt; → type text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Enter&lt;/code&gt; → submit prompt&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;capture-pane&lt;/code&gt; → read output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're not talking to Claude through an API.&lt;/p&gt;

&lt;p&gt;You're automating a terminal exactly like a human would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human Observability
&lt;/h2&gt;

&lt;p&gt;A human can attach to the worker at any time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux attach &lt;span class="nt"&gt;-t&lt;/span&gt; worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator and a human can now observe the exact same Claude session simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The interesting part isn't tmux.&lt;/p&gt;

&lt;p&gt;It's realizing that you can ask Claude to create more Claudes, assign them work, review their results, and make decisions based on the outcome.&lt;/p&gt;

&lt;p&gt;After that, &lt;code&gt;send-keys&lt;/code&gt; and &lt;code&gt;capture-pane&lt;/code&gt; are just implementation details.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>agents</category>
      <category>tmux</category>
    </item>
    <item>
      <title>Gmail ate my formatting. So I built PasteClean.</title>
      <dc:creator>Daksh Gargas</dc:creator>
      <pubDate>Wed, 27 May 2026 23:49:28 +0000</pubDate>
      <link>https://dev.to/daksh-gargas/gmail-ate-my-formatting-so-i-built-pasteclean-2i9p</link>
      <guid>https://dev.to/daksh-gargas/gmail-ate-my-formatting-so-i-built-pasteclean-2i9p</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fygrucojlb17bifc88fd9.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.amazonaws.com%2Fuploads%2Farticles%2Fygrucojlb17bifc88fd9.png" alt="Paste Clean" width="800" height="778"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem that wouldn't leave me alone: I'd draft something in Notes or ChatGPT — clean formatting, bold headings, blockquotes — copy it, paste it into Gmail's compose window, and watch it fall apart. &lt;/p&gt;

&lt;p&gt;White text on white background. Fonts swapped. Layouts collapsed. Gmail's renderer strips a lot of HTML, and nothing on iOS fixes it cleanly before the paste.&lt;/p&gt;

&lt;p&gt;So I built one.&lt;/p&gt;

&lt;p&gt;PasteClean is an iOS app that sits between your writing and Gmail's compose window:&lt;/p&gt;

&lt;p&gt;→ Write in a rich text editor&lt;br&gt;
→ Preview exactly how it'll look in Gmail (light and dark mode)&lt;br&gt;
→ Copy — PasteClean sanitizes the HTML and puts Gmail-safe markup on your clipboard&lt;br&gt;
→ Paste. Formatting intact.&lt;/p&gt;

&lt;p&gt;It's live on the &lt;a href="https://apps.apple.com/us/app/pasteclean/id6769190276" rel="noopener noreferrer"&gt;App Store&lt;/a&gt;! 🎊&lt;/p&gt;

</description>
      <category>ios</category>
      <category>reactnative</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
