<?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: LeetDezine</title>
    <description>The latest articles on DEV Community by LeetDezine (@leetdezine).</description>
    <link>https://dev.to/leetdezine</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3886899%2Fb08b199f-7074-42c4-a66a-5cb82c87672a.png</url>
      <title>DEV Community: LeetDezine</title>
      <link>https://dev.to/leetdezine</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leetdezine"/>
    <language>en</language>
    <item>
      <title>Latency vs Throughput</title>
      <dc:creator>LeetDezine</dc:creator>
      <pubDate>Thu, 07 May 2026 03:17:41 +0000</pubDate>
      <link>https://dev.to/leetdezine/latency-vs-throughput-20g</link>
      <guid>https://dev.to/leetdezine/latency-vs-throughput-20g</guid>
      <description>&lt;p&gt;&lt;a href="https://leetdezine.com/performance-metrics/latency-vs-throughput/?utm_source=devto" rel="noopener noreferrer"&gt;LeetDezine&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first time I heard "optimize for latency," I thought it meant "make it fast." So I turned off batching, flushed writes immediately, set Kafka's &lt;code&gt;linger.ms&lt;/code&gt; to 0.&lt;/p&gt;

&lt;p&gt;The system responded faster. And handled way less load.&lt;/p&gt;

&lt;p&gt;Latency and throughput pull in opposite directions. Making your system faster for individual requests usually means it handles fewer of them per second. Handling more per second usually means individual requests wait longer. This tradeoff shows up everywhere and misidentifying which axis to optimize is one of the most common architecture mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  DB Batch Writes
&lt;/h2&gt;

&lt;p&gt;Your database is getting hammered with writes. Each write goes to disk immediately.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latency per write: 10ms&lt;/li&gt;
&lt;li&gt;One thread handles: 100 writes per second&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Change the approach: collect 100 records, flush in one batch. One disk operation instead of 100.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Throughput&lt;/strong&gt; went up — the disk does the same work with 100× fewer operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency&lt;/strong&gt; went up — the first record now waits for 99 more before anything gets written.&lt;/p&gt;

&lt;p&gt;You traded fast individual responses for higher total capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Netflix's Loading Spinner
&lt;/h2&gt;

&lt;p&gt;Netflix needs to send you a video. Two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A:&lt;/strong&gt; stream one tiny chunk the moment it's ready. Low latency, you start watching fast. But thousands of tiny chunks = thousands of network trips per second = inefficient. Fewer users served.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B:&lt;/strong&gt; buffer and send larger chunks. Fewer network trips, more users served per second. But you wait a few seconds before playback starts.&lt;/p&gt;

&lt;p&gt;That loading spinner isn't a bug. Netflix deliberately accepts higher startup latency to serve more users efficiently. Same tradeoff, product-level decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kafka's linger.ms
&lt;/h2&gt;

&lt;p&gt;This one makes the tradeoff explicit. Kafka producers have a config called &lt;code&gt;linger.ms&lt;/code&gt; — how long the producer waits before flushing a batch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;linger.ms = 0:
  Every event fires immediately — one network call per event
  At 100K events/sec = 100K network calls/sec
  Low latency per event, terrible network efficiency

linger.ms = 5:
  Producer accumulates events for 5ms, flushes together
  At 100K events/sec ≈ 20K batch calls/sec
  Slightly higher latency per event, 5× better throughput
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;linger.ms&lt;/code&gt; is a literal dial between the two extremes. Kafka doesn't choose for you — it expects you to understand the tradeoff and set it intentionally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;Every example is the same thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make individuals wait → serve more of them overall.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;What you traded&lt;/th&gt;
&lt;th&gt;What you got&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DB batch writes&lt;/td&gt;
&lt;td&gt;Per-record speed&lt;/td&gt;
&lt;td&gt;Total capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netflix buffering&lt;/td&gt;
&lt;td&gt;Startup speed&lt;/td&gt;
&lt;td&gt;User capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka linger.ms &amp;gt; 0&lt;/td&gt;
&lt;td&gt;Message delay&lt;/td&gt;
&lt;td&gt;Network efficiency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The inverse is also true: processing immediately = low latency, lower throughput. You're always on this spectrum.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Decide
&lt;/h2&gt;

&lt;p&gt;Ask: &lt;strong&gt;what does a bad experience look like for this system?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chat message takes 5 seconds to send → users leave. &lt;strong&gt;Optimize for latency.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Analytics pipeline runs overnight → nobody cares if a log arrived 2 seconds late. &lt;strong&gt;Optimize for throughput.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Payment confirmation → user is staring at a loading screen. &lt;strong&gt;Optimize for latency.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Log processing → you're crunching billions of events in bulk. &lt;strong&gt;Optimize for throughput.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>performance</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>SQS Standard vs SQS FIFO vs Kafka</title>
      <dc:creator>LeetDezine</dc:creator>
      <pubDate>Sat, 02 May 2026 03:43:22 +0000</pubDate>
      <link>https://dev.to/leetdezine/sqs-standard-vs-sqs-fifo-vs-kafka-5f91</link>
      <guid>https://dev.to/leetdezine/sqs-standard-vs-sqs-fifo-vs-kafka-5f91</guid>
      <description>&lt;p&gt;&lt;a href="https://leetdezine.com/notification-system/?utm_source=devto" rel="noopener noreferrer"&gt;LeetDezine&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The SQS FIFO vs Standard decision sounds simple: need ordering? Use FIFO. Don't care? Use Standard.&lt;/p&gt;

&lt;p&gt;That framing leads to the wrong answer more often than not. The real question isn't "do I need ordering?" — it's "what actually breaks if messages arrive out of order or more than once?"&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;SQS Standard:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Throughput: high (can scale via sharding, region defaults are generous)&lt;/li&gt;
&lt;li&gt;Delivery: at-least-once — the same message can appear more than once&lt;/li&gt;
&lt;li&gt;Ordering: best-effort — messages might arrive out of order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SQS FIFO:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Throughput: 3,000 messages/sec per queue (300 per message group without batching)&lt;/li&gt;
&lt;li&gt;Delivery: exactly-once — deduplication built in via &lt;code&gt;MessageDeduplicationId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ordering: strict — messages within a group are delivered in send order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FIFO looks better on every axis except throughput. That exception is the one that kills you at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Throughput Wall
&lt;/h2&gt;

&lt;p&gt;A notification system handling 5M messages/sec — Instagram-scale, celebrity post, 10M followers needing push notifications — runs into a hard wall with FIFO.&lt;/p&gt;

&lt;p&gt;At 3K/sec per queue, you need 1,667 queues to absorb the peak. Now you need routing logic: which notification goes to which queue? You need to partition users, maintain queue mappings, handle rebalancing. You've built Kafka, badly.&lt;/p&gt;

&lt;p&gt;Even at moderate scale — 50K/sec — that's 17 FIFO queues with custom routing on top. The operational complexity eats the simplicity SQS was supposed to give you.&lt;/p&gt;

&lt;p&gt;The ceiling is architectural, not configurable. FIFO queues don't scale horizontally the way Kafka partitions do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ordering Trap
&lt;/h2&gt;

&lt;p&gt;Most engineers reach for FIFO because ordering &lt;em&gt;sounds&lt;/em&gt; important. It's usually the wrong diagnosis.&lt;/p&gt;

&lt;p&gt;Ordering matters when out-of-order processing corrupts state. A bank ledger where debit must follow credit. An event sourcing system where aggregate state is reconstructed from events in sequence. A stock trading system where order execution has legal ordering requirements.&lt;/p&gt;

&lt;p&gt;For a notification system? Notification B arriving before notification A doesn't break anything. The user sees both.&lt;/p&gt;

&lt;p&gt;What engineers actually want when they say "ordering" is usually &lt;strong&gt;idempotency&lt;/strong&gt; — the ability to process a message twice without sending duplicate notifications. That's not a queue property, that's a consumer design property.&lt;/p&gt;

&lt;p&gt;An idempotent consumer checks: "have I already sent this notification?" If yes, skip. Now you get SQS Standard's throughput with the same safety guarantee — and you're not fighting a 3K/sec ceiling.&lt;/p&gt;

&lt;p&gt;The question to ask before picking FIFO: &lt;em&gt;"What breaks if this message arrives twice or slightly out of order?"&lt;/em&gt; If the answer is "nothing, as long as the consumer handles it" — you don't need FIFO.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Neither Gives You: Replay
&lt;/h2&gt;

&lt;p&gt;Both FIFO and Standard share one critical limitation nobody talks about enough.&lt;/p&gt;

&lt;p&gt;Messages are deleted after consumption.&lt;/p&gt;

&lt;p&gt;You deploy a bug that silently sends wrong notifications for 2 hours. By the time you notice, SQS has deleted every message from that window — consumed and acknowledged, gone. You cannot rewind. You cannot re-process the affected window with fixed code. The data doesn't exist in the queue anymore.&lt;/p&gt;

&lt;p&gt;This is not a theoretical edge case. Silent bugs in notification systems happen. The right response is to replay the message stream with fixed code — SQS makes that impossible.&lt;/p&gt;

&lt;p&gt;Kafka retains messages on disk for a configurable window (7 days by default). Rewind the consumer offset to 2 hours ago, re-process the entire window. Every message that went out wrong gets corrected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision Framework
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;SQS Standard&lt;/th&gt;
&lt;th&gt;SQS FIFO&lt;/th&gt;
&lt;th&gt;Kafka&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Throughput&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;3K/sec/queue&lt;/td&gt;
&lt;td&gt;500K–1M/sec/broker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordering&lt;/td&gt;
&lt;td&gt;Best-effort&lt;/td&gt;
&lt;td&gt;Strict per group&lt;/td&gt;
&lt;td&gt;Strict per partition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery&lt;/td&gt;
&lt;td&gt;At-least-once&lt;/td&gt;
&lt;td&gt;Exactly-once&lt;/td&gt;
&lt;td&gt;At-least-once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replay&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fan-out&lt;/td&gt;
&lt;td&gt;Manual (multiple queues)&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Consumer groups (free)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right for&lt;/td&gt;
&lt;td&gt;Task queues, moderate volume&lt;/td&gt;
&lt;td&gt;Ledgers, ordered state machines&lt;/td&gt;
&lt;td&gt;High-throughput, fan-out, replay&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use Standard when:&lt;/strong&gt; task distribution, idempotent consumers, moderate throughput, operational simplicity is the priority.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use FIFO when:&lt;/strong&gt; you genuinely need strict ordering and exactly-once semantics — financial ledgers, event sourcing — and your peak throughput fits within 3K messages/sec per queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Kafka when:&lt;/strong&gt; you need fan-out to multiple independent consumers, replay capability for failure recovery, or throughput that SQS can't handle without building complexity yourself.&lt;/p&gt;

&lt;p&gt;The wrong answer at scale isn't using SQS Standard when you should have used FIFO. It's using either when your actual requirements — throughput, fan-out, replay — are requirements that Kafka was built for.&lt;/p&gt;

&lt;p&gt;Full notification system case study → &lt;a href="https://leetdezine.com/notification-system/?utm_source=devto" rel="noopener noreferrer"&gt;LeetDezine&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>backend</category>
      <category>aws</category>
    </item>
    <item>
      <title>What Actually Breaks in a URL Shortener Design at Scale?</title>
      <dc:creator>LeetDezine</dc:creator>
      <pubDate>Wed, 29 Apr 2026 05:01:50 +0000</pubDate>
      <link>https://dev.to/leetdezine/what-actually-breaks-in-a-url-shortener-design-at-scale-58n8</link>
      <guid>https://dev.to/leetdezine/what-actually-breaks-in-a-url-shortener-design-at-scale-58n8</guid>
      <description>&lt;p&gt;&lt;a href="https://leetdezine.com/?utm_source=devto" rel="noopener noreferrer"&gt;LeetDezine&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everyone can describe a URL shortener. Write a row to the DB, generate a short code, cache it on reads. The base design fits on a napkin.&lt;/p&gt;

&lt;p&gt;The interesting part is what happens when you push on any one of those steps. Where does it break? Why? And what's the fix that actually holds at scale?&lt;/p&gt;

&lt;p&gt;Here are four traps I've seen candidates walk straight into — each one looks correct on the surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Truncation Trap
&lt;/h2&gt;

&lt;p&gt;A Snowflake ID is 64 bits. You only need 36 bits to cover 50 billion URLs (2^36 = 68 billion). Encode 36 bits in base62 and you get exactly 6 characters. Clean short code, no collision check needed.&lt;/p&gt;

&lt;p&gt;The natural move: drop the rightmost 28 bits, encode what's left. You're keeping the timestamp, which feels like the important part.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ timestamp — 41 bits ][ machine ID — 10 bits ][ sequence — 12 bits ]
 ←────── keep 36 bits ─────────────────────────→ ←── drop 28 bits ──→
                                                  (machine + sequence)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what breaks: the rightmost 28 bits you dropped contain the &lt;strong&gt;sequence number&lt;/strong&gt; — the counter that differentiates two Snowflake IDs generated on the &lt;strong&gt;same server in the same millisecond&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;Request A — server 3, t=1700000001ms, seq=1
keep 36 bits → "x7k2p9"

Request B — server 3, t=1700000001ms, seq=2
keep 36 bits → "x7k2p9"  ✗  collision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only difference between A and B was &lt;code&gt;seq=1&lt;/code&gt; vs &lt;code&gt;seq=2&lt;/code&gt;. You dropped that. At 1000 creations/sec, two requests landing in the same millisecond is not an edge case — it's constant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; you cannot get both uniqueness and shortness by truncating a Snowflake. All three sections — timestamp, machine ID, sequence — contribute to the guarantee. Drop any of them and you break it.&lt;/p&gt;

&lt;p&gt;The options that actually work: accept 11-char codes from the full Snowflake, use random 6-char base62 with a collision check, or pre-generate keys where uniqueness is native to the key size.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Redis INCR Looks Perfect. It Has One Fatal Flaw.
&lt;/h2&gt;

&lt;p&gt;For collision-free short code generation, Redis INCR is elegant. Atomic counter increments — every call returns a unique integer. Base62-encode it. Done. No collision checks, no retries, no background service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INCR url_counter  →  1000000
Base62(1000000)   →  "004C9M"
INSERT short_code = "004C9M"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem has nothing to do with code generation. It's about what sequential codes leak.&lt;/p&gt;

&lt;p&gt;If a user receives &lt;code&gt;yoursite.com/004C9M&lt;/code&gt;, they know the previous URL was &lt;code&gt;yoursite.com/004C9L&lt;/code&gt; and the next will be &lt;code&gt;yoursite.com/004C9N&lt;/code&gt;. They can walk the entire sequence and enumerate every URL in your system.&lt;/p&gt;

&lt;p&gt;For an internal tool, this is fine. For a public shortener — where someone might shorten a pre-announcement, an internal doc, or a private file — it's a privacy violation.&lt;/p&gt;

&lt;p&gt;The second problem: Redis INCR makes Redis a hard dependency on every creation request. Redis down → creation fails immediately, zero fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: KGS + pre-generated key pool.&lt;/strong&gt; A Key Generation Service pre-generates random base62 codes offline and loads them into a Redis list. App servers pop keys with LPOP. Codes are in random order — no enumerability. App servers pre-fetch a local batch of 100 keys, so Redis going down doesn't immediately stop creation.&lt;/p&gt;

&lt;p&gt;Redis INCR is right for internal tools. KGS + pool is right for public shorteners.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. What Actually Happens When Redis Dies
&lt;/h2&gt;

&lt;p&gt;Most failure mode answers in interviews stop at "Redis is replicated." That's a configuration, not a plan.&lt;/p&gt;

&lt;p&gt;Redis absorbs ~80% of all redirect reads. At 1M reads/sec, the DB only sees 200k/sec. Redis dies. 1M reads/sec hits DB nodes sized for 200k/sec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without a circuit breaker&lt;/strong&gt;, every request tries Redis, waits 500ms for the connection timeout, then falls back to DB. At 1M requests/sec, that's your thread pool stalled for 500ms each. No request completes. The DB never gets a clean chance to respond. Total cascade.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis down, no circuit breaker:
Request → try Redis → wait 500ms → timeout → fallback to DB
× 1,000,000 requests/sec
= thread pool exhausted, DB never reached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Circuit breaker&lt;/strong&gt; fixes the timeout overhead. After N failures in T seconds, the circuit opens — requests skip Redis entirely and go straight to DB. Latency rises but the system stays alive.&lt;/p&gt;

&lt;p&gt;Still not enough. DB peak capacity is ~800k reads/sec. The API Gateway throttles the overflow, returning 503 to some percentage of redirect requests. The system degrades — but doesn't collapse.&lt;/p&gt;

&lt;p&gt;The trap that kills most answers: &lt;strong&gt;"auto-scaling handles this."&lt;/strong&gt; A new Postgres replica takes minutes to provision and catch up WAL replication. Your traffic surge is immediate. Auto-scaling is for gradual growth, not cache failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial availability beats total cascade.&lt;/strong&gt; Always.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. LPOP Is Atomic by Architecture, Not by Lock
&lt;/h2&gt;

&lt;p&gt;If you build a pre-generated key pool in Redis, a natural interview question is: "how do you prevent two app servers from popping the same key simultaneously?"&lt;/p&gt;

&lt;p&gt;You don't have to. Redis handles it.&lt;/p&gt;

&lt;p&gt;Redis is single-threaded. Every command executes one at a time. If 20 app servers call LPOP at the exact same millisecond, Redis processes them sequentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App Server 1  →  LPOP  →  "x7k2p9"  (removed)
App Server 2  →  LPOP  →  "k2m8q1"  (removed)
App Server 3  →  LPOP  →  "p9n3r7"  (removed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Physically impossible for two calls to return the same value. Not because of a lock — because of the execution model.&lt;/p&gt;

&lt;p&gt;Compare to a Postgres key pool. You'd need &lt;code&gt;SELECT FOR UPDATE SKIP LOCKED&lt;/code&gt; — row-level locking on every creation request. Expensive, complex, and a bottleneck under high concurrency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The batch pre-fetch makes it even better.&lt;/strong&gt; Each app server grabs 100 keys at startup and refills when empty. At 1k creations/sec across 20 servers, Redis traffic drops from 1000 LPOP calls/sec to ~10 batch refills/sec. 100x reduction. Same correctness guarantee.&lt;/p&gt;

&lt;p&gt;A crashed server loses its local batch — at most 100 keys out of a 100M key pool. That's 0.0001%. Don't bother recovering them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pattern Across All Four
&lt;/h2&gt;

&lt;p&gt;Each trap comes from a decision that's locally correct but breaks a property you assumed was safe: uniqueness, privacy, fault tolerance, atomicity.&lt;/p&gt;

&lt;p&gt;The way to catch these in an interview isn't to memorize solutions. It's to ask "what does this break?" for every component you add.&lt;/p&gt;

&lt;p&gt;The full URL shortener case study walks through every deep dive in this sequence — requirements, estimation, base architecture, then each failure mode in detail:&lt;/p&gt;

&lt;p&gt;For in depth analysis check →  &lt;a href="https://leetdezine.com/?utm_source=devto" rel="noopener noreferrer"&gt;LeetDezine&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>backend</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why Is Redis INCR a Bad Fit for a Public URL Shortener?</title>
      <dc:creator>LeetDezine</dc:creator>
      <pubDate>Thu, 23 Apr 2026 17:18:06 +0000</pubDate>
      <link>https://dev.to/leetdezine/url-shortener-traps-that-look-correct-until-they-break-2o8g</link>
      <guid>https://dev.to/leetdezine/url-shortener-traps-that-look-correct-until-they-break-2o8g</guid>
      <description>&lt;p&gt;&lt;a href="https://leetdezine.com/?utm_source=devto" rel="noopener noreferrer"&gt;LeetDezine&lt;/a&gt;&lt;br&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%2Fp3plm3uket9rftps6qq9.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%2Fp3plm3uket9rftps6qq9.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Redis INCR is one of those solutions that looks perfect the first time you see it. Atomic counter increments. Every call returns a unique integer. Base62-encode it and you have a short code — zero collision checks, zero retries, no background service.&lt;/p&gt;

&lt;p&gt;It's cleaner than anything else on the board. So why does every serious URL shortener reject it?&lt;/p&gt;

&lt;p&gt;The answer has nothing to do with code generation.&lt;/p&gt;


&lt;h2&gt;
  
  
  How Redis INCR Works (And Why It's Technically Correct)
&lt;/h2&gt;

&lt;p&gt;The mechanics are clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Creation request arrives
→ Redis: INCR url_counter → returns 1000000
→ Base62 encode 1000000:

  Divide repeatedly, collect remainders, stop when quotient = 0:

  1000000 ÷ 62 = 16129  remainder 22 → 'M'  (quotient != 0, keep going)
  16129   ÷ 62 = 260    remainder 9  → '9'  (quotient != 0, keep going)
  260     ÷ 62 = 4      remainder 12 → 'C'  (quotient != 0, keep going)
  4       ÷ 62 = 0      remainder 4  → '4'  (quotient = 0, stop)

  Read remainders bottom to top: "4C9M" → pad to 6 chars → "004C9M"

→ INSERT short_code = "004C9M"
→ Done.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis is single-threaded. &lt;code&gt;INCR&lt;/code&gt; is atomic — it increments and returns in a single operation. Two simultaneous calls always get different values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App server 1: INCR → 1000000
App server 2: INCR → 1000001  ← different, guaranteed
App server 3: INCR → 1000002  ← different, guaranteed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No race condition. No collision. No retry loop. Encoding a unique number always produces a unique code. The math is correct.&lt;/p&gt;

&lt;p&gt;So what's the problem?&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1 — Sequential Codes Are a Privacy Violation
&lt;/h2&gt;

&lt;p&gt;Counter values are sequential. If your user receives &lt;code&gt;yoursite.com/004C9M&lt;/code&gt;, they immediately know:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yoursite.com/004C9L  ← previous URL, someone else's
yoursite.com/004C9N  ← next URL, someone else's
yoursite.com/004C9K  ← keep going...
yoursite.com/004C9J  ← and going...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They can walk the entire database. Every URL in your system is discoverable by incrementing one character.&lt;/p&gt;

&lt;p&gt;For an internal tool where all users are trusted, this might be fine. For a public shortener — where someone might shorten a pre-announcement link, an internal doc, a private file, a personal photo album — it's a real privacy violation. Your users have a reasonable expectation that their short link isn't guessable.&lt;/p&gt;

&lt;p&gt;Sequential codes make that expectation impossible to satisfy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2 — Redis Becomes a Hard Dependency on Every Creation
&lt;/h2&gt;

&lt;p&gt;With INCR, the hot path looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request → INCR Redis → encode → INSERT DB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis is in the critical path of every single URL creation. If Redis goes down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis down
→ INCR fails
→ No counter value
→ Creation fails immediately
→ Zero fallback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no graceful degradation. No buffer. No local state to drain. The moment Redis is unreachable, your creation endpoint returns errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: KGS + Pre-Generated Key Pool
&lt;/h2&gt;

&lt;p&gt;The Key Generation Service approach flips the model. Instead of generating a key at request time, keys are generated in advance and stored in a Redis pool. When a request arrives, the app server just pops one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before any request arrives:
→ KGS generates random base62 codes offline
→ Loads them into Redis list (RPUSH)

When creation request arrives:
→ App server pops key from local batch
→ INSERT into DB
→ Done — zero Redis call on hot path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why LPOP is atomic:&lt;/strong&gt; Redis is single-threaded. Even if 20 app servers call LPOP at the same millisecond, Redis processes them one at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App server 1: LPOP → "x7k2p9" (removed)
App server 2: LPOP → "k2m8q1" (removed)
App server 3: LPOP → "p9n3r7" (removed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Physically impossible for two LPOP calls to return the same key. No locks needed. No &lt;code&gt;SELECT FOR UPDATE&lt;/code&gt;. Atomicity comes from the architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The batch pre-fetch:&lt;/strong&gt; Each app server grabs 100 keys at startup and keeps them in local memory. At 1k creations/sec across 20 servers, Redis traffic drops from 1000 LPOP/sec to ~10 batch refills/sec. 100x reduction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App server starts:
→ LPOP 100 keys → store in local queue

Creation request:
→ Pop from local queue (zero network call)
→ Queue empty → refill from Redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this fixes for Redis failure:&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;Redis down
→ App servers drain local batch (100 keys × 20 servers = 2000 keys)
→ At 1k creations/sec → ~2 seconds of local runway
→ Circuit breaker engages, Redis recovers
→ Graceful degradation instead of hard failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Side by Side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Redis INCR&lt;/th&gt;
&lt;th&gt;KGS + Pool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Collision checks&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code predictability&lt;/td&gt;
&lt;td&gt;Sequential — enumerable&lt;/td&gt;
&lt;td&gt;Random — private&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis failure&lt;/td&gt;
&lt;td&gt;Creation fails instantly&lt;/td&gt;
&lt;td&gt;Local batch buys time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational cost&lt;/td&gt;
&lt;td&gt;Very simple&lt;/td&gt;
&lt;td&gt;Small background worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right for&lt;/td&gt;
&lt;td&gt;Internal tools&lt;/td&gt;
&lt;td&gt;Public URL shortener&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;Redis INCR fails not because of what it does, but because of what it leaks. Sequential uniqueness and privacy are in direct conflict. You can't have both with a counter.&lt;/p&gt;

&lt;p&gt;The KGS + pool approach keeps the "no collision checks, no retries" guarantee while adding randomness and resilience. The operational cost is a 50-line background worker and one metric to monitor. The privacy and fault tolerance gains are worth it for any public-facing system.&lt;/p&gt;

&lt;p&gt;The full URL shortener case study — including requirements, DB design, caching, peak traffic, and every failure mode — is at:&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://leetdezine.com/?utm_source=devto" rel="noopener noreferrer"&gt;https://leetdezine.com/?utm_source=devto&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>backend</category>
      <category>redis</category>
    </item>
    <item>
      <title>Why Random UUIDs are Killing Your Database Performance</title>
      <dc:creator>LeetDezine</dc:creator>
      <pubDate>Mon, 20 Apr 2026 10:15:57 +0000</pubDate>
      <link>https://dev.to/leetdezine/why-random-uuids-are-killing-your-database-performance-h59</link>
      <guid>https://dev.to/leetdezine/why-random-uuids-are-killing-your-database-performance-h59</guid>
      <description>&lt;p&gt;Every developer starts with a UUID. It’s the industry standard for a reason: zero coordination, zero DB checks, and zero single point of failure. Any machine can generate one and be 100% sure it’s unique.&lt;/p&gt;

&lt;p&gt;But as your system scales, that "standard" choice starts to hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: UUIDs vs. Databases
&lt;/h2&gt;

&lt;p&gt;If you're using &lt;strong&gt;UUID v4&lt;/strong&gt; (completely random), you're essentially handing your database a grenade. &lt;/p&gt;

&lt;p&gt;Because the IDs are random, every new insert lands in a random spot in your B-Tree index. This causes &lt;strong&gt;page splits&lt;/strong&gt;, fragments your storage, and slows down your writes as the table grows. Plus, at 128 bits (16 bytes), they're twice as large as a standard &lt;code&gt;BIGINT&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Evolution of ID Generation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Single Server Counter:&lt;/strong&gt; Simple, but if the server dies, your ID generation stops (SPOF).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;UUID v4:&lt;/strong&gt; Globally unique, but random and huge. No time-sortability.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;UUID v7:&lt;/strong&gt; The modern middle ground. It's still 16 bytes, but it's &lt;strong&gt;time-sortable&lt;/strong&gt;, which fixes the database page-split problem.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Ticket Server (Redis):&lt;/strong&gt; Centralized counter. Fast, but now your ID generation depends on Redis availability.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Snowflake IDs:&lt;/strong&gt; The "Big Tech" solution (used by Twitter, Discord, and Instagram).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why Snowflake Wins
&lt;/h3&gt;

&lt;p&gt;Snowflake IDs pack everything you need into just &lt;strong&gt;64 bits (8 bytes)&lt;/strong&gt;. They fit perfectly into a standard &lt;code&gt;BIGINT&lt;/code&gt;, making them fast to index and easy to store.&lt;/p&gt;

&lt;p&gt;Here is the breakdown of how those 64 bits are structured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;1 bit (Sign):&lt;/strong&gt; Always 0 (keeps the number positive).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;41 bits (Timestamp):&lt;/strong&gt; Milliseconds since a custom epoch. This gives you ~69 years of IDs and makes them &lt;strong&gt;natively time-sortable&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;10 bits (Machine ID):&lt;/strong&gt; Allows up to 1,024 independent nodes to generate IDs simultaneously without talking to each other.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;12 bits (Sequence):&lt;/strong&gt; A counter for IDs generated in the same millisecond on the same machine (up to 4,096 IDs/ms).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;UUID v4&lt;/th&gt;
&lt;th&gt;UUID v7&lt;/th&gt;
&lt;th&gt;Snowflake&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;64-bit&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sortable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coordination&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ None&lt;/td&gt;
&lt;td&gt;✅ None&lt;/td&gt;
&lt;td&gt;✅ None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB Friendly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Best&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Which one should you choose?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Quick Prototypes:&lt;/strong&gt; Stick with &lt;strong&gt;UUID v4&lt;/strong&gt;. It’s easy and requires zero setup.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Modern Web Apps:&lt;/strong&gt; Move to &lt;strong&gt;UUID v7&lt;/strong&gt;. You get the simplicity of UUIDs with the performance of time-sortable IDs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;High-Scale Systems:&lt;/strong&gt; Go with &lt;strong&gt;Snowflake&lt;/strong&gt;. When every byte and every millisecond of database latency matters, 64-bit sortable IDs are the only way to go.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Golden Rule:&lt;/strong&gt; You can't just "trim" a UUID to make it shorter. Trimming 128 bits down to 6 characters for a "short link" throws away 92 bits of entropy, turning a global guarantee into a collision nightmare.&lt;/p&gt;

&lt;p&gt;For a full deep dive into the math and architecture behind distributed IDs, check out the case study at &lt;a href="https://leetdezine.com/03-Case-Studies/01-Foundation/01-Unique-ID-Generator/" rel="noopener noreferrer"&gt;LeetDezine&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>snowflake</category>
      <category>distributedsystems</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
