<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Defragment Your Brain: Why Every Developer Needs an Analog Break]]></title><description><![CDATA[Defragment Your Brain: Why Every Developer Needs an Analog Break]]></description><link>https://toeday.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Defragment Your Brain: Why Every Developer Needs an Analog Break</title><link>https://toeday.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 07 Sep 2026 09:31:53 GMT</lastBuildDate><atom:link href="https://toeday.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Under the Hood of Database Engines: B-Trees vs. LSM-Trees Explained]]></title><description><![CDATA[When building high-throughput systems, choosing the right database is one of the most critical architecture decisions you will make. However, developers often evaluate databases based solely on query ]]></description><link>https://toeday.hashnode.dev/under-the-hood-of-database-engines-b-trees-vs-lsm-trees-explained</link><guid isPermaLink="true">https://toeday.hashnode.dev/under-the-hood-of-database-engines-b-trees-vs-lsm-trees-explained</guid><dc:creator><![CDATA[happyday]]></dc:creator><pubDate>Tue, 28 Jul 2026 03:03:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a6813635e3fdf7d560f2bbf/43a74c6a-9b2c-42d5-964f-ea932bd00d89.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When building high-throughput systems, choosing the right database is one of the most critical architecture decisions you will make. However, developers often evaluate databases based solely on query syntax or feature lists, overlooking the most fundamental component: the storage engine.At the core of almost every modern database—from PostgreSQL and MySQL to Cassandra and RocksDB—is a data structure optimized either for read-heavy or write-heavy workloads.In this deep dive, we will unpack the internal mechanics of the two dominant storage engine paradigms: B-Trees and Log-Structured Merge-Trees (LSM-Trees).1. The Core Bottleneck: Disk I/O MechanicsTo understand why database engines are designed the way they are, we must look at physical storage hardware (HDDs and NVMe SSDs).Sequential I/O is significantly faster than Random I/O.SSD Wear &amp; Amplification: Random writes cause block erasure overhead (Garbage Collection) on SSDs, reducing hardware lifespan and increasing latency spikes.Storage engines are fundamentally trade-off engines designed to manage this disk I/O reality.2. B-Trees: The In-Place Update ParadigmIntroduced by Rudolf Bayer and Edward M. McCreight in 1970, B-Trees (and their variants like \(B^+\)-Trees) remain the default storage engine structure for traditional relational databases like PostgreSQL (InnoDB/Heap) and MySQL.Architectural LayoutA B-Tree breaks the database down into fixed-size pages (typically 4KB to 16KB) and maps directly to the underlying hardware disk blocks. [ Root Page ] /<br />[ Internal Page ] [ Internal Page ] / | \ |<br />[Leaf] [Leaf] [Leaf] [Leaf] [Leaf] (Contains actual rows/pointers) Pages: The tree consists of Root Pages, Internal Pages, and Leaf Pages.In-Place Updates: When a row is updated, the engine overwrites the specific page on disk containing that record.\(B^+\)-Tree Variation: Leaf pages form a doubly linked list, enabling highly efficient sequential range scans (\(O(\log N)\) point lookups + sequential traversal).The Trade-offPros (Fast Reads): Point lookups require traversing only \(O(\log N)\) depth. Since pages are balanced, read latency is deterministic and low.Cons (Random Write Overhead): Every insert/update requires modifying random pages on disk. To prevent corruption during crashes, B-Trees require a Write-Ahead Log (WAL), meaning every write is written twice (once to the WAL sequentially, once to the page randomly).3. LSM-Trees: The Append-Only Paradigmpopularized by Google’s Bigtable paper and used in Cassandra, ScyllaDB, and RocksDB, the Log-Structured Merge-Tree (LSM-Tree) turns random writes into sequential writes.Architectural ComponentsInstead of overwriting data in place, LSM-Trees treat disk writes as append-only immutable logs.[ Write ] ──&gt; 1. WAL (Disk, Sequential) │ ▼ 2. MemTable (In-Memory RAM Buffer / Red-Black Tree) │ ▼ (Flush when full) 3. SSTables (Sorted String Tables on Disk) [ Level 0 ] ──(Compaction)──&gt; [ Level 1 ] ──&gt; [ Level 2 ] MemTable: An in-memory write buffer (usually implemented as a SkipList or Red-Black Tree). Incoming writes (INSERT, UPDATE, DELETE) are appended here in sorted order.SSTable (Sorted String Table): When the MemTable reaches capacity, it is flushed to disk as an immutable, sorted file called an SSTable.Compaction: Background threads merge overlapping SSTables, removing deleted/overwritten records to reclaim disk space and keep read performance manageable.The Trade-offPros (Blazing Fast Writes): Writes append sequentially to the WAL and MemTable in RAM, bypassing random disk I/O entirely.Cons (Read &amp; Compaction Overhead): To read a key, the database must check the MemTable, then search down through multiple SSTable levels. Databases use Bloom Filters in RAM to avoid reading SSTables that do not contain the target key.4. Deep Comparison: B-Tree vs. LSM-TreeFeatureB-Tree (B+-Tree)LSM-TreePrimary WorkloadRead-heavy / Transactional (OLTP)Write-heavy / Ingestion / Time-seriesWrite MechanismIn-place update (Random Disk I/O)Append-only (Sequential Disk I/O)Read Complexity\(O(\log N)\) (Direct page lookup)Variable (MemTable + Bloom Filter + SSTables)Space AmplificationMedium (Fragmented pages/padding)Variable (Increases until compaction runs)Write AmplificationHigh (WAL + Page Overwrites)Medium-High (Repeated Compaction cycles)ACID SuitabilityExcellent for strong isolation levelsRequires extra concurrency controlExamplesPostgreSQL, MySQL (InnoDB), SQLiteRocksDB, Apache Cassandra, LevelDB5. Decision Framework: Which One Should You Choose?When designing system architecture, use this rule of thumb:Choose a B-Tree Engine (e.g., PostgreSQL, MySQL) if your application requires strict ACID transactions, complex join queries, and predictable point-read performance (e.g., e-commerce order management, financial ledgers).Choose an LSM-Tree Engine (e.g., Cassandra, RocksDB) if your workload is append-heavy, involves massive log/metric ingestion, or demands maximum write throughput with high availability (e.g., IoT data streams, messaging apps, event logging).ConclusionNeither structure is inherently superior; each represents a different compromise between read amplification, write amplification, and space amplification (the RUM Conjecture).Understanding these underlying storage mechanics allows you to select database technologies based on hardware realities rather than marketing <a href="https://www.ichizenn.com/koi-mikuji/">恋みくじ</a> hype.</p>
]]></content:encoded></item><item><title><![CDATA[Designing a High-Throughput Distributed Rate Limiter: From Token Bucket to Redis Lua Scripts]]></title><description><![CDATA[Rate limiting is a foundational building block for modern web scale architectures. Whether you are protecting an internal service from cascading failures, enforcing API monetization tiers, or mitigati]]></description><link>https://toeday.hashnode.dev/designing-a-high-throughput-distributed-rate-limiter-from-token-bucket-to-redis-lua-scripts</link><guid isPermaLink="true">https://toeday.hashnode.dev/designing-a-high-throughput-distributed-rate-limiter-from-token-bucket-to-redis-lua-scripts</guid><dc:creator><![CDATA[happyday]]></dc:creator><pubDate>Tue, 28 Jul 2026 02:54:45 GMT</pubDate><content:encoded><![CDATA[<p>Rate limiting is a foundational building block for modern web scale architectures. Whether you are protecting an internal service from cascading failures, enforcing API monetization tiers, or mitigating distributed denial-of-service (DDoS) attacks, a robust rate limiter is essential.</p>
<p>In this deep dive, we will explore the core mechanics of rate limiting algorithms, the trade-offs of distributed implementations, and how to build a production-ready, race-condition-free distributed rate limiter using Go, Redis, and atomic Lua scripts.</p>
<ol>
<li>Why Naive Rate Limiting Fails in Distributed Systems In a single-instance architecture, rate limiting is straightforward. You can keep an in-memory counter using atomic operations or a localized mutex. However, modern infrastructure relies on horizontal scaling: multiple API gateways or microservices behind a load balancer.</li>
</ol>
<p>[ Client Request ] ───&gt; [ Load Balancer ] │ ┌────────────────┴────────────────┐ ▼ ▼ [ API Gateway Instance A ] [ API Gateway Instance B ] │ │ └───────────────┬─────────────────┘ ▼ [ Shared Storage / Redis ] When rate limiting state is distributed across multiple gateway nodes, two main problems arise:</p>
<p>Inconsistent State: If Instance A tracks requests independently of Instance B, a client can easily bypass the limit by hitting different instances.</p>
<p>Race Conditions (Check-Then-Set Bug): If instances fetch a counter from a shared cache (like Redis), increment it in memory, and write it back, concurrent requests will lead to race conditions where requests are under-counted.</p>
<p>To solve this, we need atomic execution and centralized state management.</p>
<ol>
<li>Comparing Core Rate Limiting Algorithms Before implementing our distributed solution, let's compare the four primary algorithms used in system design:</li>
</ol>
<p>A. Token Bucket Mechanism: Tokens are added to a "bucket" at a constant fill rate. Each incoming request consumes one token. If the bucket is empty, the request is rejected.</p>
<p>Pros: Handles bursts of traffic gracefully up to the maximum bucket capacity.</p>
<p>Cons: Memory state requires tracking both timestamps and token counts.</p>
<p>B. Leaky Bucket Mechanism: Requests enter a FIFO queue (the bucket) and leave at a fixed, constant rate. If the queue overflows, new requests are dropped.</p>
<p>Pros: Smooths out traffic spikes; guarantees a uniform outflow rate.</p>
<p>Cons: Bursts are delayed rather than processed immediately; can increase latency.</p>
<p>C. Fixed Window Counter Mechanism: Time is divided into fixed windows (e.g., 1-minute intervals). A simple counter increments for each request in the current window.</p>
<p>Pros: Memory efficient and easy to implement.</p>
<p>Cons: Traffic spikes at the window boundaries (e.g., 59th second and 1st second) can allow up to 2x the allowed requests in a short burst.</p>
<p>D. Sliding Window Counter (Best Balance) Mechanism: Combines the current window counter with the weighted counter of the previous window to estimate the current rate.</p>
<p>Pros: Extremely memory-efficient with high accuracy; prevents boundary bursts.</p>
<p>Cons: Slightly more complex math computation.</p>
<ol>
<li>Solving Race Conditions with Redis &amp; Lua Scripts To implement a Sliding Window Counter or Token Bucket across multiple API gateway nodes without locking overhead, we can offload the rate limiting logic directly into Redis using a Lua script.</li>
</ol>
<p>Redis guarantees that Lua scripts execute atomically. No other Redis command will run while a script is executing, eliminating race conditions entirely without requiring distributed locks (like Redlock).</p>
<p>The Atomic Token Bucket Lua Script Here is an optimized Lua script implementing the Token Bucket algorithm in Redis:</p>
<p>Lua -- KEYS[1]: Rate limit key (e.g., "rate_limit:user_123") -- ARGV[1]: Max bucket capacity (burst limit) -- ARGV[2]: Refill rate per second -- ARGV[3]: Current Unix timestamp (in seconds) -- ARGV[4]: Requested tokens (usually 1)</p>
<p>local key = KEYS[1] local capacity = tonumber(ARGV[1]) local fill_rate = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local requested = tonumber(ARGV[4])</p>
<p>-- Fetch current state from Redis local data = redis.call("HMGET", key, "tokens", "last_updated") local tokens = tonumber(data[1]) local last_updated = tonumber(data[2])</p>
<p>if tokens == nil then -- First request, initialize bucket at full capacity tokens = capacity last_updated = now else -- Calculate leaked/refilled tokens since last request local delta = math.max(0, now - last_updated) tokens = math.min(capacity, tokens + (delta * fill_rate)) last_updated = now end</p>
<p>-- Check if enough tokens exist if tokens &gt;= requested then tokens = tokens - requested redis.call("HMSET", key, "tokens", tokens, "last_updated", last_updated) -- Set TTL to auto-cleanup inactive keys (capacity / fill_rate) redis.call("EXPIRE", key, math.ceil(capacity / fill_rate) * 2) return {1, math.floor(tokens)} -- Allowed: 1, Remaining tokens else redis.call("HMSET", key, "tokens", tokens, "last_updated", last_updated) return {0, math.floor(tokens)} -- Denied: 0, Remaining tokens end 4. Go Implementation Now let's wrap this in a clean, idiomatic Go package using go-redis.</p>
<p>Go package main</p>
<p>import ( "context" "context" "fmt" "time"</p>
<pre><code class="language-plaintext">"github.com/redis/go-redis/v9"
</code></pre>
<p>)</p>
<p>const rateLimitScript = ` local key = KEYS[1] local capacity = tonumber(ARGV[1]) local fill_rate = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local requested = tonumber(ARGV[4])</p>
<p>local data = redis.call("HMGET", key, "tokens", "last_updated") local tokens = tonumber(data[1]) local last_updated = tonumber(data[2])</p>
<p>if tokens == nil then tokens = capacity last_updated = now else local delta = math.max(0, now - last_updated) tokens = math.min(capacity, tokens + (delta * fill_rate)) last_updated = now end</p>
<p>if tokens &gt;= requested then tokens = tokens - requested redis.call("HMSET", key, "tokens", tokens, "last_updated", last_updated) redis.call("EXPIRE", key, math.ceil(capacity / fill_rate) * 2) return {1, math.floor(tokens)} else redis.call("HMSET", key, "tokens", tokens, "last_updated", last_updated) return {0, math.floor(tokens)} end `</p>
<p>type Limiter struct { client *redis.Client sha string }</p>
<p>func NewLimiter(client *redis.Client) (*Limiter, error) { ctx := context.Background() sha, err := client.ScriptLoad(ctx, rateLimitScript).Result() if err != nil { return nil, fmt.Errorf("failed to load Lua script: %w", err) } return &amp;Limiter{client: client, sha: sha}, nil }</p>
<p>func (l *Limiter) Allow(ctx context.Context, identifier string, capacity int, fillRate float64) (bool, int, error) { key := fmt.Sprintf("ratelimit:%s", identifier) now := time.Now().Unix()</p>
<pre><code class="language-plaintext">res, err := l.client.EvalSha(ctx, l.sha, []string{key}, capacity, fillRate, now, 1).Result()
if err != nil {
	return false, 0, err
}

results, ok := res.([]interface{})
if !ok || len(results) &lt; 2 {
	return false, 0, fmt.Errorf("invalid response from Redis")
}

allowed := results[0].(int64) == 1
remaining := int(results[1].(int64))

return allowed, remaining, nil
</code></pre>
<p>}</p>
<p>func main() { rdb := redis.NewClient(&amp;redis.Options{ Addr: "localhost:6379", })</p>
<pre><code class="language-plaintext">limiter, err := NewLimiter(rdb)
if err != nil {
	panic(err)
}

ctx := context.Background()
userID := "user_42"

// Allow burst of 10 requests, refilling at 2 requests/sec
for i := 1; i &lt;= 12; i++ {
	allowed, remaining, _ := limiter.Allow(ctx, userID, 10, 2.0)
	fmt.Printf("Request %2d: Allowed = %-5v | Remaining Tokens = %d\n", i, allowed, remaining)
}
</code></pre>
<p>} 5. Resilience Considerations and Best Practices Deploying a rate limiter into high-availability production environments introduces edge-case vulnerabilities that require careful architecting:</p>
<ol>
<li>Redis High Availability &amp; Fail-Open Behavior If your Redis cluster experiences latency or a node failure, should your rate limiter fail open or fail closed?</li>
</ol>
<p>Fail-Open (Recommended for Most APIs): If Redis times out, allow requests through and log a metric alert. This preserves user experience at the risk of brief backend overload.</p>
<p>Fail-Closed: Necessary for critical billing endpoints or non-scalable microservices where backend crash prevention is priority #1.</p>
<ol>
<li><p>Clock Synchronization The Token Bucket Lua script relies on Unix Timestamp. Ensure all app instances and Redis nodes use Network Time Protocol (NTP) to prevent clock drift from distorting the rate limiter calculations.</p>
</li>
<li><p>HTTP Header Standards Always communicate limit status clearly to API consumers by returning standardized HTTP headers (RFC 6585):</p>
</li>
</ol>
<p>HTTP HTTP/1.1 429 Too Many Requests Retry-After: 30 X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1672531200 Conclusion Building a high-performance distributed rate limiter requires balancing state centralization, execution speed, and resilience. By leveraging Redis Lua scripting, you achieve atomic execution and microsecond latency without distributed locking bottlenecks.<a href="https://www.ichizenn.com/koi-mikuji/">恋みくじ</a></p>
<p>As your system scales, consider tiering your rate limits (e.g., IP-level limits at Cloudflare/Envoy CDN, and user-level business logic limits at the API Gateway layer) for multi-layered system defense.</p>
]]></content:encoded></item></channel></rss>