<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Concurrency on</title><link>/tags/concurrency/</link><description>Recent content in Concurrency on</description><generator>Hugo</generator><language>en</language><lastBuildDate>Fri, 20 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="/tags/concurrency/index.xml" rel="self" type="application/rss+xml"/><item><title>Lesson 28: Production Concurrency Architecture — Putting it all together</title><link>/post/go/go-concurrency-production-architecture/</link><pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-production-architecture/</guid><description>&lt;p&gt;This is the lesson I wish existed when I started. Not &amp;ldquo;here&amp;rsquo;s how channels work&amp;rdquo; or &amp;ldquo;here&amp;rsquo;s a simple worker pool&amp;rdquo; — but the full picture: how you take everything you&amp;rsquo;ve learned about goroutines, channels, contexts, rate limiting, idempotency, observability, and graceful shutdown and assemble it into a service that actually belongs in production. One that handles failures, recovers gracefully, doesn&amp;rsquo;t leak resources, and gives you the visibility to understand what it&amp;rsquo;s doing.&lt;/p&gt;</description></item><item><title>Lesson 27: Idempotency in Concurrent Systems — Assume everything runs twice</title><link>/post/go/go-concurrency-idempotency/</link><pubDate>Sun, 08 Feb 2026 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-idempotency/</guid><description>&lt;p&gt;Every distributed system I&amp;rsquo;ve worked on eventually ran something twice. A retry after a timeout. A duplicate webhook delivery. A job that got processed by two workers simultaneously because of a clock skew issue in the claim timeout logic. A user who double-clicked a submit button. Systems aren&amp;rsquo;t gentle about this — they don&amp;rsquo;t ask &amp;ldquo;are you sure?&amp;rdquo; before running your code again. They just run it. The question isn&amp;rsquo;t whether your code will ever run twice. It&amp;rsquo;s whether running it twice causes a problem.&lt;/p&gt;</description></item><item><title>Lesson 26: Safe Background Jobs in Web Servers — Don''t spawn goroutines in handlers</title><link>/post/go/go-concurrency-safe-background-jobs/</link><pubDate>Fri, 23 Jan 2026 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-safe-background-jobs/</guid><description>&lt;p&gt;The first time I saw &lt;code&gt;go func()&lt;/code&gt; inside an HTTP handler, I thought it was clever. Fire off the slow work in the background, respond to the client fast, everybody wins. Then I watched what happened when we deployed a new version: the server started shutting down, half the background goroutines got killed mid-operation, we had partial writes in the database and no way to know which ones had completed. The &amp;ldquo;clever&amp;rdquo; optimization had created a correctness nightmare. The real problem wasn&amp;rsquo;t the goroutine — it was that it was invisible to the server&amp;rsquo;s shutdown lifecycle.&lt;/p&gt;</description></item><item><title>Lesson 25: Observability for Concurrency — You can''t fix what you can''t see</title><link>/post/go/go-concurrency-observability/</link><pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-observability/</guid><description>&lt;p&gt;The goroutine leak doesn&amp;rsquo;t announce itself. It just slowly inflates your memory graph — 50MB, 80MB, 150MB — until either your alerting fires or the OOM killer shows up. Then you&amp;rsquo;re staring at a core dump or (if you&amp;rsquo;re lucky) a live process trying to figure out which of the 10,000 goroutines running in your service is the culprit. I&amp;rsquo;ve been there. It&amp;rsquo;s not fun. The difference between spending 20 minutes diagnosing a leak and spending 4 hours diagnosing a leak is almost entirely whether you invested in observability before the incident.&lt;/p&gt;</description></item><item><title>Lesson 24: Testing Concurrent Code — Flaky tests mean flaky design</title><link>/post/go/go-concurrency-testing/</link><pubDate>Tue, 30 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-testing/</guid><description>&lt;p&gt;A flaky test is a lie your codebase tells you. It says &amp;ldquo;this sometimes works and sometimes doesn&amp;rsquo;t&amp;rdquo; and if you&amp;rsquo;re honest with yourself, you already know what that means: there&amp;rsquo;s a race condition somewhere, and the test is just unlucky enough to expose it occasionally. I used to mark flaky tests with &lt;code&gt;t.Skip(&amp;quot;flaky, fix later&amp;quot;)&lt;/code&gt; and move on. I stopped doing that when a race condition that a flaky test was hinting at caused a double-charge bug in production. Now I treat every flaky test as a production incident waiting to happen.&lt;/p&gt;</description></item><item><title>Lesson 23: Distributed vs Local Concurrency — Channels don''t cross process boundaries</title><link>/post/go/go-concurrency-distributed/</link><pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-distributed/</guid><description>&lt;p&gt;There&amp;rsquo;s a particular kind of confidence that comes after you&amp;rsquo;ve spent a few months writing Go. You&amp;rsquo;ve learned channels, you understand the happens-before guarantees, you&amp;rsquo;ve built a worker pool that hums along beautifully. Then someone says &amp;ldquo;we need to scale this across multiple pods&amp;rdquo; and you think: easy, I&amp;rsquo;ll just make the channels bigger. That thought has caused more production incidents than I care to remember — including one where we lost about 3,000 job records because a pod restarted mid-processing and nobody had thought about what &amp;ldquo;at-least-once delivery&amp;rdquo; actually means.&lt;/p&gt;</description></item><item><title>Lesson 22: Rate Limiting and Load Shedding — Say no before you fall over</title><link>/post/go/go-concurrency-rate-limiting/</link><pubDate>Wed, 10 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-rate-limiting/</guid><description>&lt;p&gt;The most honest thing a server can do is say &amp;ldquo;no.&amp;rdquo; Not crash, not time out after 30 seconds, not queue work indefinitely until memory explodes — just return a clean 503 and let the caller decide what to do. I spent a long time thinking rate limiting was about protecting &lt;em&gt;users&lt;/em&gt; from themselves. Then I got paged at midnight because a misbehaving client hammered an endpoint, the service queued everything politely, RAM climbed to the ceiling, and the process died. The fix wasn&amp;rsquo;t more memory. It was teaching the service to refuse work it couldn&amp;rsquo;t handle.&lt;/p&gt;</description></item><item><title>Lesson 21: Supervisor Patterns — Let it crash, then restart it</title><link>/post/go/go-concurrency-supervisor-patterns/</link><pubDate>Tue, 25 Nov 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-supervisor-patterns/</guid><description>&lt;p&gt;Erlang got famous for the &amp;ldquo;let it crash&amp;rdquo; philosophy. The idea is that trying to handle every possible error in every possible place produces fragile, complicated code. It&amp;rsquo;s often better to let a process crash cleanly and have a supervisor restart it. The supervisor knows how to bring the process back to a known-good state. The process itself just needs to do its job and fail fast when something&amp;rsquo;s wrong.&lt;/p&gt;</description></item><item><title>Lesson 20: Profiling Contention — pprof knows where your goroutines sleep</title><link>/post/go/go-concurrency-contention-profiling/</link><pubDate>Thu, 13 Nov 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-contention-profiling/</guid><description>&lt;p&gt;You&amp;rsquo;ve added goroutines, you&amp;rsquo;ve got worker pools, you&amp;rsquo;ve written careful concurrent code — and the service is still slower than expected. Maybe goroutine count is climbing in your metrics dashboard. Maybe p99 latency has a long tail you can&amp;rsquo;t explain. Maybe a throughput test plateaus at 40% of what you thought the hardware should support.&lt;/p&gt;
&lt;p&gt;This is where guessing stops and profiling starts. Go ships world-class concurrency profiling tools in the standard library — mutex profiles, block profiles, goroutine dumps, and the execution tracer. Most engineers know about the CPU and memory profiler. Far fewer use the concurrency-specific profiles, which is a shame because they find the exact thing that&amp;rsquo;s wrong in minutes instead of days.&lt;/p&gt;</description></item><item><title>Lesson 19: Ordering vs Throughput — You can have fast or ordered, pick one</title><link>/post/go/go-concurrency-ordering-throughput/</link><pubDate>Fri, 31 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-ordering-throughput/</guid><description>&lt;p&gt;Here&amp;rsquo;s a tension that comes up in almost every real concurrent system: you want to process things fast, which means doing them in parallel, but the output needs to come out in the same order the input arrived. These two goals are in direct conflict. Parallel execution means things finish in unpredictable order. Ordered output means you have to wait for the slowest thing in each batch.&lt;/p&gt;
&lt;p&gt;The engineers who understand this tension build systems that make the trade-off explicitly. The engineers who don&amp;rsquo;t notice it build systems that either throttle themselves to single-goroutine throughput &amp;ldquo;to preserve order&amp;rdquo; or silently return results in the wrong order and wonder why tests fail non-deterministically.&lt;/p&gt;</description></item><item><title>Lesson 18: Backpressure Design — Slow down or blow up</title><link>/post/go/go-concurrency-backpressure/</link><pubDate>Sun, 19 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-backpressure/</guid><description>&lt;p&gt;Every system has a throughput ceiling. The question isn&amp;rsquo;t whether your service can be overwhelmed — it can. The question is what happens when it is. Does it slow down gracefully, maintaining correctness and giving the caller a clear signal? Or does it blow up — OOM-killed, goroutines piling up, latency spiking to infinity while requests pile up in an unbounded queue?&lt;/p&gt;
&lt;p&gt;Backpressure is the mechanism that answers that question. It&amp;rsquo;s how a component communicates to its upstream: &amp;ldquo;I&amp;rsquo;m full — slow down.&amp;rdquo; Without it, fast producers eat slow consumers alive. In Go, backpressure is naturally expressed through blocking channel sends — and understanding when to block versus when to drop is one of the more interesting design decisions in concurrent systems.&lt;/p&gt;</description></item><item><title>Lesson 17: Go Scheduler Behavior — M:N scheduling is not magic</title><link>/post/go/go-concurrency-scheduler/</link><pubDate>Wed, 08 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-scheduler/</guid><description>&lt;p&gt;Most Go developers write concurrent code for years without thinking about the scheduler. That&amp;rsquo;s by design — the scheduler is supposed to be invisible. But eventually you&amp;rsquo;ll hit a situation where goroutines aren&amp;rsquo;t running when you expect them to, CPU cores are idle while goroutines pile up, or a CPU-bound workload is somehow slower with more goroutines. At that point you need a mental model of what&amp;rsquo;s actually happening under the hood.&lt;/p&gt;</description></item><item><title>Lesson 29: sync.Cond — The coordination primitive nobody teaches</title><link>/post/go/go-concurrency-sync-cond/</link><pubDate>Wed, 01 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-sync-cond/</guid><description>&lt;p&gt;Every Go concurrency course covers goroutines, channels, mutexes, WaitGroups, and maybe semaphores. Very few touch &lt;code&gt;sync.Cond&lt;/code&gt;. It&amp;rsquo;s either treated as advanced or dismissed as unnecessary because &amp;ldquo;just use channels.&amp;rdquo; But there&amp;rsquo;s a whole class of coordination problem where channels are the wrong tool and &lt;code&gt;sync.Cond&lt;/code&gt; is exactly right. Once you see the pattern, you&amp;rsquo;ll recognize it everywhere — and you&amp;rsquo;ll stop reaching for &lt;code&gt;time.Sleep&lt;/code&gt; polling loops when you shouldn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;This is a bonus lesson. Not because the topic is minor, but because it builds on mutexes (Lesson 6) and you really need to understand lock ownership before &lt;code&gt;sync.Cond&lt;/code&gt; clicks.&lt;/p&gt;</description></item><item><title>Lesson 16: Atomic Operations — Lock-free when you can, mutex when you must</title><link>/post/go/go-concurrency-atomics/</link><pubDate>Mon, 29 Sep 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-atomics/</guid><description>&lt;p&gt;The first time I looked at &lt;code&gt;sync/atomic&lt;/code&gt;, it felt like a niche tool for systems programmers writing lock-free data structures. Turns out it&amp;rsquo;s one of the most practically useful packages in the standard library — and most Go developers reach for it way too late, after they&amp;rsquo;ve already built something with a mutex and then profiled it into submission.&lt;/p&gt;
&lt;p&gt;Atomic operations are CPU-level instructions that read or modify memory as a single, indivisible unit. There&amp;rsquo;s no scheduler window between the read and the write. That means multiple goroutines can operate on the same memory location without a lock — and without any blocking. When your shared state is a counter, a flag, or a single configuration value, atomics are almost always the right tool.&lt;/p&gt;</description></item><item><title>Lesson 15: Semaphores for Concurrency Limits — A channel with a size is a semaphore</title><link>/post/go/go-concurrency-semaphores/</link><pubDate>Thu, 18 Sep 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-semaphores/</guid><description>&lt;p&gt;There&amp;rsquo;s a class of production bugs I see over and over — and the cause is almost always the same: nothing is telling the program to &lt;em&gt;slow down&lt;/em&gt;. A spike in traffic arrives, every goroutine blasts outbound, and suddenly you&amp;rsquo;ve got five hundred simultaneous connections against a Postgres instance that&amp;rsquo;s configured for a hundred. The database starts rejecting connections. The application throws errors. Everyone&amp;rsquo;s paged at 2am.&lt;/p&gt;
&lt;p&gt;The fix isn&amp;rsquo;t complicated. It&amp;rsquo;s a semaphore — a primitive that limits how many concurrent operations are in flight at once. Go doesn&amp;rsquo;t ship a dedicated semaphore type, but it doesn&amp;rsquo;t need to. A buffered channel of the right size &lt;em&gt;is&lt;/em&gt; a semaphore, and that insight unlocks a whole class of resource-limiting patterns.&lt;/p&gt;</description></item><item><title>Lesson 14: errgroup for Structured Concurrency — All succeed or all cancel</title><link>/post/go/go-concurrency-errgroup/</link><pubDate>Tue, 02 Sep 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-errgroup/</guid><description>&lt;p&gt;There&amp;rsquo;s a pattern that comes up constantly in backend services: make N concurrent calls, collect all their results, and if any one of them fails, cancel the rest and return the error. This is the &amp;ldquo;all succeed or all cancel&amp;rdquo; pattern — and before &lt;code&gt;errgroup&lt;/code&gt;, implementing it correctly required a non-trivial amount of boilerplate involving &lt;code&gt;WaitGroup&lt;/code&gt;, error channels, and manual context cancellation. People got it wrong often enough that &lt;code&gt;errgroup&lt;/code&gt; was created specifically to handle it.&lt;/p&gt;</description></item><item><title>Lesson 13: Timeouts Everywhere — Unbounded waits are production bugs</title><link>/post/go/go-concurrency-timeouts/</link><pubDate>Tue, 26 Aug 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-timeouts/</guid><description>&lt;p&gt;I have a strongly held opinion about timeouts: if you&amp;rsquo;re making a network call, a database query, or waiting on any external resource without a timeout, you&amp;rsquo;ve written a production bug. It just hasn&amp;rsquo;t fired yet. The network will eventually hang. The database will eventually have a slow query. The external API will eventually stop responding. And when it does, your goroutine will wait. And wait. And wait — holding a connection, a file descriptor, a slot in your worker pool — until the process runs out of resources or someone restarts it.&lt;/p&gt;</description></item><item><title>Lesson 12: Leak Prevention — Every goroutine must have an exit</title><link>/post/go/go-concurrency-leak-prevention/</link><pubDate>Fri, 15 Aug 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-leak-prevention/</guid><description>&lt;p&gt;Goroutine leaks are the memory leaks of concurrent Go. They&amp;rsquo;re slow, invisible, and tend to surface only under load — or worse, only after days of continuous running when the service has accumulated tens of thousands of stuck goroutines. I&amp;rsquo;ve debugged two production incidents that traced back to leaks in code that had been in production for months, completely unnoticed during normal load.&lt;/p&gt;
&lt;p&gt;The root cause is almost always the same: someone started a goroutine and didn&amp;rsquo;t give it a way to exit. Not a way to exit eventually — a way to exit in every possible code path.&lt;/p&gt;</description></item><item><title>Lesson 11: Graceful Shutdown — Stop accepting, finish what you started</title><link>/post/go/go-concurrency-graceful-shutdown/</link><pubDate>Sun, 03 Aug 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-graceful-shutdown/</guid><description>&lt;p&gt;Most Go services handle startup carefully and shutdown carelessly. The startup code has retries, health checks, dependency validation. The shutdown code is &lt;code&gt;os.Exit(0)&lt;/code&gt; or — if the developer was feeling generous — nothing at all, just letting the process get killed. That&amp;rsquo;s how you get dropped HTTP connections, half-written database records, uncommitted Kafka offsets, and on-call alerts at 3am.&lt;/p&gt;
&lt;p&gt;Graceful shutdown has one principle: stop accepting new work, finish what you already started. That&amp;rsquo;s it. But implementing it correctly requires knowing the shutdown order, wiring up OS signals properly, and draining workers before pulling the plug.&lt;/p&gt;</description></item><item><title>Lesson 10: Pipelines and Stage Isolation — Each stage owns its output channel</title><link>/post/go/go-concurrency-pipelines/</link><pubDate>Thu, 17 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-pipelines/</guid><description>&lt;p&gt;A pipeline is how you turn a stream of work into a sequence of transformations without writing a single monolithic function that does everything. Each stage reads from one channel, does its thing, and writes to another. Stages compose. Stages are independently testable. And because each stage runs concurrently with every other stage, the whole pipeline processes multiple items simultaneously — like an assembly line rather than a factory worker doing each product start to finish.&lt;/p&gt;</description></item><item><title>Lesson 9: Worker Pools — Bounded concurrency or bust</title><link>/post/go/go-concurrency-worker-pools/</link><pubDate>Thu, 10 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-worker-pools/</guid><description>&lt;p&gt;Production Go codebases that run fine in staging will sometimes crater under real traffic. More often than not, the cause is unbounded goroutine creation. Some background job processor spins up a goroutine per message. The queue backs up. Suddenly there are 50,000 goroutines fighting for CPU, exhausting file descriptors, allocating gigabytes of stack space. The service falls over. The incident post-mortem says &amp;ldquo;we didn&amp;rsquo;t expect this load.&amp;rdquo; The real cause: no worker pool.&lt;/p&gt;</description></item><item><title>Lesson 8: Fan-Out / Fan-In — Distribute, collect, don''t leak</title><link>/post/go/go-concurrency-fan-out-fan-in/</link><pubDate>Tue, 24 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-fan-out-fan-in/</guid><description>&lt;p&gt;There&amp;rsquo;s a moment in every Go developer&amp;rsquo;s journey where the serial loop stops being acceptable. You&amp;rsquo;ve got 200 URLs to fetch, 500 records to transform, 50 API calls to make — and you&amp;rsquo;re doing them one at a time. The fix feels obvious: spin up goroutines. But spin them up without a plan and you&amp;rsquo;ve traded one problem for three.&lt;/p&gt;
&lt;p&gt;Fan-out / fan-in is the pattern that makes parallel work manageable. Fan-out means distributing a stream of work across multiple goroutines. Fan-in means collecting their results into a single channel. Simple in concept, surprisingly easy to get wrong in practice.&lt;/p&gt;</description></item><item><title>Lesson 7: Race Conditions and the Go Memory Model — The bug you can't reproduce</title><link>/post/go/go-concurrency-race-conditions/</link><pubDate>Thu, 12 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-race-conditions/</guid><description>&lt;p&gt;Race conditions are the category of bug that makes senior engineers paranoid and junior engineers dismissive. They don&amp;rsquo;t reproduce consistently. Your tests pass. Your staging environment looks fine. Then, in production, under a specific load pattern at a specific time, two goroutines read and write the same memory address with no synchronization, and you get corrupted data — or a panic — or silently wrong results that sit in your database for three weeks before someone notices. Understanding why they happen at the language level is what separates engineers who prevent them from engineers who just get lucky.&lt;/p&gt;</description></item><item><title>Lesson 6: Mutexes Done Right — The boring tool that actually works</title><link>/post/go/go-concurrency-mutexes/</link><pubDate>Sat, 31 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-mutexes/</guid><description>&lt;p&gt;Channels get the spotlight in Go talks and blog posts — they&amp;rsquo;re the shiny, idiomatic, philosophically interesting tool. Mutexes feel like the C and Java baggage everyone was supposed to leave behind. But here&amp;rsquo;s what I&amp;rsquo;ve learned after several years of writing concurrent Go: mutexes are often the &lt;em&gt;right&lt;/em&gt; tool, especially when you&amp;rsquo;re protecting shared state, and the problems you see in production are almost never &amp;ldquo;we should&amp;rsquo;ve used channels&amp;rdquo; — they&amp;rsquo;re &amp;ldquo;we copied the mutex&amp;rdquo; or &amp;ldquo;we forgot to narrow the critical section.&amp;rdquo; Learn to use mutexes properly and stop apologizing for them.&lt;/p&gt;</description></item><item><title>Lesson 5: sync.WaitGroup — Wait for everyone, then move on</title><link>/post/go/go-concurrency-waitgroup/</link><pubDate>Wed, 21 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-waitgroup/</guid><description>&lt;p&gt;&lt;code&gt;sync.WaitGroup&lt;/code&gt; is one of the first concurrency primitives you reach for in Go, and also one of the first you misuse. The API looks deceptively simple — three methods, twenty minutes of reading, you think you&amp;rsquo;ve got it. Then you hit a negative counter panic in production, or you find out your &lt;code&gt;Wait()&lt;/code&gt; returned while goroutines were still running, and you spend an afternoon learning the rules you thought you already knew.&lt;/p&gt;</description></item><item><title>Lesson 4: Buffered vs Unbuffered Channels — Buffering hides bugs</title><link>/post/go/go-concurrency-buffered-vs-unbuffered/</link><pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-buffered-vs-unbuffered/</guid><description>&lt;p&gt;The first time someone tells you that buffered channels are faster, you believe them — and you spend the next year throwing &lt;code&gt;make(chan T, 100)&lt;/code&gt; at every performance complaint, wondering why your system still deadlocks sometimes, and why those deadlocks disappeared when you bumped the buffer to 200. The buffer wasn&amp;rsquo;t fixing anything. It was delaying the problem. Understanding why requires actually thinking about what buffering changes — not at the performance level, but at the coordination level.&lt;/p&gt;</description></item><item><title>Lesson 3: Channel Ownership Rules — Who closes the channel?</title><link>/post/go/go-concurrency-channel-ownership/</link><pubDate>Fri, 25 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-channel-ownership/</guid><description>&lt;p&gt;At some point, you&amp;rsquo;re going to close a channel twice and the runtime is going to panic with &lt;code&gt;close of closed channel&lt;/code&gt;. Or you&amp;rsquo;re going to close a channel from the wrong goroutine and send a value into it right after, and the runtime is going to panic with &lt;code&gt;send on closed channel&lt;/code&gt;. These aren&amp;rsquo;t subtle race conditions that only show up under load — they&amp;rsquo;re logic errors that exist because nobody in the codebase agreed on who &lt;em&gt;owns&lt;/em&gt; the channel. That agreement is the whole game.&lt;/p&gt;</description></item><item><title>Lesson 2: Cancellation with context.Context — Every goroutine needs a kill switch</title><link>/post/go/go-concurrency-context-cancellation/</link><pubDate>Sun, 13 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-context-cancellation/</guid><description>&lt;p&gt;Without &lt;code&gt;context.Context&lt;/code&gt;, you have goroutines running long after the user who triggered them gave up, HTTP connections burning server resources for requests nobody&amp;rsquo;s waiting for, and database queries executing on behalf of clients that disconnected ten seconds ago. Context is the mechanism Go gives you to propagate &amp;ldquo;I changed my mind&amp;rdquo; — and most engineers don&amp;rsquo;t use it until something in production embarrasses them into learning it properly.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s start with the most common mistake — blocking on a slow operation with no timeout:&lt;/p&gt;</description></item><item><title>Lesson 1: Goroutine Lifecycle Management — Who owns this goroutine?</title><link>/post/go/go-concurrency-goroutine-lifecycle/</link><pubDate>Sun, 06 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-goroutine-lifecycle/</guid><description>&lt;p&gt;Nobody tells you that the first goroutine you &amp;ldquo;fire and forget&amp;rdquo; in production is the one that eventually takes down your service at 3 AM. You start it, it runs, and everything looks fine — until requests pile up, memory climbs, and your dashboards turn red because five hundred goroutines are blocked waiting on a channel that&amp;rsquo;ll never receive another value. The problem isn&amp;rsquo;t that goroutines are dangerous. It&amp;rsquo;s that nobody taught you to think about &lt;em&gt;who owns them&lt;/em&gt;.&lt;/p&gt;</description></item><item><title>Lesson 25: Production Concurrency Architecture — Putting it all together</title><link>/post/rust/rust-conc-production-patterns/</link><pubDate>Sun, 22 Dec 2024 10:00:00 +0000</pubDate><guid>/post/rust/rust-conc-production-patterns/</guid><description>&lt;p&gt;After 24 lessons of building blocks, let&amp;rsquo;s talk about how they compose in real systems. I&amp;rsquo;ve shipped concurrent Rust services handling millions of requests per day, and the architecture patterns that survive production are surprisingly consistent. Not because they&amp;rsquo;re clever — because they&amp;rsquo;re boring. Boring is good when your pager is involved.&lt;/p&gt;
&lt;p&gt;This lesson is the blueprint I wish I&amp;rsquo;d had when I started building concurrent Rust systems for real.&lt;/p&gt;</description></item><item><title>Lesson 24: Testing Concurrent Code — Loom and beyond</title><link>/post/rust/rust-conc-testing/</link><pubDate>Sat, 21 Dec 2024 09:15:00 +0000</pubDate><guid>/post/rust/rust-conc-testing/</guid><description>&lt;p&gt;I spent a full week writing tests for a lock-free queue. Ran them a thousand times — all green. Shipped it. Two days later, a production crash. A race condition that occurred roughly once every 50,000 operations under specific timing. My tests never hit it because standard testing can&amp;rsquo;t explore all possible thread interleavings. That&amp;rsquo;s when I found Loom.&lt;/p&gt;
&lt;p&gt;Testing concurrent code is fundamentally different from testing sequential code. A test that passes doesn&amp;rsquo;t mean the code is correct — it means the code was correct &lt;em&gt;for that particular thread scheduling&lt;/em&gt;. Run the same test with different timing and you might get a different result.&lt;/p&gt;</description></item><item><title>Lesson 23: GPU Computing from Rust — wgpu and compute shaders</title><link>/post/rust/rust-conc-gpu/</link><pubDate>Thu, 19 Dec 2024 11:35:00 +0000</pubDate><guid>/post/rust/rust-conc-gpu/</guid><description>&lt;p&gt;The first time I ran a matrix multiplication on a GPU, my jaw dropped. A computation that took 8 seconds on an 8-core CPU finished in 40 milliseconds on a mid-range GPU. That&amp;rsquo;s a 200x speedup. GPUs have thousands of cores — small, simple cores designed for massively parallel uniform computation.&lt;/p&gt;
&lt;p&gt;Rust&amp;rsquo;s GPU story has gotten remarkably good. &lt;code&gt;wgpu&lt;/code&gt; gives you cross-platform GPU compute that works on Vulkan, Metal, DX12, and even in the browser via WebGPU. No CUDA lock-in.&lt;/p&gt;</description></item><item><title>Lesson 22: SIMD — Explicit vectorization</title><link>/post/rust/rust-conc-simd/</link><pubDate>Tue, 17 Dec 2024 08:20:00 +0000</pubDate><guid>/post/rust/rust-conc-simd/</guid><description>&lt;p&gt;I had an image processing pipeline that took 4.2 seconds per frame on a single core. After rewriting the hot loop with SIMD intrinsics, it dropped to 0.9 seconds. Same core, same algorithm, 4.7x faster. SIMD doesn&amp;rsquo;t add more cores — it makes each core do more work per clock cycle.&lt;/p&gt;
&lt;p&gt;SIMD (Single Instruction, Multiple Data) is the other kind of parallelism. While threads run code on different cores, SIMD runs the same operation on multiple data elements simultaneously within a single core.&lt;/p&gt;</description></item><item><title>Lesson 21: CSP-Style Concurrency — Go channels in Rust</title><link>/post/rust/rust-conc-csp/</link><pubDate>Sun, 15 Dec 2024 10:40:00 +0000</pubDate><guid>/post/rust/rust-conc-csp/</guid><description>&lt;p&gt;Before I wrote Rust full-time, I spent two years writing Go. The goroutine-plus-channel model gets into your brain. You start thinking about problems as independent processes connected by typed pipes. When I switched to Rust, the first thing I looked for was the equivalent of Go&amp;rsquo;s &lt;code&gt;select&lt;/code&gt; statement. Crossbeam has it — and in some ways it&amp;rsquo;s even better.&lt;/p&gt;
&lt;p&gt;CSP (Communicating Sequential Processes) is the formal model behind Go&amp;rsquo;s concurrency. The idea: concurrent processes interact only by passing messages through channels. No shared memory. Each process is sequential internally. Concurrency comes from composition.&lt;/p&gt;</description></item><item><title>Lesson 20: The Actor Model with Rust — Message-passing architectures</title><link>/post/rust/rust-conc-actor-model/</link><pubDate>Fri, 13 Dec 2024 14:20:00 +0000</pubDate><guid>/post/rust/rust-conc-actor-model/</guid><description>&lt;p&gt;The first time I saw Erlang&amp;rsquo;s actor model, I thought it was over-engineered. Every piece of state behind a process, every interaction a message. Then I worked on a system with 40 mutexes, 12 deadlock-prone code paths, and a debugging story that involved printf-ing thread IDs into a file and diffing them. I became an actor model convert that week.&lt;/p&gt;
&lt;p&gt;Rust doesn&amp;rsquo;t have a built-in actor system like Erlang or Akka. But the building blocks — channels, threads, ownership transfer — make building one surprisingly natural.&lt;/p&gt;</description></item><item><title>Lesson 19: Thread-Local Storage — Per-thread state</title><link>/post/rust/rust-conc-thread-local/</link><pubDate>Wed, 11 Dec 2024 09:55:00 +0000</pubDate><guid>/post/rust/rust-conc-thread-local/</guid><description>&lt;p&gt;I was optimizing a JSON serializer that allocated a buffer for every call. Under profiling, those allocations were 30% of the cost. The fix? A thread-local buffer that gets reused across calls on the same thread. No synchronization needed. No contention. Each thread has its own buffer. Throughput doubled.&lt;/p&gt;
&lt;p&gt;Thread-local storage is the ultimate escape hatch from synchronization overhead. If each thread has its own copy, there&amp;rsquo;s nothing to synchronize.&lt;/p&gt;</description></item><item><title>Lesson 18: Debugging Deadlocks and Data Races — Tools and techniques</title><link>/post/rust/rust-conc-deadlocks/</link><pubDate>Mon, 09 Dec 2024 11:30:00 +0000</pubDate><guid>/post/rust/rust-conc-deadlocks/</guid><description>&lt;p&gt;The worst deadlock I ever encountered wasn&amp;rsquo;t between two mutexes. It was between a mutex and a channel. Thread A held a lock and tried to send on a full bounded channel. Thread B was the consumer for that channel but was waiting to acquire the same lock before it could receive. Clean deadlock. Took me four hours to find because I was looking at mutex ordering and the real problem was a channel.&lt;/p&gt;</description></item><item><title>Lesson 17: Lock-Free Data Structures in Rust — Beyond mutexes</title><link>/post/rust/rust-conc-lock-free/</link><pubDate>Sat, 07 Dec 2024 08:50:00 +0000</pubDate><guid>/post/rust/rust-conc-lock-free/</guid><description>&lt;p&gt;A few years ago I was profiling a metrics collection service and found that 60% of the CPU time was spent on mutex contention. Thirty-two threads, one mutex protecting a counter map. The actual &amp;ldquo;work&amp;rdquo; — incrementing counters — took nanoseconds. The locking overhead was three orders of magnitude more expensive than the operation it protected.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s when lock-free data structures become worth the complexity. When the lock is the bottleneck, remove the lock.&lt;/p&gt;</description></item><item><title>Lesson 16: Barriers and Once — Synchronization primitives</title><link>/post/rust/rust-conc-barriers/</link><pubDate>Thu, 05 Dec 2024 10:15:00 +0000</pubDate><guid>/post/rust/rust-conc-barriers/</guid><description>&lt;p&gt;I was building a benchmark suite once — eight threads, each measuring throughput of a different operation. The problem was that threads started at different times depending on OS scheduling. Thread 0 might start 50ms before thread 7, skewing the results. I needed all threads to start their measurement at exactly the same time.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what barriers do. Everyone waits at the barrier until the last thread arrives, then they all proceed together.&lt;/p&gt;</description></item><item><title>Lesson 15: Condvar — Waiting for conditions</title><link>/post/rust/rust-conc-condvar/</link><pubDate>Tue, 03 Dec 2024 14:40:00 +0000</pubDate><guid>/post/rust/rust-conc-condvar/</guid><description>&lt;p&gt;Early in my career, I wrote a producer-consumer queue using a mutex and a busy-wait loop. The consumer would lock the mutex, check if there&amp;rsquo;s data, unlock, sleep for 10 milliseconds, and repeat. It worked, but it burned CPU doing nothing and had up to 10ms of latency on every message. My tech lead pointed me to condition variables, and the latency dropped to microseconds while CPU usage went to near zero.&lt;/p&gt;</description></item><item><title>Lesson 14: parking_lot — Faster mutexes</title><link>/post/rust/rust-conc-parking-lot/</link><pubDate>Sun, 01 Dec 2024 09:25:00 +0000</pubDate><guid>/post/rust/rust-conc-parking-lot/</guid><description>&lt;p&gt;I switched a high-contention service from &lt;code&gt;std::sync::Mutex&lt;/code&gt; to &lt;code&gt;parking_lot::Mutex&lt;/code&gt; and saw lock acquisition time drop by 30% under load. The API is nearly identical — it was a find-and-replace job. That&amp;rsquo;s the kind of optimization I like: massive payoff, zero complexity cost.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;parking_lot&lt;/code&gt; is one of those crates that probably should have been in the standard library. It provides the same synchronization primitives as std, but faster, smaller, and with more features.&lt;/p&gt;</description></item><item><title>Lesson 13: Fan-Out Fan-In in Rust — Parallel pipelines</title><link>/post/rust/rust-conc-fan-out-fan-in/</link><pubDate>Fri, 29 Nov 2024 12:50:00 +0000</pubDate><guid>/post/rust/rust-conc-fan-out-fan-in/</guid><description>&lt;p&gt;One of the most satisfying architectures I&amp;rsquo;ve built was a real-time analytics pipeline. Events came in through a single ingestion point, fanned out to eight processing workers, then fanned back in to a single aggregator that wrote results to the database. Throughput went from 2,000 events/second to 14,000 with zero data loss.&lt;/p&gt;
&lt;p&gt;Fan-out/fan-in is one of those patterns that looks simple on a whiteboard but has real subtlety in implementation. Getting the channel lifecycle right, handling errors, managing backpressure — that&amp;rsquo;s where things get interesting.&lt;/p&gt;</description></item><item><title>Lesson 12: Worker Pool Patterns — Bounded concurrency</title><link>/post/rust/rust-conc-worker-pools/</link><pubDate>Wed, 27 Nov 2024 08:35:00 +0000</pubDate><guid>/post/rust/rust-conc-worker-pools/</guid><description>&lt;p&gt;I blew up a production database once by spawning unlimited concurrent connections. A batch job that normally processed 100 items suddenly got 50,000. Each item opened a database connection. The connection pool maxed out, then the OS ran out of file descriptors. The database server stopped accepting connections from any service. All because I wrote &lt;code&gt;for item in items { thread::spawn(...) }&lt;/code&gt; without thinking about bounds.&lt;/p&gt;
&lt;p&gt;Worker pools solve this. Fixed number of threads, bounded queue, backpressure when the system is overloaded. After that incident, I never write unbounded concurrency again.&lt;/p&gt;</description></item><item><title>Lesson 11: Crossbeam — Scoped threads and lock-free structures</title><link>/post/rust/rust-conc-crossbeam/</link><pubDate>Mon, 25 Nov 2024 11:05:00 +0000</pubDate><guid>/post/rust/rust-conc-crossbeam/</guid><description>&lt;p&gt;Before Rust 1.63 added &lt;code&gt;thread::scope&lt;/code&gt; to the standard library, crossbeam was the only ergonomic way to spawn threads that could borrow local data. Even now that std has scoped threads, crossbeam remains essential. Its channels are faster than &lt;code&gt;std::sync::mpsc&lt;/code&gt;, it provides lock-free data structures, and its utilities fill gaps the standard library doesn&amp;rsquo;t cover.&lt;/p&gt;
&lt;p&gt;I reach for crossbeam in nearly every concurrent Rust project. Here&amp;rsquo;s why.&lt;/p&gt;
&lt;h2 id="crossbeam-channels"&gt;Crossbeam Channels&lt;/h2&gt;
&lt;p&gt;The biggest win: crossbeam&amp;rsquo;s channels are multi-producer, multi-consumer (MPMC) and significantly faster than &lt;code&gt;std::sync::mpsc&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Lesson 10: Rayon — Data parallelism made easy</title><link>/post/rust/rust-conc-rayon/</link><pubDate>Sat, 23 Nov 2024 15:20:00 +0000</pubDate><guid>/post/rust/rust-conc-rayon/</guid><description>&lt;p&gt;I had a batch processing job — reading 50,000 JSON files, parsing them, running validation, writing results. Single-threaded, it took 12 minutes. I added Rayon, changed &lt;code&gt;.iter()&lt;/code&gt; to &lt;code&gt;.par_iter()&lt;/code&gt;, and it dropped to 90 seconds. Five characters added to my code. That&amp;rsquo;s it.&lt;/p&gt;
&lt;p&gt;Rayon is probably the most impressive crate in the Rust ecosystem for the effort-to-impact ratio. If you have CPU-bound work that operates on collections, Rayon makes parallelism trivial.&lt;/p&gt;</description></item><item><title>Lesson 9: Send and Sync — The traits behind thread safety</title><link>/post/rust/rust-conc-send-sync/</link><pubDate>Thu, 21 Nov 2024 09:40:00 +0000</pubDate><guid>/post/rust/rust-conc-send-sync/</guid><description>&lt;p&gt;There&amp;rsquo;s a moment in every Rust developer&amp;rsquo;s life when they try to send an &lt;code&gt;Rc&amp;lt;RefCell&amp;lt;Vec&amp;lt;String&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; to another thread and get a wall of compiler errors. They Google the error, see something about &lt;code&gt;Send&lt;/code&gt; and &lt;code&gt;Sync&lt;/code&gt;, patch the type to &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;Vec&amp;lt;String&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;, and move on without understanding why.&lt;/p&gt;
&lt;p&gt;I did exactly that for six months. Then I needed to write a custom type that crossed thread boundaries, and I had to actually learn what these traits mean. Turns out they&amp;rsquo;re beautifully simple once you see the design.&lt;/p&gt;</description></item><item><title>Lesson 8: Memory Ordering — Relaxed, Acquire, Release, SeqCst</title><link>/post/rust/rust-conc-ordering/</link><pubDate>Tue, 19 Nov 2024 13:15:00 +0000</pubDate><guid>/post/rust/rust-conc-ordering/</guid><description>&lt;p&gt;Memory ordering is the thing that separates people who &lt;em&gt;use&lt;/em&gt; concurrent code from people who &lt;em&gt;write&lt;/em&gt; concurrent primitives. I avoided understanding it for years, using &lt;code&gt;SeqCst&lt;/code&gt; everywhere like a safety blanket. It worked. But it left performance on the table and, more importantly, left me unable to read half the lock-free code I encountered.&lt;/p&gt;
&lt;p&gt;So here&amp;rsquo;s the actual explanation — no hand-waving.&lt;/p&gt;
&lt;h2 id="why-ordering-matters"&gt;Why Ordering Matters&lt;/h2&gt;
&lt;p&gt;Modern CPUs and compilers reorder instructions for performance. Your code says &amp;ldquo;write A, then write B,&amp;rdquo; but the CPU might execute B first if there&amp;rsquo;s no dependency between them. On a single thread, this is invisible — the final result is the same.&lt;/p&gt;</description></item><item><title>Lesson 7: Atomics — Lock-free primitives</title><link>/post/rust/rust-conc-atomics/</link><pubDate>Sun, 17 Nov 2024 07:45:00 +0000</pubDate><guid>/post/rust/rust-conc-atomics/</guid><description>&lt;p&gt;I once replaced a &lt;code&gt;Mutex&amp;lt;u64&amp;gt;&lt;/code&gt; counter in a hot path with an &lt;code&gt;AtomicU64&lt;/code&gt; and saw throughput jump 40%. Not because mutexes are slow — they&amp;rsquo;re fast. But for a single integer being incremented by 32 threads, the overhead of acquiring and releasing a lock millions of times per second adds up to real time.&lt;/p&gt;
&lt;p&gt;Atomics are the foundation of lock-free programming. They let you do thread-safe operations on primitive values without any lock at all.&lt;/p&gt;</description></item><item><title>Lesson 6: Arc&lt;Mutex&lt;T&gt;&gt; — The shared mutable state pattern</title><link>/post/rust/rust-conc-arc-mutex/</link><pubDate>Fri, 15 Nov 2024 10:30:00 +0000</pubDate><guid>/post/rust/rust-conc-arc-mutex/</guid><description>&lt;p&gt;I remember staring at &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;HashMap&amp;lt;String, Vec&amp;lt;u8&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; in a codebase and thinking &amp;ldquo;this is the ugliest type I&amp;rsquo;ve ever seen.&amp;rdquo; Three months later, after debugging a similar system in Go that had zero type safety around its concurrent map access, I came crawling back to Rust&amp;rsquo;s ugly-but-correct approach.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; pattern is everywhere in Rust concurrent code. Understanding &lt;em&gt;why&lt;/em&gt; it exists — not just how to type it — is the key.&lt;/p&gt;</description></item><item><title>Lesson 5: Shared State — Mutex, RwLock, and poisoning</title><link>/post/rust/rust-conc-shared-state/</link><pubDate>Wed, 13 Nov 2024 16:55:00 +0000</pubDate><guid>/post/rust/rust-conc-shared-state/</guid><description>&lt;p&gt;The nastiest production bug I ever tracked down involved a Java ConcurrentHashMap that was &amp;ldquo;thread-safe&amp;rdquo; in the API sense but not in the logic sense. Two threads would read a key, both see it&amp;rsquo;s absent, both insert with computed values, and one would silently overwrite the other. The map itself was fine — the &lt;em&gt;access pattern&lt;/em&gt; was broken.&lt;/p&gt;
&lt;p&gt;Rust&amp;rsquo;s Mutex won&amp;rsquo;t save you from logic bugs either. But it will absolutely prevent you from forgetting the lock in the first place.&lt;/p&gt;</description></item><item><title>Lesson 4: Channels — mpsc and beyond</title><link>/post/rust/rust-conc-channels/</link><pubDate>Mon, 11 Nov 2024 09:10:00 +0000</pubDate><guid>/post/rust/rust-conc-channels/</guid><description>&lt;p&gt;The first concurrent system I built that actually worked well was a log aggregation pipeline. Multiple producers writing log lines, one consumer batching and flushing to disk. No shared state, no locks, no races. Just messages flowing through a pipe.&lt;/p&gt;
&lt;p&gt;That experience sold me on message passing. And Rust&amp;rsquo;s channel implementation makes it surprisingly ergonomic.&lt;/p&gt;
&lt;h2 id="the-problem-shared-state-is-hard"&gt;The Problem: Shared State Is Hard&lt;/h2&gt;
&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; share state between threads with mutexes. But every shared mutable variable is a coordination point, a potential bottleneck, and a source of bugs. The more threads touching the same data, the harder the code is to reason about.&lt;/p&gt;</description></item><item><title>Lesson 3: move Closures — Sending data to threads</title><link>/post/rust/rust-conc-move-closures/</link><pubDate>Sat, 09 Nov 2024 14:20:00 +0000</pubDate><guid>/post/rust/rust-conc-move-closures/</guid><description>&lt;p&gt;When I first started writing threaded Rust code, I hit the same compiler error about forty times in one afternoon. Something about closures borrowing values that might be dropped. I kept slapping &lt;code&gt;move&lt;/code&gt; on closures until things compiled, without really understanding what was happening.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a terrible way to learn. So here&amp;rsquo;s the actual explanation I wish I&amp;rsquo;d had.&lt;/p&gt;
&lt;h2 id="the-problem-closures-and-thread-lifetimes"&gt;The Problem: Closures and Thread Lifetimes&lt;/h2&gt;
&lt;p&gt;When you pass a closure to &lt;code&gt;thread::spawn&lt;/code&gt;, the new thread might run for an arbitrary amount of time. It could outlive the function that spawned it. It could outlive the variables it references.&lt;/p&gt;</description></item><item><title>Lesson 2: std::thread — Spawning and joining</title><link>/post/rust/rust-conc-threads/</link><pubDate>Thu, 07 Nov 2024 11:45:00 +0000</pubDate><guid>/post/rust/rust-conc-threads/</guid><description>&lt;p&gt;A few years back I was reviewing a Go service that spawned goroutines like confetti at a parade — hundreds of them, no tracking, no lifecycle management. When the service shut down, half those goroutines just vanished mid-work. Orphaned database connections everywhere. The Go runtime made it &lt;em&gt;so easy&lt;/em&gt; to fire off concurrent work that nobody stopped to think about cleanup.&lt;/p&gt;
&lt;p&gt;Rust&amp;rsquo;s threading model forces you to think about it. Every thread gives you a handle. Every handle demands acknowledgment. You can ignore it — but you have to do so explicitly.&lt;/p&gt;</description></item><item><title>Lesson 1: Why Rust Concurrency Is "Fearless" — The compiler has your back</title><link>/post/rust/rust-conc-why-fearless/</link><pubDate>Tue, 05 Nov 2024 08:30:00 +0000</pubDate><guid>/post/rust/rust-conc-why-fearless/</guid><description>&lt;p&gt;I once spent three days chasing a race condition in a Java service that only manifested under production load. The bug? Two threads updating a shared HashMap — no synchronization, no errors at compile time, no warnings. Just silent data corruption that showed up as incorrect billing amounts. Three days of my life, gone, because the language didn&amp;rsquo;t care.&lt;/p&gt;
&lt;p&gt;Then I tried to write the same bug in Rust. The compiler said no.&lt;/p&gt;</description></item></channel></rss>