<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Tarun Nahak</title>
        <link>https://www.tarunnahak.in</link>
        <description>Writing by Tarun Nahak on backend systems, HPC remote desktops, AI agents, Go, Kafka and teaching.</description>
        <lastBuildDate>Tue, 10 Nov 2026 16:46:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Tarun Nahak</title>
            <url>https://www.tarunnahak.in/favicon.ico</url>
            <link>https://www.tarunnahak.in</link>
        </image>
        <copyright>All rights reserved 2026</copyright>
        <item>
            <title><![CDATA[Generic methods in Go 1.27, and the one thing they still can’t do]]></title>
            <link>https://www.tarunnahak.in/articles/go-1-27-generic-methods</link>
            <guid>https://www.tarunnahak.in/articles/go-1-27-generic-methods</guid>
            <pubDate>Mon, 02 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Go 1.27 lets a method declare its own type parameters. Where that cleans up real code, and why a generic method still can’t satisfy an interface.]]></description>
            <content:encoded><![CDATA[<p>Go 1.27 shipped on 19 August 2026 with the language change I’ve wanted most since generics arrived in Go 1.18: a method can now declare its own type parameters. Until now a method could use its receiver’s type parameters but couldn’t add any of its own, so anything that needed a new type ended up as a top-level function sitting next to the type it really belonged to.</p>
<p>This post covers what changes in everyday code, the rule that generic methods can’t satisfy interfaces, and why the Go team drew that line where it did.</p>

<h2>The shape we’ve been writing since 1.18</h2>
<p>Take a paginated API result. The page type is generic over its items, and sooner or later you need to turn a page of database rows into a page of response objects.</p>
<pre><code class="language-go">type Page[T any] struct {
	Items      []T
	NextCursor string
}

// before Go 1.27: a method can't introduce R, so this has to be a function
func MapPage[T, R any](p Page[T], f func(T) R) Page[R] {
	out := Page[R]{Items: make([]R, 0, len(p.Items)), NextCursor: p.NextCursor}
	for _, item := range p.Items {
		out.Items = append(out.Items, f(item))
	}
	return out
}</code></pre>
<p>It works, but two steps in a row read inside-out: <code>MapPage(MapPage(p, toDTO), redact)</code>. The step that runs last is the first thing you read.</p>

<h2>What changes in 1.27</h2>
<p>The same function becomes a method. The receiver supplies <code>T</code>, and the method declares <code>R</code> right after its name:</p>
<pre><code class="language-go">func (p Page[T]) Map[R any](f func(T) R) Page[R] {
	out := Page[R]{Items: make([]R, 0, len(p.Items)), NextCursor: p.NextCursor}
	for _, item := range p.Items {
		out.Items = append(out.Items, f(item))
	}
	return out
}

resp := rows.Map(toDTO).Map(redact)</code></pre>
<p>Type arguments are inferred from the call’s arguments, just as they are for generic functions, so <code>.Map(toDTO)</code> needs no brackets. The <a href="https://go.dev/doc/go1.27">release notes</a> put the whole change in one sentence: “a method declaration may declare its own type parameters.”</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/go-1-27-generic-methods/nested-vs-chained.svg" alt="Hand-drawn comparison of the same two-step transformation. Top, before Go 1.27: nested function calls, MapPage(MapPage(p, toDTO), redact), drawn as a box inside a box, so the step that runs first sits in the middle and you read it inside-out. Bottom, with Go 1.27 generic methods: a left-to-right pipeline, p of type Page[User], then .Map(toDTO) gives Page[UserDTO], then .Map(redact) gives a redacted Page[UserDTO]." width="1000" height="540" loading="lazy" decoding="async" /><figcaption>Nested generic functions read inside-out. Generic methods read in the order they run.</figcaption></figure>
<p>Chaining is the obvious win, but the case I expect to use more is a non-generic type with one generic method. An HTTP client that decodes JSON into whatever type you ask for has always had to be a function that takes the client as an argument. Now it can be a method:</p>
<pre><code class="language-go">type Client struct {
	base string
	http *http.Client
}

func (c *Client) GetJSON[T any](ctx context.Context, path string) (T, error) {
	var out T
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.base+path, nil)
	if err != nil {
		return out, err
	}
	resp, err := c.http.Do(req)
	if err != nil {
		return out, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return out, fmt.Errorf("GET %s: %s", path, resp.Status)
	}
	err = json.NewDecoder(resp.Body).Decode(&amp;out)
	return out, err
}

user, err := client.GetJSON[User](ctx, "/users/42")</code></pre>
<p>Here <code>T</code> appears only in the result, so there’s nothing to infer it from and you write it explicitly. The standard library uses the feature too: <code>math/rand/v2</code> gains a generic <code>(*Rand).N</code> method.</p>
<p>One rule from the <a href="https://go.dev/blog/generic-methods">Go blog post on generic methods</a> is worth knowing early: a generic method has to be instantiated, explicitly or by inference, before it’s called or turned into a function value. A method expression names both sets of type arguments, so <code>Page[User].Map[UserDTO]</code> is an ordinary function that takes the page as its first argument.</p>

<h2>The one thing they can’t do: satisfy an interface</h2>
<p>This is the rule that will catch people. From the release notes: “methods of interfaces may not declare type parameters nor can interface methods be implemented by generic methods.”</p>
<p>The first half means you can’t write <code>Map[R any](...)</code> inside an interface. The second half is subtler. Suppose an interface asks for a method whose signature exactly matches one instantiation of a generic method:</p>
<pre><code class="language-go">type Labeler interface {
	Label(int) string
}

type Formatter struct{}

func (Formatter) Label[T any](v T) string { return fmt.Sprint(v) }

var _ Labeler = Formatter{} // does not compile: a generic method never satisfies an interface</code></pre>
<p><code>Formatter{}.Label[int]</code> has exactly the signature <code>Labeler</code> asks for, and it still doesn’t count. The blog post explains why: “Interface implementation is a property of a (possibly instantiated) type, not of any particular method.” <code>Formatter</code> declares the generic <code>Label</code>, not <code>Label[int]</code>.</p>

<h2>Why the line is there</h2>
<p>It comes down to how Go compiles. A call through an interface goes through a method table: one slot per method, each pointing at code that already exists for the concrete type. A direct call such as <code>page.Map[string](f)</code> is easy, because the type argument is right there at the call site and the compiler generates exactly that instantiation.</p>
<p>A generic method called through an interface would need code for every type argument anyone might ever pass, including types from packages that haven’t been written yet. The blog post calls that “impractical with Go’s approach to instantiation.” The alternative, boxing the arguments so that every instantiation shares one implementation, would make even direct calls pay for an indirect call. So Go 1.27 ships the useful half: type parameters on methods of concrete types, and none on interface methods.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/go-1-27-generic-methods/why-no-interface.svg" alt="Hand-drawn diagram of why a generic method cannot satisfy an interface. Left, a direct call: the compiler sees page.Map[string](f), knows the type argument at the call site, and generates exactly one instantiation, Map[string]. Right, a call through an interface value: the interface method table for Page has one slot per method, pointing at compiled code. A generic Map would need a slot for every possible type argument, Map[int], Map[string], Map[User] and types from packages that do not exist yet, so the table cannot be built ahead of time." width="1000" height="610" loading="lazy" decoding="async" /><figcaption>A direct call knows its type argument. An interface method table would need a slot for every type argument there could ever be.</figcaption></figure>

<h2>How I’ll use them</h2>
<ul>
<li><strong>Typed helpers on clients and stores.</strong> <code>client.GetJSON[T]</code>, or a repository method that scans rows into a type the caller picks, now live on the type instead of next to it.</li>
<li><strong>Short transformations on my own generic types.</strong> <code>Map</code> on a page or a result type reads well. I’m not going to build a streams library out of it, though. A <code>for</code> loop is still the clearest way to filter and sum a slice.</li>
<li><strong>Interfaces stay non-generic.</strong> If something with a generic method needs a test double, the double goes behind a small interface with concrete methods, such as <code>GetUser(ctx, id) (User, error)</code>, and the real implementation calls <code>GetJSON[User]</code> inside it.</li>
</ul>
<p>Go 1.27 also generalizes function type inference to every assignment context, so a generic function can be assigned or converted to a matching function type without spelling out its type arguments. It’s a small change that removes another set of brackets.</p>

<h2>The takeaway</h2>
<p>Generic methods won’t change how most Go code looks, and that’s a good thing. They take a handful of awkward top-level functions and put them back on the types they belong to. Just keep the rule in mind before you design around them: a generic method is a convenience on a concrete type, never part of an interface’s contract.</p>
]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Why your async trait still can’t go behind dyn, and what Rust is doing about it]]></title>
            <link>https://www.tarunnahak.in/articles/why-async-traits-cant-go-behind-dyn</link>
            <guid>https://www.tarunnahak.in/articles/why-async-traits-cant-go-behind-dyn</guid>
            <pubDate>Mon, 02 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[async fn in traits has been stable since Rust 1.75, but dyn Trait still rejects it. Why the compiler says no, the workarounds I reach for today, and the 2026 plan to fix it.]]></description>
            <content:encoded><![CDATA[<p>Almost every async Rust backend grows a trait like this one sooner or later:</p>
<pre><code class="language-rust">trait Storage {
    async fn get(&amp;self, key: &amp;str) -&gt; Option&lt;Bytes&gt;;
    async fn put(&amp;self, key: &amp;str, value: Bytes) -&gt; Result&lt;(), StorageError&gt;;
}</code></pre>
<p>It compiles fine. <code>async fn</code> in traits has been stable since Rust 1.75, at the end of 2023. Then you try to pick the backend at startup and keep it in your app state as <code>Arc&lt;dyn Storage&gt;</code>, and the compiler stops you: the trait is not dyn compatible, because method <code>get</code> is <code>async</code>.</p>
<p>The 1.75 stabilization deliberately left dyn out, and nearly three years later it’s still the first wall people hit with async traits. Here’s why it happens, what to use today, and what the Rust project plans to do about it.</p>

<h2>What an async fn in a trait returns</h2>
<p>An <code>async fn</code> is sugar for a function that returns <code>impl Future</code>. The <a href="https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits/">1.75 announcement</a> spells it out: an async fn in a trait desugars to a method returning <code>impl Future&lt;Output = ...&gt;</code>. So <code>get</code> is really this:</p>
<pre><code class="language-rust">fn get(&amp;self, key: &amp;str) -&gt; impl Future&lt;Output = Option&lt;Bytes&gt;&gt;;</code></pre>
<p>That <code>impl Future</code> is a different anonymous type in every implementation. The compiler turns each async body into a state machine that stores everything it holds across an <code>.await</code>. An S3-backed store holds a request in flight, a response buffer and maybe some retry state. An in-memory store holds a reference and a key. Same method, two types, two very different sizes.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/why-async-traits-cant-go-behind-dyn/future-types.svg" alt="Hand-drawn diagram of what async fn in a trait returns. The Storage trait declares async fn get. The S3Store implementation returns its own anonymous future type, a large state machine holding the key, an HTTP request in flight, a response buffer and retry state across await points. The MemStore implementation returns a different anonymous future type that holds only a reference and the key. Same method, two different types, two different sizes." width="1000" height="480" loading="lazy" decoding="async" /><figcaption>Each implementation of an async method returns its own anonymous future type, with its own size.</figcaption></figure>

<h2>Why dyn needs to know the size</h2>
<p>When you call a method through <code>&amp;dyn Storage</code>, the call goes through a vtable and the caller doesn’t know which implementation is behind it. That’s fine for a method that returns <code>Option&lt;Bytes&gt;</code>, because the return type is the same for every implementation. It’s not fine for a method that returns an opaque future. The Rust project’s goal for this feature puts it in one line: “The caller must provide storage for the returned value, but doesn’t know its size.”</p>
<p>The standard way out is to give the future a known size by putting it on the heap. A <code>Pin&lt;Box&lt;dyn Future&gt;&gt;</code> is always the same size, a data pointer and a vtable pointer, whatever future sits inside it, so it fits through a vtable. The cost is one allocation per call.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/why-async-traits-cant-go-behind-dyn/dyn-call.svg" alt="Hand-drawn diagram of why dyn dispatch fails for async methods. Top: a caller holding store of type &amp;dyn Storage calls get through the vtable. The caller must reserve space on its stack for the returned future, but it does not know which implementation it has, so it does not know how many bytes to reserve, and the compiler rejects the trait as not dyn compatible. Bottom: if get returns Pin&lt;Box&lt;dyn Future&gt;&gt;, the caller only reserves a fixed-size pointer, and the real future lives in a heap allocation made on every call." width="1000" height="500" loading="lazy" decoding="async" /><figcaption>Through dyn, the caller can’t reserve space for a future of unknown size. A boxed future is always the same size.</figcaption></figure>

<h2>What I use today</h2>
<p><strong>Static dispatch first.</strong> Most of the time dyn isn’t actually needed. A generic <code>S: Storage</code> parameter, or <code>impl Storage</code>, keeps every call unboxed. When the set of backends is closed and known, an enum works too:</p>
<pre><code class="language-rust">enum AnyStorage {
    S3(S3Store),
    Memory(MemStore),
}

impl Storage for AnyStorage {
    async fn get(&amp;self, key: &amp;str) -&gt; Option&lt;Bytes&gt; {
        match self {
            AnyStorage::S3(s) =&gt; s.get(key).await,
            AnyStorage::Memory(m) =&gt; m.get(key).await,
        }
    }
    // put follows the same pattern
}</code></pre>
<p>No boxing, no macro, and adding a backend becomes a compile error at every match that needs updating.</p>
<p><strong>When I really need dyn: <code>async-trait</code> or <code>dynosaur</code>.</strong> The <a href="https://docs.rs/async-trait">async-trait</a> crate rewrites each async method to return <code>Pin&lt;Box&lt;dyn Future + Send + 'async_trait&gt;&gt;</code> and wraps the body in <code>Box::pin(async move { ... })</code>. It’s simple and it’s everywhere, but it boxes every call, static ones included, and its futures are <code>Send</code> unless you write <code>#[async_trait(?Send)]</code>.</p>
<p><a href="https://docs.rs/dynosaur">dynosaur</a>, maintained by Santiago Pastorino and Tyler Mandry, takes the other approach. You keep native <code>async fn</code> in the trait and ask for a dyn-friendly companion type:</p>
<pre><code class="language-rust">#[dynosaur::dynosaur(DynStorage = dyn(box) Storage)]
trait Storage {
    async fn get(&amp;self, key: &amp;str) -&gt; Option&lt;Bytes&gt;;
}</code></pre>
<p>Anywhere you would have written <code>dyn Storage</code>, you write <code>DynStorage</code> instead, and you build one with constructors such as <code>DynStorage::new_box(store)</code>. Calls through <code>DynStorage</code> box the returned future. Static calls on the concrete type don’t.</p>
<p><strong>Or by hand.</strong> Declare the method as <code>fn get(&amp;self, key: &amp;str) -&gt; Pin&lt;Box&lt;dyn Future&lt;Output = Option&lt;Bytes&gt;&gt; + Send + '_&gt;&gt;</code>. It works, and you lose the ergonomics of <code>async fn</code> in every implementation.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/why-async-traits-cant-go-behind-dyn/where-the-box-goes.svg" alt="Hand-drawn table of where each approach allocates. Generics or an enum over the implementations: no boxing for static calls, and no dyn needed. The async-trait crate: a box on every call, static or dynamic. A hand-written Pin&lt;Box&lt;dyn Future&gt;&gt; return type: a box on every call. The dynosaur crate: no box for static calls, a box only when called through the generated DynStorage type. The dyn_box! helper from the 2026 project goal, nightly only: a box only at the call sites you mark." width="1000" height="500" loading="lazy" decoding="async" /><figcaption>Where each approach pays for a heap allocation.</figcaption></figure>

<h2>The Send problem next to it</h2>
<p>A second gap shows up as soon as you <code>tokio::spawn</code> a call. With native <code>async fn</code> in a trait, a generic caller can’t require the returned future to be <code>Send</code>, so this fails with “future cannot be sent between threads safely”:</p>
<pre><code class="language-rust">async fn warm_cache&lt;S: Storage + Send + Sync + 'static&gt;(store: Arc&lt;S&gt;) {
    tokio::spawn(async move {
        store.get("config").await; // the future from get() might not be Send
    });
}</code></pre>
<p>Today’s answer, shipped alongside 1.75, is the <code>trait-variant</code> crate. Put <code>#[trait_variant::make(Storage: Send)]</code> on a <code>LocalStorage</code> trait and it generates a second trait, <code>Storage</code>, whose futures are all <code>Send</code>. The longer-term fix is return type notation, which lets the caller write the bound directly, as in <code>S::get(..): Send</code>. RTN is implemented on nightly behind the <code>return_type_notation</code> feature, and its 2026 project goal says full stabilization is blocked on the next-generation trait solver and is intended for late this year.</p>

<h2>The plan for 2026</h2>
<p>The Rust project’s <a href="https://goals.rust-lang.org/2026/afidt-box.html">2026 goals</a> include “Native async fn dynamic dispatch in traits”. It’s accepted, spans 2026 and 2027, and is owned by Jack Huey and Santiago Pastorino. It comes in two phases:</p>
<ol>
<li><strong>Method-scoped dyn compatibility</strong>, about four months. Instead of rejecting a whole trait because some methods can’t be dispatched dynamically, the trait becomes dyn compatible anyway, and those methods are reachable only through a new mechanism.</li>
<li><strong>Async through dynamic dispatch</strong>, about eight months, built on top of that.</li>
</ol>
<p>The first concrete piece is a deliberately minimal, nightly-only helper that boxes at the call site:</p>
<pre><code class="language-rust">async fn fetch_data(client: &amp;dyn HttpClient, url: &amp;str) -&gt; Response {
    std::preview::dyn_box!(client.fetch(url)).await
}</code></pre>
<p>The allocation happens only where you ask for it, and static calls don’t change at all. The roadmap calls this <code>.box</code> notation, but it’s explicit that no final syntax has been agreed and that the details will be settled in RFCs. Treat any syntax you see now as a sketch.</p>

<h2>What I’d do today</h2>
<ul>
<li><strong>Reach for generics or an enum first.</strong> A lot of code that seems to need dyn has a closed set of implementations.</li>
<li><strong>For open-ended dyn, use <code>dynosaur</code> in new code</strong> so static calls stay unboxed, and <code>async-trait</code> where a codebase already standardizes on it.</li>
<li><strong>Don’t fear the box.</strong> One allocation per call is noise next to a network round trip. It matters in tight loops, not in a storage call.</li>
<li><strong>Watch the project goal.</strong> When native support lands, traits already written with plain <code>async fn</code> will have the least to change.</li>
</ul>
]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Go 1.27 can find your leaked goroutines. Here’s how it knows]]></title>
            <link>https://www.tarunnahak.in/articles/goroutine-leak-profile-go-1-27</link>
            <guid>https://www.tarunnahak.in/articles/goroutine-leak-profile-go-1-27</guid>
            <pubDate>Mon, 02 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Go 1.27 makes the goroutineleak profile generally available. How the garbage collector spots goroutines that can never wake up, how to read the profile, and what it still can’t see.]]></description>
            <content:encoded><![CDATA[<p>A goroutine leak is quiet. A goroutine blocks on a channel that nothing will ever touch again and sits there for the life of the process, holding its stack and everything it references. One is harmless. One per request is a slow memory leak and a goroutine count that only goes up.</p>
<p>Until recently the tools for this were indirect: graph <code>runtime.NumGoroutine</code>, take a goroutine profile and look for suspicious piles of identical stacks, or catch leaks in tests with <code>goleak</code>. Go 1.27 adds a direct one. The <code>goroutineleak</code> profile, an experiment in Go 1.26 behind <code>GOEXPERIMENT=goroutineleakprofile</code>, is now generally available, and the experiment flag is gone.</p>

<h2>A leak in a dozen lines</h2>
<p>The most common shape I know is fan out, collect, and return early on the first error.</p>
<pre><code class="language-go">type result struct {
	resp Response
	err  error
}

func fetchAll(ctx context.Context, urls []string) ([]Response, error) {
	ch := make(chan result)
	for _, u := range urls {
		go func() {
			resp, err := fetch(ctx, u)
			ch &lt;- result{resp, err} // blocks forever once fetchAll has returned
		}()
	}

	var out []Response
	for range urls {
		r := &lt;-ch
		if r.err != nil {
			return nil, r.err // the remaining senders are now stuck
		}
		out = append(out, r.resp)
	}
	return out, nil
}</code></pre>
<p>The channel is unbuffered, so every send waits for a receive. When one fetch fails, <code>fetchAll</code> returns, and every goroutine that hasn’t sent yet blocks on <code>ch &lt;-</code> with nobody left to receive. The fix is one line, a buffer with room for every sender, <code>make(chan result, len(urls))</code>, or an <code>errgroup</code> with a context that cancels the rest.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/goroutine-leak-profile-go-1-27/early-return.svg" alt="Hand-drawn diagram of a goroutine leak. fetchAll starts six goroutines that each send one result on an unbuffered channel ch. It receives two results, the third is an error, and fetchAll returns. The other three goroutines are left blocked on their send to ch forever, because nothing will ever receive from it again." width="1000" height="540" loading="lazy" decoding="async" /><figcaption>One early return, and every goroutine that hasn’t sent yet is stuck for the life of the process.</figcaption></figure>

<h2>How the runtime knows a goroutine can never wake up</h2>
<p>You can’t tell a leaked goroutine from a slow one by looking at it. Both are blocked on a channel. The difference is whether anything still running could ever unblock it, and that’s a reachability question, which is exactly what the garbage collector answers all day.</p>
<p>The <a href="https://go.dev/blog/goroutine-leak-profiles">Go blog post on goroutine leak profiles</a> defines it precisely. A goroutine is live if it isn’t blocked by a concurrency primitive, or if at least one primitive blocking it is referenced by another live goroutine. To find the rest, the runtime adapts the garbage collector’s marking phase:</p>
<ol>
<li>A normal GC treats every goroutine as a root. Here, marking starts only from global variables and the goroutines that aren’t blocked.</li>
<li>When marking reaches a channel or lock that a blocked goroutine is waiting on, that goroutine becomes reachable too, and marking continues from it.</li>
<li>When nothing new gets marked, any blocked goroutine that’s still unmarked is waiting on something only other stuck goroutines can reach. It’s reported as leaked.</li>
</ol>
<p>The leaked goroutines are then marked anyway, so their memory isn’t freed out from under them. The profile reports leaks; it doesn’t collect them.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/goroutine-leak-profile-go-1-27/reachability.svg" alt="Hand-drawn diagram of how the goroutineleak profile decides a goroutine has leaked. Marking starts only from global variables and goroutines that are running or runnable. Running goroutine G1 references channel jobs, so G3, which is blocked receiving from jobs, could still be woken and is live. Channel ch is referenced only by G5 and G6, which are themselves blocked sending on it, so marking never reaches it, and G5 and G6 are reported as leaked." width="1000" height="600" loading="lazy" decoding="async" /><figcaption>Marking starts from globals and running goroutines. A blocked goroutine counts only if something live can reach what it’s waiting on.</figcaption></figure>
<p>It covers goroutines blocked on channel sends and receives, including on nil channels, on a <code>select</code> without a <code>default</code> case, and on <code>sync.Mutex</code>, <code>sync.RWMutex</code>, <code>sync.WaitGroup</code> and <code>sync.Cond</code>.</p>

<h2>Reading the profile</h2>
<p>If your service already registers <code>net/http/pprof</code>, there’s nothing to add. The profile shows up at <code>/debug/pprof/goroutineleak</code> next to the others:</p>
<pre><code class="language-bash">curl http://localhost:6060/debug/pprof/goroutineleak &gt; leak.prof
go tool pprof leak.prof</code></pre>
<p>From there it works like any other pprof profile. <code>top</code> shows where the leaked goroutines are stuck, grouped by stack, and <code>list fetchAll</code> points at the exact line, which in the example above is the send. Unlike the plain goroutine profile, you don’t have to guess which of a thousand parked stacks are the bad ones: everything in it is a goroutine the runtime has decided can never be woken.</p>

<h2>What it can’t see</h2>
<p>A clean profile doesn’t mean there are no leaks. The blog lists three limits, and they’re worth knowing before you trust an empty result:</p>
<ul>
<li><strong>Anything still reachable.</strong> If the channel or mutex a goroutine waits on can be reached from a global variable or from a goroutine that’s still running, the waiting goroutine is never reported, even if nothing will ever use that channel again. A channel parked in a package-level map is invisible to it.</li>
<li><strong>Blocking that isn’t a channel or a sync primitive.</strong> A goroutine stuck on network or file I/O, or in a system call, is never considered leaked. A <code>conn.Read</code> with no deadline can hang forever without appearing. Deadlines and contexts are still your job.</li>
<li><strong>Leaks that haven’t happened yet.</strong> The profile finds a leak after it occurs. To catch one before it ships, pair it with <code>goleak</code> and <code>testing/synctest</code> in tests.</li>
</ul>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/goroutine-leak-profile-go-1-27/blind-spots.svg" alt="Hand-drawn diagram of three things the goroutineleak profile cannot report. One: a goroutine blocked on a channel that is still reachable from a global variable, such as a registry map, is never reported even if nothing will use that channel again. Two: a goroutine blocked on network or file I/O, such as conn.Read with no deadline, is never considered leaked. Three: the profile only sees leaks that have already happened, so tests with goleak and testing/synctest catch them earlier." width="1000" height="430" loading="lazy" decoding="async" /><figcaption>Three things the leak profile won’t report.</figcaption></figure>
<p>There’s a cost note too. The memory overhead is negligible, but in the worst case, a long chain of goroutines each blocked on the next, detection takes O(n²) steps for one GC cycle, where n is the number of goroutines. The blog suggests tuning how often you collect it, for example every few hours, rather than scraping it continuously.</p>

<h2>My checklist for every go statement</h2>
<ul>
<li>Who receives what this goroutine sends, and what happens if they stop early? If there’s an early return, buffer the channel or <code>select</code> on <code>ctx.Done()</code>.</li>
<li>Who closes the channel this goroutine ranges over?</li>
<li>Does every network call it makes have a deadline?</li>
<li>Is there a <code>goleak.VerifyNone(t)</code> in the package’s tests?</li>
</ul>
<p>And now one more: collect <code>/debug/pprof/goroutineleak</code> in staging after a load test. A goroutine count that only goes up used to mean an afternoon of reading stack dumps. Now the runtime hands you the line.</p>
]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Where the memory goes in a Tokio service]]></title>
            <link>https://www.tarunnahak.in/articles/where-the-memory-goes-in-a-tokio-service</link>
            <guid>https://www.tarunnahak.in/articles/where-the-memory-goes-in-a-tokio-service</guid>
            <pubDate>Mon, 02 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[An async Rust service that grows to gigabytes rarely has a single leak. The four places I look: future sizes, queues without backpressure, the allocator, and tasks that never finish.]]></description>
            <content:encoded><![CDATA[<p>Rust makes classic leaks rare. You don’t forget to free memory, and the borrow checker won’t let you use memory after it’s freed. So when an async Rust service climbs to several gigabytes, the cause is almost never a leak in the C sense. It’s memory that something is legitimately holding, in a place you weren’t looking.</p>
<p>These are the four places I look.</p>

<h2>1. Futures are bigger than they look</h2>
<p>Tokio tasks are cheap. The <a href="https://tokio.rs/tokio/tutorial/spawning">Tokio tutorial</a> says a task needs “only a single allocation and 64 bytes of memory”, and that applications can spawn thousands, even millions, of them. What those 64 bytes don’t include is the future itself, and the future lives in that same allocation.</p>
<p>An <code>async fn</code> compiles to a state machine, and every local that’s alive across an <code>.await</code> becomes a field of it. Futures you await are stored inline too, with their own locals, all the way down. So this innocent-looking handler costs 64 KiB per connection for its whole life, whether it’s reading or not:</p>
<pre><code class="language-rust">async fn handle(mut socket: TcpStream) -&gt; io::Result&lt;()&gt; {
    let mut buf = [0u8; 64 * 1024]; // lives in the future, across every .await below
    loop {
        let n = socket.read(&amp;mut buf).await?;
        if n == 0 {
            return Ok(());
        }
        socket.write_all(&amp;buf[..n]).await?;
    }
}</code></pre>
<p>A hundred thousand idle connections is over 6 GiB of buffers, and nothing is leaking.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/where-the-memory-goes-in-a-tokio-service/task-anatomy.svg" alt="Hand-drawn diagram of what one spawned Tokio task costs. The task is a single allocation: a small header for the runtime, about 64 bytes, plus the whole future. The future is a state machine that stores every local held across an await, any futures it awaits nested inline, and in this example a 64 KiB array buffer held across an await. Multiply by the number of tasks. Fixes: keep big buffers in a Vec sized to what you need, or Box::pin the big inner future so it is allocated only while it runs." width="1000" height="520" loading="lazy" decoding="async" /><figcaption>A spawned task is one allocation, and most of it is the future: everything held across an .await, all the way down.</figcaption></figure>
<p>Finding these is easy once you know to look:</p>
<ul>
<li><code>std::mem::size_of_val(&amp;fut)</code> on a future, before you spawn it, gives its size. A small test that checks it catches regressions in the futures you spawn per connection or per message.</li>
<li>Clippy’s <code>large_futures</code> lint flags futures over a size threshold, 16 KiB by default. It’s in the pedantic group, so you have to turn it on.</li>
<li><a href="https://github.com/tokio-rs/console">tokio-console</a> has <code>large-future</code> and <code>auto-boxed-future</code> warnings, the second for futures the runtime boxed because they were large.</li>
</ul>
<p>The fixes: keep big buffers in a <code>Vec</code> or <code>BytesMut</code> sized to what you actually need, or <code>Box::pin</code> a large inner future so it gets its own allocation while it runs instead of living inside every outer future. Large futures can also overflow the stack before they’re ever spawned, which is the other reason Clippy warns about them.</p>

<h2>2. Queues without backpressure</h2>
<p>The second place is any queue that can grow without limit. Tokio’s <code>unbounded_channel</code> docs are blunt about it: it’s a channel “without backpressure”, and if the receiver falls behind, messages are “arbitrarily buffered”. The only real limit is system memory, and when that runs out the process is aborted.</p>
<p>A bounded channel turns that failure into a slowdown. With <code>mpsc::channel(64)</code>, <code>send().await</code> waits while the queue is full, so the producer runs at the consumer’s pace and memory stays flat.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/where-the-memory-goes-in-a-tokio-service/backpressure.svg" alt="Hand-drawn comparison of an unbounded and a bounded Tokio channel. Top: with unbounded_channel, a fast producer can always send, so when the consumer is slow the queue between them grows without limit until the process runs out of memory. Bottom: with channel(64), the queue holds at most 64 messages, and when it is full the producer waits at send().await, so it slows to the consumer pace and memory stays flat." width="1000" height="505" loading="lazy" decoding="async" /><figcaption>An unbounded channel grows until memory runs out. A bounded one makes the producer wait.</figcaption></figure>
<pre><code class="language-rust">let (tx, mut rx) = tokio::sync::mpsc::channel::&lt;Event&gt;(1024);

// producer: waits here instead of piling up events while the consumer is behind
tx.send(event).await?;</code></pre>
<p>The same thinking applies to anything that buffers: a <code>Vec</code> you push to in a loop, a stream with <code>buffer_unordered</code>, a cache without eviction. For <code>broadcast</code> channels, remember that the capacity is rounded up to the next power of two and allocated up front, and that a receiver which falls too far behind gets a <code>Lagged</code> error and skips the oldest messages rather than growing the buffer.</p>

<h2>3. The allocator is holding it, not you</h2>
<p>This one looks exactly like a leak. Traffic spikes, memory climbs, traffic drops, and resident memory (RSS) stays high for hours, while a heap profile shows live allocations back at their baseline. Nothing is leaking. The allocator is keeping freed memory instead of returning it to the operating system.</p>
<p>On Linux, Rust uses the system allocator by default, which is usually glibc’s malloc. glibc gives threads separate arenas to reduce contention, and the arena limit is usually a multiple of the number of CPUs. Each arena is its own heap, and the <a href="https://man7.org/linux/man-pages/man3/mallopt.3.html">mallopt man page</a> notes that a heap can be trimmed only if memory is freed at the top end. One long-lived allocation near the top of an arena keeps all the freed space below it. A multi-threaded Tokio runtime runs work on many threads, so there are many arenas, each holding its own leftovers.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/where-the-memory-goes-in-a-tokio-service/allocator.svg" alt="Hand-drawn diagram of why a Rust service on glibc malloc can hold memory after a spike. Left: each worker thread allocates from a malloc arena, and each arena is a heap with live blocks and freed holes. A heap can only shrink from its top, so one long-lived block near the top keeps all the freed memory below it. Right: a sketch of memory over time during a traffic spike. The live heap rises and falls back, but resident memory, RSS, rises and stays high, which looks like a leak but is freed memory the allocator has not returned. Fixes: MALLOC_ARENA_MAX or switching to jemalloc or mimalloc." width="1000" height="520" loading="lazy" decoding="async" /><figcaption>After a spike, the live heap comes back down but RSS doesn’t: freed memory the allocator kept.</figcaption></figure>
<p>Two fixes, in order of effort:</p>
<ol>
<li>Cap the number of arenas with the <code>MALLOC_ARENA_MAX</code> environment variable, for example <code>MALLOC_ARENA_MAX=2</code>, and measure. Fewer arenas means less memory stuck in leftovers, at the cost of more contention.</li>
<li>Switch the global allocator. jemalloc and mimalloc often hold far less memory for this kind of workload, and the switch is a few lines:</li>
</ol>
<pre><code class="language-rust">// Cargo.toml:
// [target.'cfg(not(target_env = "msvc"))'.dependencies]
// tikv-jemallocator = "0.7"

#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;</code></pre>
<p>Whichever you pick, compare RSS under the same load test before and after. Allocator behaviour depends on the workload, and your own numbers are the only argument that counts.</p>

<h2>4. Tasks that never finish</h2>
<p>The last place is the closest thing to a real leak. Dropping a <code>JoinHandle</code> doesn’t cancel a Tokio task; it detaches it. A spawned loop that waits on a channel whose sender is kept alive somewhere, or that retries forever, keeps its future and everything it captured, often an <code>Arc</code> of shared state, until the process exits.</p>
<p>The tool for this is <a href="https://github.com/tokio-rs/console">tokio-console</a>, which its README describes as “like <code>top(1)</code> for tasks”. It needs the <code>tokio_unstable</code> cfg and one line of setup:</p>
<pre><code class="language-rust">// build with: RUSTFLAGS="--cfg tokio_unstable" cargo build
#[tokio::main]
async fn main() {
    console_subscriber::init();
    // ... the rest of your service
}</code></pre>
<p>Run <code>tokio-console</code> next to it and look at task counts and idle times. A count that only grows, or a pile of tasks spawned from the same line that have been idle for hours, points straight at the loop that never exits. The fix is usually an exit condition: a <code>CancellationToken</code>, a channel that gets closed, or a <code>JoinSet</code> whose tasks are aborted when it’s dropped along with its owner.</p>

<h2>Where to start</h2>
<ol>
<li>Compare RSS with the live heap. If the heap is flat and RSS isn’t, it’s the allocator.</li>
<li>Count tasks over time. If the count only grows, find the task that never ends.</li>
<li>Look for unbounded queues on the hot path.</li>
<li>Measure the futures you spawn per connection or per message.</li>
</ol>
<p>None of these is a bug the compiler can catch, and none of them shows up in a small test. They appear under real load, a few hours in, so it pays to know where to look before then.</p>
]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Where the milliseconds go in a remote desktop for HPC]]></title>
            <link>https://www.tarunnahak.in/articles/remote-desktop-for-hpc-where-the-milliseconds-go</link>
            <guid>https://www.tarunnahak.in/articles/remote-desktop-for-hpc-where-the-milliseconds-go</guid>
            <pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A remote desktop is a pipeline: schedule, capture, encode, transport, decode, present. How I find which stage is slow, and when GPU decoding is worth it.]]></description>
            <content:encoded><![CDATA[<p>At Siemens I work on delivering Windows remote desktops for HPC workloads that are scheduled through PBS. People use these sessions to drive heavy interactive applications that run on cluster nodes instead of on their own machines. When a session feels sluggish, the cause can sit in any of half a dozen places, and the fix for one is useless for another.</p>
<p>This post is the mental model I use to find out which place it is. None of it is specific to one product; it applies to any system that turns a desktop into a video stream and back.</p>

<h2>A remote desktop is a pipeline</h2>
<p>Every frame you see goes through the same stages:</p>
<ol>
<li><strong>Schedule.</strong> Before there is a desktop at all, the scheduler has to find a node with the right resources and start the session there. This happens once, but it is the first thing users notice.</li>
<li><strong>Capture.</strong> On the host, the compositor produces frames and the capture layer grabs what changed since the last one.</li>
<li><strong>Encode.</strong> The frame is compressed with a video codec, on the GPU if the node has a hardware encoder or on the CPU if it doesn't.</li>
<li><strong>Transport.</strong> The encoded frame crosses the network, along with everything that network brings: round-trip time, jitter and packet loss.</li>
<li><strong>Decode.</strong> The client turns the bitstream back into pixels, again on either the CPU or the GPU.</li>
<li><strong>Present.</strong> The decoded frame is converted to the display format and drawn, ideally in step with the monitor’s refresh.</li>
</ol>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/remote-desktop-for-hpc-where-the-milliseconds-go/pipeline.svg" alt="Hand-drawn diagram of the remote desktop pipeline: Schedule on the scheduler, Capture and Encode on the HPC node, Transport over the network, Decode and Present on your machine, with input travelling back from the client to the host." width="1000" height="470" loading="lazy" decoding="async" /><figcaption>Every frame makes the same trip. Input goes the other way, and lag is the whole round trip.</figcaption></figure>
<p>Input travels the other way: a key press goes from the client to the host, the application reacts, and the result comes back through all six stages. What users call lag is the sum of that round trip.</p>

<h2>Throughput and latency are different problems</h2>
<p>It’s worth separating two numbers that are easy to blur together. <strong>Throughput</strong> is how many frames per second the pipeline can sustain. <strong>Latency</strong> is how long a single frame, or a single key press, takes to make it through.</p>
<p>They pull against each other. Buffers smooth out throughput: if decoding occasionally takes too long, a queue of frames hides it. But every buffered frame is a frame of latency. For video playback that trade is fine. For an interactive session it usually isn’t, and it’s better to drop a late frame than to queue it behind others and make everything after it late too.</p>
<p>So when someone reports that a session is slow, the first question is which kind of slow. Choppy motion points at throughput. A delay between moving the mouse and seeing the cursor move points at latency.</p>

<h2>CPU or GPU decoding</h2>
<p>Decoding is where the CPU-versus-GPU choice gets interesting, because the answer depends on the client, not the server.</p>
<p><strong>Hardware decoding</strong> on the GPU, through APIs like D3D11VA or DXVA on Windows, VA-API on Linux or VideoToolbox on macOS, can handle high resolutions at high frame rates while leaving the CPU almost idle. When it works, it’s the best option by a wide margin.</p>
<p>The catch is the word “when”. Hardware decoders differ between vendors, generations and driver versions. Some don’t support every codec profile. Some limit how many streams can decode at once. And there is a trap that quietly eats the benefit: if the decoded frame is copied from GPU memory back to system memory so the CPU can convert and draw it, that copy can cost more than the decode saved. To get the full benefit, the frame should stay on the GPU from decode to present.</p>
<p><strong>Software decoding</strong> on the CPU, for example through FFmpeg’s libavcodec, is predictable and runs everywhere, including virtual machines and thin clients without a usable GPU. It scales with cores, but at high resolutions and frame rates it can saturate a laptop CPU, and the colour conversion from the decoder’s output format to RGB adds its own cost.</p>
<p>In practice the answer is to support both and decide at runtime. Probe what the client can actually do, prefer the hardware path, and fall back to software automatically, both when the hardware decoder fails to initialise and when it fails halfway through a session. A fallback users never notice is worth more than a fast path that occasionally freezes.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/remote-desktop-for-hpc-where-the-milliseconds-go/decode-paths.svg" alt="Hand-drawn diagram of three decode paths. Hardware: GPU decode, convert on the GPU, present, with the frame staying in GPU memory. Software: CPU decode with libavcodec, CPU convert, present. The trap: GPU decode, copy back to system memory, CPU convert, present, where the copy can cost more than the decode saved. A dashed arrow shows falling back from hardware to software." width="1000" height="600" loading="lazy" decoding="async" /><figcaption>Prefer the hardware path, keep the frame on the GPU, and fall back to software without the user noticing.</figcaption></figure>

<h2>Measure each stage, not the whole</h2>
<p>An end-to-end number tells you that something is slow, not what. What helps is per-stage timing:</p>
<ul>
<li>Stamp each frame when it is captured and carry that stamp through the pipeline, so the client can tell how old a frame is when it’s presented.</li>
<li>Record how long encode, decode and present take, separately from network time.</li>
<li>Look at percentiles, not averages. A session with a fine average and a bad 99th percentile frame time is a session that stutters, and stutter is what people complain about.</li>
<li>Log decoder decisions: which path was chosen, and every fallback with its reason.</li>
</ul>
<p>With that in place, most investigations become short. If frames leave the host on time and arrive late, it’s the network. If they arrive on time and are presented late, it’s the client.</p>

<h2>The things that bite</h2>
<p>A few problems come up again and again:</p>
<ul>
<li><strong>Resolution changes.</strong> Plugging in a monitor or resizing a window can force the decoder to reinitialise. If that path isn’t well tested, it’s where sessions freeze.</li>
<li><strong>Multiple monitors.</strong> More pixels, and sometimes several streams, each with its own decoder.</li>
<li><strong>Refresh-rate mismatches.</strong> A stream at one rate shown on a display at another needs careful frame pacing, or motion looks uneven even when no frames are lost.</li>
<li><strong>The first minute.</strong> Scheduling, session start-up and decoder initialisation all happen at once, so a slow start is often several small delays stacked together.</li>
</ul>

<h2>Know which stage you’re in</h2>
<p>Performance work on a remote desktop is mostly about locating the problem. The pipeline view gives you the map, per-stage timing tells you where you are on it, and a reliable fallback keeps users working while you fix the fast path. The fix is rarely where you’d guess first, which is exactly why it pays to measure before you change anything.</p>
]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Building an MCP server for debugging remote desktop sessions]]></title>
            <link>https://www.tarunnahak.in/articles/mcp-server-for-debugging-remote-sessions</link>
            <guid>https://www.tarunnahak.in/articles/mcp-server-for-debugging-remote-sessions</guid>
            <pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[What the Model Context Protocol is, why it suits root-cause analysis, and the rules I follow when designing tools an AI assistant uses to debug remote desktop sessions.]]></description>
            <content:encoded><![CDATA[<p>When a remote desktop session fails, the evidence is scattered. The scheduler knows whether the job started. The session host has its own logs. The client knows which decoder it picked and whether it fell back. The network has an opinion too. Working out what happened means visiting each of those, lining up timestamps and forming a story.</p>
<p>At Siemens I built a diagnosis tool for exactly this, based on the Model Context Protocol, so an AI assistant can gather and correlate that evidence for whoever is investigating. This post covers the general approach: what MCP is, why it suits diagnostics, and the design rules I’d follow for any tool like it.</p>

<h2>What MCP is</h2>
<p>The Model Context Protocol is an open protocol for connecting AI applications to the systems they need to work with. An MCP <em>server</em> exposes capabilities; an MCP <em>client</em>, such as a chat assistant or an IDE, connects to it and makes those capabilities available to the model. The messages are JSON-RPC, carried over standard input and output for local servers or HTTP for remote ones.</p>
<p>A server can offer three kinds of things:</p>
<ul>
<li><strong>Tools</strong>: functions the model can call, each with a name, a description and a JSON Schema for its input.</li>
<li><strong>Resources</strong>: data the client can read, like files or records.</li>
<li><strong>Prompts</strong>: reusable templates for common tasks.</li>
</ul>
<p>For diagnostics, tools do almost all of the work. The appeal is that you write the integration once, as a server, and any MCP-capable assistant can use it.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/mcp-server-for-debugging-remote-sessions/architecture.svg" alt="Hand-drawn diagram of an MCP diagnosis setup. A person talks to an AI assistant, which is the MCP client. It sends JSON-RPC over stdio or HTTP to an MCP server called session-doctor, which exposes four read-only tools: get_session_summary, get_job_status, search_logs and get_codec_stats. The server reads from the scheduler, the session host logs and the client decoder statistics, and redacts data on the way out." width="1000" height="500" loading="lazy" decoding="async" /><figcaption>One MCP server wraps the scattered evidence behind a few narrow, read-only tools.</figcaption></figure>

<h2>Why diagnostics is a good fit</h2>
<p>Root-cause analysis is mostly gathering and cross-referencing: look up the session, check the job, read the logs around the failure, compare with the decoder statistics. Each step is simple; the tedium is in doing all of them and keeping the details straight. That’s a good job for a model with well-designed tools, as long as a person still makes the final call.</p>

<h2>Design rules for diagnostic tools</h2>
<p><strong>Read-only by default.</strong> A diagnosis tool should observe, not act. Restarting a session or killing a job may be the right fix, but those actions belong behind a separate, explicit path with a person in the loop. MCP lets you mark tools with a read-only hint, and clients can use it to decide what needs confirmation.</p>
<p><strong>Narrow tools with typed inputs.</strong> One tool per question works better than one tool that runs arbitrary queries. Something like:</p>
<ul>
<li><code>get_session_summary(session_id)</code>: state, host, timings, codec and decoder, last errors</li>
<li><code>get_job_status(job_id)</code>: what the scheduler knows about the job</li>
<li><code>search_logs(host, since, until, pattern, limit)</code>: a bounded window of matching log lines</li>
<li><code>get_codec_stats(session_id)</code>: frame timings, decoder path and fallbacks</li>
</ul>
<p><strong>Return summaries, not dumps.</strong> Everything a tool returns lands in the model’s context window. Ten thousand raw log lines crowd out the reasoning. Return the relevant part, say how much was left out, and let the model ask for more with explicit limits and pagination.</p>
<p><strong>Write descriptions for the model.</strong> The model picks tools by reading their descriptions, so a description should say when to use the tool, not only what it returns. “Use this first when a user reports a broken session” is more useful than “returns session data”.</p>
<p><strong>Treat errors as data.</strong> “No session with that ID; check the job output for the correct one” lets the model recover. A stack trace doesn’t.</p>
<p><strong>Return the IDs that connect things.</strong> If the session summary includes the job ID and the host, the model can chain into the next tool without guessing.</p>
<p><strong>Redact on the way out.</strong> Logs contain user names, paths and sometimes tokens. Strip what the investigation doesn’t need before it leaves the server.</p>

<h2>A minimal tool</h2>
<p>Here is the shape of one tool using the official TypeScript SDK. The lookup function is a stand-in for whatever read-only data source you have:</p>
<pre><code class="language-ts">import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'

const server = new McpServer({ name: 'session-doctor', version: '0.1.0' })

server.registerTool(
  'get_session_summary',
  {
    description:
      'Summarise one remote desktop session: state, host, codec, decoder ' +
      'and the most recent errors. Use this first when a user reports a ' +
      'broken or slow session.',
    inputSchema: {
      sessionId: z.string().describe('Session ID from the user or the job output'),
    },
    annotations: { readOnlyHint: true },
  },
  async ({ sessionId }) =&gt; {
    const summary = await loadSessionSummary(sessionId)
    if (!summary) {
      return {
        content: [{ type: 'text', text: `No session ${sessionId}. Check the job output for the right ID.` }],
        isError: true,
      }
    }
    return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] }
  },
)

await server.connect(new StdioServerTransport())</code></pre>
<p>Most of the effort goes into <code>loadSessionSummary</code> and its siblings: deciding what a useful summary contains, and keeping it small.</p>

<h2>What an investigation looks like</h2>
<p>Take a typical report: a session froze after a few minutes. With tools like the ones above, the assistant fetches the session summary, sees a decoder fallback shortly before the freeze, pulls the codec statistics to confirm frame times climbing, then searches the client logs in that window and finds the decoder being reinitialised after a display change. The person investigating gets that timeline with the evidence attached, instead of spending the first half hour collecting it.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/mcp-server-for-debugging-remote-sessions/investigation.svg" alt="Hand-drawn diagram of an investigation. A user reports a session froze after a few minutes. The assistant calls get_session_summary and finds a decoder fallback just before the freeze, calls get_codec_stats and sees frame times climbing, then calls search_logs and finds the decoder being reinitialised after a display change. The findings become a timeline with evidence for the person investigating." width="1000" height="520" loading="lazy" decoding="async" /><figcaption>Each tool returns the IDs and clues that lead to the next call.</figcaption></figure>
<p>The assistant doesn’t need to be right every time to be useful. It needs to be fast at gathering, and honest about what it found and what it’s guessing.</p>

<h2>Lessons</h2>
<ul>
<li><strong>Start with three or four tools.</strong> Add more when you see the assistant reach for something it doesn’t have.</li>
<li><strong>Log every tool call.</strong> The transcripts show which tools get used, which get misused, and which descriptions need rewriting.</li>
<li><strong>Test against past incidents.</strong> Replaying problems you’ve already solved is the cheapest way to check that the tools lead to the right answer.</li>
</ul>
<p>MCP doesn’t make diagnosis automatic. It makes the dull part fast, which leaves people more time for the part that needs judgement.</p>
]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Letting AI agents do the boring parts of a service migration]]></title>
            <link>https://www.tarunnahak.in/articles/ai-agents-for-service-migrations</link>
            <guid>https://www.tarunnahak.in/articles/ai-agents-for-service-migrations</guid>
            <pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Agents are good at reading a lot of code and making the same change many times. How I split a migration between agents and people, and the guardrails that keep it safe.]]></description>
            <content:encoded><![CDATA[<p>Most of a service migration isn’t hard. It’s long. You inventory what the old service does, map each piece to the new platform, rewrite the call sites, update configuration and tests, and repeat that across dozens of endpoints. The hard parts, the undocumented behaviour and the trade-offs, are a small fraction of the work, buried under a lot of repetition.</p>
<p>At Siemens I’ve been architecting migrations around agentic workflows, where AI agents analyse services, plan the migration steps and carry out the repetitive refactoring. This post is about how I split that work between agents and people, and the guardrails that make it safe.</p>

<h2>Three jobs, three agents</h2>
<p>Asking one agent to “migrate this service” gives you a large, unreviewable change. Splitting the work into separate jobs, each with its own output, works much better.</p>
<p><strong>The analyst</strong> reads the service and produces an inventory: endpoints, their inputs and outputs, dependencies on other services, data stores, configuration, background jobs. It doesn’t change anything. Its output is a structured document that a person can check against what they know.</p>
<p><strong>The planner</strong> turns the inventory into an ordered list of small steps. Each step says what changes, which files it touches, and how to verify it: which tests must pass, which behaviour must stay the same.</p>
<p><strong>The executor</strong> takes one step at a time. It makes the change on a branch, runs the checks, and opens a pull request. Then it stops.</p>
<p>People review the inventory, approve the plan, and review every pull request. The agents do the reading and typing; people do the deciding.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/ai-agents-for-service-migrations/three-agents.svg" alt="Hand-drawn diagram of three agents. The analyst reads the legacy service and writes inventory.json, which a person checks. The planner turns it into plan.json, a list of small steps with checks, which a person approves. The executor takes one step at a time and opens a pull request, which a person reviews." width="1000" height="540" loading="lazy" decoding="async" /><figcaption>Agents do the reading and typing; people do the deciding.</figcaption></figure>

<h2>Structured output between stages</h2>
<p>The inventory and the plan should be data, not prose. A JSON document with a fixed schema can be validated, diffed when the agent reruns, and fed to the next stage without anything getting lost in paraphrase. When the planner refers to an endpoint, it refers to an entry in the inventory, not to its own description of one.</p>
<p>This also makes review faster. Checking forty inventory entries in a table is easier than reading four pages of an agent’s summary.</p>

<h2>Guardrails</h2>
<p><strong>Tests are the contract.</strong> An agent can’t preserve behaviour nobody has written down. If the old service lacks tests, the first steps of the plan are characterisation tests that capture what it does today, including the odd parts. Every later step has to keep them green.</p>
<p><strong>Small diffs.</strong> If a pull request takes longer to review than it would have taken to write by hand, the step was too big. Small steps also make failures cheap: you throw away one branch, not a week of work.</p>
<p><strong>Use deterministic tools for deterministic work.</strong> Renaming a package or updating an import path across a codebase is a job for a codemod, a compiler or a formatter, not for a model retyping files. The agent’s job is to decide which tool to run and check the result. That keeps changes reproducible and avoids the subtle typos that come from regenerating code.</p>
<p><strong>Limited permissions.</strong> Agents work in a sandbox with access to the repository and the test runner. They can open pull requests. They can’t merge, deploy or touch production.</p>
<p><strong>Log everything.</strong> Every tool call and every decision goes into a log you can read afterwards. When a step goes wrong, the log tells you whether the plan was wrong or the execution was.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/ai-agents-for-service-migrations/guardrails.svg" alt="Hand-drawn diagram of the guardrails around an executor agent. Inside a sandbox it can use the repository, the test runner and deterministic tools such as codemods, compilers and formatters, and every tool call goes to a log. Through the sandbox wall it may open a pull request, but merging, deploying and touching production are blocked." width="1000" height="450" loading="lazy" decoding="async" /><figcaption>The executor works in a sandbox. It can open a pull request and nothing more.</figcaption></figure>

<h2>What stays with people</h2>
<p>Agents are good at reading a lot of code quickly and at making the same kind of change many times. They are not good at knowing what isn’t in the code:</p>
<ul>
<li>behaviour that clients depend on but nobody documented,</li>
<li>operational context, like which endpoint gets hammered at the end of every month,</li>
<li>trade-offs between doing it properly and doing it now,</li>
<li>who owns what, and who needs to be told.</li>
</ul>
<p>Those questions come up during inventory review and planning, which is exactly why people sign off on both before any code changes.</p>

<h2>Is it working?</h2>
<p>Two measures tell you a lot. The first is how many agent pull requests merge without changes; if reviewers keep rewriting them, the steps are too vague or the checks too weak. The second is review time per pull request; if it creeps up, the steps are getting too big.</p>
<p>The tooling matters less than the boundaries. Frameworks like Google’s Agent Development Kit make it easy to wire agents and tools together, and protocols like MCP give agents typed access to the systems around them. But what makes the workflow trustworthy is the shape of the work: structured hand-offs, small verified steps, and a person approving every change that ships.</p>
<p>Used that way, agents don’t replace the engineers on a migration. They take away the part of the job that nobody wanted, and leave the part that needed an engineer in the first place.</p>
]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Zero-downtime migrations from legacy services to Go and Kotlin]]></title>
            <link>https://www.tarunnahak.in/articles/zero-downtime-migrations-to-go-and-kotlin</link>
            <guid>https://www.tarunnahak.in/articles/zero-downtime-migrations-to-go-and-kotlin</guid>
            <pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How to replace a legacy service while it keeps serving traffic: a routing layer, recorded behaviour, shadow traffic, gradual cutovers and a rollback you’ve actually rehearsed.]]></description>
            <content:encoded><![CDATA[<p>At Mercedes-Benz R&amp;D India I drove the migration of several legacy services to Go and Kotlin. The rule for all of them was the same: users must not notice. No maintenance window, no “please try again later”, no big-bang release night.</p>
<p>That rule shapes everything about how you migrate. You can’t replace a service that is serving traffic in one move, so you replace it in many small ones, each of which you can undo. This post is the sequence I follow.</p>

<h2>1. Put a routing layer in front</h2>
<p>Before writing any new code, make sure every request to the old service passes through something you control: an API gateway, a reverse proxy, or a thin routing service. Clients keep calling the same address; the routing layer decides which implementation answers.</p>
<p>This is the strangler fig pattern. The new service grows around the old one, route by route, until the old one has nothing left to do. The routing layer is what makes each step reversible, so it’s worth getting in place first, even though it changes nothing visible.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/zero-downtime-migrations-to-go-and-kotlin/routing-layer.svg" alt="Hand-drawn diagram of the strangler fig pattern. Clients keep calling the same address. A routing layer with a flag per route sends /users and /search to the legacy service, splits /cart 90 percent legacy and 10 percent new, and sends /orders and /accounts to the new Go or Kotlin service." width="1000" height="500" loading="lazy" decoding="async" /><figcaption>Clients keep the same address. The routing layer moves one route at a time, and each move can be undone.</figcaption></figure>

<h2>2. Write down what the old service actually does</h2>
<p>Legacy services do things nobody remembers asking for: a field that is always returned in a certain order, an error code a client parses, a trailing slash that is accepted on one route and not another. Clients depend on these.</p>
<p>So before rebuilding a route, capture its behaviour. Contract tests built from real requests and responses work well, with sensitive data scrubbed. They become the definition of done for the new implementation.</p>

<h2>3. Rebuild route by route</h2>
<p>Pick a route that is low risk and well understood, and build it in the new service. Resist the urge to fix its design while you’re there. A migration that also changes behaviour is two projects, and when something breaks you won’t know which one caused it. Improve the design after the cutover, when the old implementation is gone.</p>

<h2>4. Shadow traffic before real traffic</h2>
<p>Once a route passes its contract tests, have the routing layer mirror real requests to the new implementation while the old one keeps answering. The client gets the old response; the new response is compared with it in the background and then discarded.</p>
<p>The comparisons find what the tests missed. Expect noise: timestamps, generated IDs and field ordering will differ legitimately, so the comparer needs rules for what to ignore. Everything else is either a bug in the new service or a behaviour of the old one you didn’t know about, and both are worth knowing before any user sees a response from the new code.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/zero-downtime-migrations-to-go-and-kotlin/shadow-traffic.svg" alt="Hand-drawn diagram of shadow traffic. The routing layer sends each request to the legacy service, whose response goes back to the client, and mirrors a copy to the new service. Both responses go to a comparer that ignores timestamps, generated IDs and field order. The new response is then discarded; every remaining difference is a bug or an undocumented behaviour." width="1000" height="500" loading="lazy" decoding="async" /><figcaption>Shadowing: the client always gets the old response, and the new one is only compared.</figcaption></figure>
<p>Shadowing only works for reads, or for writes you can make safely twice. For anything with side effects, like sending an email or charging a card, the shadow copy has to stop short of the side effect.</p>

<h2>5. Decide where the data lives</h2>
<p>Data is usually the hardest part. The simplest option is for old and new services to share the same database during the migration, so there is one source of truth and nothing to reconcile. If the new service needs its own store, don’t write to both from application code; a crash between the two writes leaves them disagreeing. Write to one and propagate changes to the other, through an outbox table or change data capture, then backfill history and check the two stores match before switching reads.</p>

<h2>6. Cut over gradually</h2>
<p>Move real traffic in steps: one percent, then ten, then half, then all of it, with a flag per route in the routing layer. At each step, watch the numbers that matter to users: error rate, 99th percentile latency, and whatever business metric the route drives. Move on only when they look the same as before.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/zero-downtime-migrations-to-go-and-kotlin/cutover.svg" alt="Hand-drawn staircase of a gradual cutover: 1 percent, 10 percent, 50 percent, then 100 percent of traffic on the new service, checking error rate, 99th percentile latency and the business metric before each step. A dashed red arrow shows rollback as one configuration change at any step. At 100 percent, the old code is deleted." width="1000" height="450" loading="lazy" decoding="async" /><figcaption>Move traffic in steps, check the numbers users feel at each one, and keep a rehearsed one-line rollback.</figcaption></figure>

<h2>7. Rehearse the rollback</h2>
<p>Rolling back should be one configuration change in the routing layer, and you should have done it at least once on purpose before you need it. A rollback path that has never been exercised is a hope, not a plan.</p>

<h2>8. Finish the job</h2>
<p>When a route has run on the new service for long enough to trust it, delete the old code for it, remove its flag and its shadowing rules. Migrations that stall at 95 percent are common, and they leave you running two systems indefinitely, which is worse than either one alone.</p>

<h2>Why Go and Kotlin</h2>
<p>Each target language suited different services. Go fits small, network-heavy services well: fast start-up, low memory use, a strong standard library for HTTP, and goroutines that make concurrent I/O straightforward. Kotlin made sense where teams depended on JVM libraries and frameworks: it keeps all of that and adds null safety, concise syntax and coroutines.</p>
<p>The language mattered less than the sequence, though. Routing layer, recorded behaviour, route-by-route rebuilds, shadow traffic, gradual cutover and a rehearsed rollback would have worked just as well for any other target. They’re what turn a risky rewrite into a series of small, boring changes, and boring is exactly what you want from a migration.</p>
]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Idempotent consumers: the Kafka lesson I keep relearning]]></title>
            <link>https://www.tarunnahak.in/articles/idempotent-kafka-consumers</link>
            <guid>https://www.tarunnahak.in/articles/idempotent-kafka-consumers</guid>
            <pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Kafka delivers at least once, so your consumer will see duplicates. The patterns I use so that every event is processed exactly once in effect.]]></description>
            <content:encoded><![CDATA[<p>Most of the event-driven services I built at Mercedes-Benz R&amp;D India ran on Kafka, in Go, Kotlin, Rust and Python. Across all of them, one lesson came up more than any other: your consumer will see the same event more than once, so it had better not matter.</p>
<p>This post explains why duplicates happen and the patterns I use so that every event is processed exactly once in effect, even when it’s delivered twice.</p>

<h2>Why duplicates happen</h2>
<p>A Kafka consumer reads a message, processes it, and commits its offset so it won’t read that message again. The trouble is the gap between processing and committing. If the consumer crashes, is restarted by a deploy, or loses its partition in a rebalance after processing a message but before committing the offset, the next consumer to own that partition starts from the last committed offset and processes the message again.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/idempotent-kafka-consumers/duplicates.svg" alt="Hand-drawn diagram of how a duplicate happens. A partition holds offsets 39 to 46 and the committed offset points at 42. Consumer A reads 42, processes it and writes to the database, then crashes before committing. Consumer B resumes from the committed offset, processes 42 again, and commits." width="1000" height="540" loading="lazy" decoding="async" /><figcaption>The gap between processing and committing is where duplicates come from.</figcaption></figure>
<p>Committing before processing just swaps one problem for another: crash at the wrong moment and the message is never processed at all. Producers add duplicates of their own when they retry a send whose acknowledgement got lost.</p>
<p>So the realistic guarantee for the system as a whole is at-least-once delivery. Kafka does offer exactly-once semantics through idempotent producers and transactions, but that covers reading from Kafka and writing back to Kafka. The moment your consumer writes to a database or calls another service, you’re outside it.</p>

<h2>Make processing idempotent</h2>
<p>An operation is idempotent if doing it twice has the same effect as doing it once. The goal is to make each consumer’s effect idempotent, so redelivery is harmless.</p>
<p><strong>Give every event an ID.</strong> The producer assigns a unique ID when the event is created, not when it’s sent, so retries carry the same ID. Put it in a header or in the payload.</p>
<p><strong>Record processed IDs in the same transaction as the effect.</strong> If the consumer writes to a relational database, keep a table of processed event IDs and insert into it in the same transaction as the real change. A duplicate hits the primary key and the whole thing is skipped. Because both writes commit together, there’s no window where the effect happened but the ID wasn’t recorded.</p>
<pre><code class="language-go">func (h *Handler) Handle(ctx context.Context, msg kafka.Message) error {
	tx, err := h.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	// the event ID is the idempotency key; a redelivered event inserts nothing
	res, err := tx.ExecContext(ctx,
		`INSERT INTO processed_events (event_id) VALUES ($1) ON CONFLICT DO NOTHING`,
		eventID(msg))
	if err != nil {
		return err
	}
	if n, err := res.RowsAffected(); err != nil || n == 0 {
		return err // nil when it's a duplicate: already handled
	}

	if err := h.apply(ctx, tx, msg); err != nil {
		return err
	}
	return tx.Commit()
}</code></pre>
<p>The offset is committed only after <code>Handle</code> returns without an error. If the process dies anywhere in between, the transaction rolls back and the message is simply processed again later, safely.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/idempotent-kafka-consumers/idempotent-handler.svg" alt="Hand-drawn flowchart of an idempotent consumer. A message with an event ID enters one database transaction: insert the event ID into processed_events; if a new row was inserted, apply the change and commit both together; if not, it is a duplicate and is skipped. Only then is the Kafka offset committed. A crash anywhere before the commit rolls back and the message is processed again safely." width="1000" height="560" loading="lazy" decoding="async" /><figcaption>The processed-ID insert and the real change commit together, and the offset is committed only after that.</figcaption></figure>
<p><strong>Or make the write itself idempotent.</strong> Sometimes you don’t need a separate table. Setting a value is naturally idempotent where incrementing one isn’t: “set the order status to shipped” can run twice, “add 10 to the balance” can’t. Upserts keyed on a natural ID, and updates guarded by a version number so an older event can’t overwrite a newer state, cover a lot of cases.</p>

<h2>Producing events safely: the outbox</h2>
<p>The mirror-image problem appears when a service changes its database and publishes an event about it. Write to the database and then publish, and a crash in between loses the event. Publish first, and a failed database write leaves an event announcing something that never happened.</p>
<p>The outbox pattern solves it. In the same transaction as the business change, insert the event into an outbox table. A separate relay reads the outbox and publishes to Kafka, marking rows as sent. The relay might publish an event twice if it crashes after sending but before marking, which is fine, because the consumers are idempotent.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/idempotent-kafka-consumers/outbox.svg" alt="Hand-drawn diagram of the outbox pattern. The order service writes the business change to the orders table and the event to the outbox table in one transaction. A relay reads the outbox, publishes to a Kafka topic and marks rows as sent. Idempotent consumers read from the topic. The relay may publish an event twice, which is fine." width="1000" height="450" loading="lazy" decoding="async" /><figcaption>The outbox: the change and the event commit together, and a relay publishes the event afterwards.</figcaption></figure>

<h2>Ordering, retries and poison messages</h2>
<ul>
<li><strong>Ordering.</strong> Kafka orders messages only within a partition. If events about the same entity must be processed in order, use the entity’s ID as the message key so they land on the same partition.</li>
<li><strong>Retries.</strong> Transient failures, like a database timeout, deserve a few retries with backoff, inside the consumer, before giving up.</li>
<li><strong>Poison messages.</strong> A message that can never be processed, because it’s malformed or breaks an assumption, will block its partition forever if you keep retrying it. After a bounded number of attempts, move it to a dead-letter topic with the error attached, alert on it, and keep going.</li>
</ul>

<h2>Watch consumer lag</h2>
<p>Consumer lag, the distance between the newest message in a partition and the consumer’s committed offset, is the single most useful health metric for an event-driven service. Growing lag means the consumer can’t keep up or is stuck on something. A partition whose lag climbs while the others stay flat usually points to a poison message or a hot key.</p>

<h2>The habit</h2>
<p>None of these patterns is complicated. The lesson I keep relearning is to assume duplicates from the first line of a consumer, not to add idempotency after the first incident. Ask “what happens if this runs twice?” in every review, and event-driven systems become a lot less surprising.</p>
]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[What teaching 100+ students taught me about explaining code]]></title>
            <link>https://www.tarunnahak.in/articles/what-teaching-taught-me-about-explaining-code</link>
            <guid>https://www.tarunnahak.in/articles/what-teaching-taught-me-about-explaining-code</guid>
            <pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Lessons from more than 250 hours of teaching and a year of mentoring developers, and how they changed the way I review, name and design code.]]></description>
            <content:encoded><![CDATA[<p>Between 2021 and 2023 I spent a lot of my evenings teaching. At Growing Stars I taught more than 100 students across the United States, in over 250 hours of live sessions on Python, JavaScript, and data structures and algorithms. After that I spent a year as a MERN stack mentor at GeeksforGeeks, helping students and working professionals with problem solving, coding and system design. I still run mock interviews and publish the material I use for them.</p>
<p>Teaching changed how I explain things, and it changed how I write code. These are the lessons that stuck.</p>

<h2>Start with an example, then name the idea</h2>
<p>The instinct is to define a concept and then show an example of it. It almost always works better the other way round. Show a function that calls itself to walk a folder tree, let people follow it, and only then say the word “recursion”. The name lands on something they already understand instead of floating in the air waiting for an example to attach to.</p>

<h2>Ask for a prediction before the answer</h2>
<p>When someone asks why their code doesn’t work, the fastest route is rarely to tell them. Ask them what they expect it to print, and then run it. The gap between the prediction and the output is exactly where their mental model is wrong, and finding it themselves sticks far better than any explanation I could give.</p>

<h2>Let people get stuck, but not for long</h2>
<p>Struggling with a problem is how the learning happens, and solving it for someone takes that away. But being stuck for too long just turns into frustration. What worked for me was a ladder of hints: first a question that points in the right direction, then a smaller version of the problem, then the key insight, and only at the very end the solution.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/what-teaching-taught-me-about-explaining-code/hint-ladder.svg" alt="Hand-drawn staircase of hints. From &quot;stuck&quot; at the bottom, each step gives a little more: first a question that points the right way, then a smaller version of the problem, then the key insight, and only at the top the solution. A note says to stop climbing as soon as they are unstuck." width="1000" height="470" loading="lazy" decoding="async" /><figcaption>A ladder of hints: give the smallest one that gets them moving again.</figcaption></figure>

<h2>Draw the state</h2>
<p>A lot of confusion comes from state people can’t see. For recursion, a table with one row per call and a column per variable makes the call stack visible. For pointers and references, boxes and arrows. For system design, the path of one request through every component, drawn before discussing how to scale anything. Once the state is on the page, most questions answer themselves.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/what-teaching-taught-me-about-explaining-code/call-stack-table.svg" alt="Hand-drawn sketch of a recursive factorial function next to a table with one row per call and a column per variable. fact(3) has n equal to 3 and waits for fact(2), which waits for fact(1), which returns 1. The returns flow back up: fact(2) returns 2 and fact(3) returns 6." width="1000" height="450" loading="lazy" decoding="async" /><figcaption>Draw the state: one row per call, one column per variable, and the call stack stops being invisible.</figcaption></figure>

<h2>Get to something that works early</h2>
<p>Motivation is a resource, and nothing replenishes it like seeing your own program work. The best sessions got students to a running result quickly, even a crude one, and improved it from there. That’s also how I approach new projects now: an ugly end-to-end version first, then refinement.</p>

<h2>Make feedback specific</h2>
<p>In mock interviews, “you did well” and “work on your communication” help nobody. A scorecard with concrete criteria, such as clarifying the requirements, explaining the approach before coding, testing with an edge case and discussing complexity, makes feedback something a person can act on. It also makes me more consistent: two candidates who do the same thing get the same feedback. The mock interview kit on my GitHub grew out of exactly that.</p>

<h2>How it changed my engineering</h2>
<p>Teaching taught me that if I can’t explain something simply, I probably don’t understand it well enough yet. That shows up in my work every day:</p>
<ul>
<li><strong>Code reviews.</strong> I try to explain the why behind a suggestion, and ask questions rather than issue verdicts, the same way I would with a student.</li>
<li><strong>Naming.</strong> A name is a tiny explanation. If a function is hard to name, it usually does too much.</li>
<li><strong>Design documents.</strong> I write them for someone joining the team next year, not for the people in the room today. That forces me to state assumptions I would otherwise leave implicit.</li>
<li><strong>Debugging.</strong> “What did you expect, and what happened instead?” is as useful a question for a production incident as it is for a beginner’s homework.</li>
</ul>

<h2>Why I keep doing it</h2>
<p>Every time I explain something, I find a gap in my own understanding. Students ask the questions experienced engineers have stopped asking, and answering them properly keeps me honest. That’s the real reason I’ve kept teaching alongside engineering, and why the low-level design and interview material I use is open on GitHub for anyone who wants it.</p>
]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Mastering Precision: Understanding PostgreSQL’s NUMERIC Type and How It Stands Out]]></title>
            <link>https://www.tarunnahak.in/articles/postgresql-numeric-type</link>
            <guid>https://www.tarunnahak.in/articles/postgresql-numeric-type</guid>
            <pubDate>Mon, 02 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[What PostgreSQL’s NUMERIC type is, how it differs from the floating-point types, when exact precision is worth its cost, and tips for using it well.]]></description>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*w0J1OHWB_cuAgkvB-KfVeg.jpeg" /><figcaption>Postgres Numeric type</figcaption></figure><p>When working with databases, accuracy and performance are pivotal, especially when handling numbers. PostgreSQL, a robust and versatile relational database, offers several numeric data types, each tailored to different use cases. Among these, the NUMERIC type shines as a tool for handling precise calculations. In this post, we’ll explore the NUMERIC type, its benefits, and how it compares to other numeric types in PostgreSQL.</p><h2>What is the NUMERIC Type in PostgreSQL?</h2><p>The NUMERIC type in PostgreSQL is a high-precision, arbitrary-scale data type designed to store numbers without losing accuracy. It is ideal for use cases where exactness is non-negotiable, such as:</p><ul><li><strong>Financial transactions</strong></li><li><strong>Scientific calculations</strong></li><li><strong>Engineering applications</strong></li></ul><p>With NUMERIC, you can specify precision (total number of digits) and scale (number of digits after the decimal point). For example:</p><pre>CREATE TABLE transactions (<br>    id SERIAL PRIMARY KEY,<br>    amount NUMERIC(10, 2) -- up to 10 digits, 2 of which are after the decimal<br>);</pre><p>This ensures that no rounding errors creep into calculations, making it perfect for critical systems.</p><h2>Key Features of NUMERIC</h2><ol><li><strong>User-Defined Precision and Scale</strong><br>You can tailor the type to fit your specific requirements:</li></ol><ul><li>NUMERIC(10, 2) allows values like 12345.67.</li><li>Without specification, NUMERIC can hold any value with arbitrary precision.</li></ul><ol><li><strong>Guaranteed Accuracy</strong><br>Unlike floating-point types (REAL, DOUBLE PRECISION), which approximate values, NUMERIC stores data exactly as entered.</li><li><strong>Extensive Range</strong><br>With a virtually unlimited range of values, NUMERIC can handle data that exceeds the capacity of standard integer types.</li></ol>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/postgresql-numeric-type/numeric-vs-float.svg" alt="Hand-drawn comparison. In DOUBLE PRECISION, 0.1 plus 0.2 gives 0.30000000000000004, close but not exact. In NUMERIC, 0.1 plus 0.2 gives exactly 0.3. Below, NUMERIC(10, 2) is drawn as ten digit boxes, 12345678.90, with a brace showing precision is 10 digits in total and another showing scale is the 2 digits after the decimal point." width="1000" height="470" loading="lazy" decoding="async" /><figcaption>Floating point approximates; NUMERIC stores exactly the digits you asked for.</figcaption></figure>
<h2>When to Use NUMERIC</h2><p>The NUMERIC type is your go-to choice when precision is a top priority. Here are some specific scenarios:</p><ol><li><strong>Finance</strong><br>Calculating interest rates, balances, and taxes requires precision. Even tiny rounding errors can lead to significant discrepancies.</li><li><strong>Scientific Research</strong><br>Fields like physics or chemistry often demand exactness in measurements and calculations.</li><li><strong>Compliance</strong><br>Industries with regulatory requirements (e.g., healthcare, banking) may require the use of precise, non-approximated data storage.</li></ol><h2>Performance Considerations</h2><p>While the NUMERIC type ensures precision, it comes with a trade-off: performance. It uses more storage than fixed-width types (INTEGER, BIGINT) and takes longer to process due to its flexible precision. For high-speed applications where slight inaccuracies are tolerable, floating-point types like REAL or DOUBLE PRECISION may be preferable.</p><p>To optimize performance:</p><ul><li>Use NUMERIC only when precision is required.</li><li>Leverage indexes to speed up searches and calculations on NUMERIC columns.</li></ul><h2>Tips for Using NUMERIC Effectively</h2><ol><li><strong>Specify Precision and Scale Where Possible</strong><br>Defining precision and scale not only improves clarity but also optimises storage.</li></ol><pre>CREATE TABLE orders ( order_id SERIAL PRIMARY KEY, total_price NUMERIC(15, 2) );</pre><ol><li><strong>Avoid Overusing It</strong><br>Use NUMERIC sparingly for large-scale datasets that don’t need exact precision to save storage and processing power.</li><li><strong>Combine with Constraints</strong><br>Add constraints to ensure data integrity. For example:</li></ol><pre>CHECK (amount &gt;= 0)</pre><h2>Conclusion</h2><p>PostgreSQL’s NUMERIC type is a powerful ally for applications that demand precision. Its ability to handle arbitrary precision and scale makes it indispensable for fields like finance and science. However, it’s essential to balance its accuracy with the performance demands of your application.</p><p>By understanding the strengths and trade-offs of NUMERIC and its alternatives, you can make informed decisions that optimize both the reliability and efficiency of your database. For developers who value precision, NUMERIC is not just a data type—it’s peace of mind.</p><p><em>Originally published on <a href="https://medium.com/@tarunnahak/mastering-precision-understanding-postgresqls-numeric-type-and-how-it-stands-out-09ec960ab6ae">Medium</a>.</em></p>]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Golang and Scalability: Building High-Performance Systems]]></title>
            <link>https://www.tarunnahak.in/articles/golang-and-scalability</link>
            <guid>https://www.tarunnahak.in/articles/golang-and-scalability</guid>
            <pubDate>Sat, 09 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Why Go suits scalable backends: its concurrency model, real-world case studies, the patterns that help services scale, and how to optimise performance.]]></description>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-saQy-UyDtKiS_dpikRnyQ.jpeg" /><figcaption>Golang for Performance</figcaption></figure><h2>Introduction</h2><p>In today’s world of rapid tech growth, scalability is essential. Modern applications need to handle increasing traffic, complex data processing, and real-time demands without compromising performance. <strong>Golang</strong>, or <strong>Go</strong>, has emerged as a powerful tool for building scalable, high-performance systems. Its lightweight nature, efficient concurrency model, and simple syntax make it a go-to choice for backend development, cloud-native services, and microservices architectures.</p><p>This article delves into why Go is well-suited for scalable applications, exploring its core features, concurrency handling, garbage collection, and real-world examples from companies that rely on Go for their high-traffic systems. Whether you’re starting with Go or looking to deepen your understanding of its scalability strengths, this article offers insights and best practices for leveraging Go in scalable system design.</p><h2>1. Why Go is Designed for Scalability</h2><p>Go was designed by Google engineers for large-scale systems. It emphasizes simplicity and speed, with specific features that make it ideal for scalable architecture:</p><ul><li><strong>Lightweight Goroutines</strong>: Goroutines are Go’s method for handling concurrency. Unlike traditional threads, they are lightweight and don’t require large memory allocation, which allows you to manage thousands or even millions of concurrent tasks.</li><li><strong>Efficient Garbage Collection</strong>: Go’s garbage collector is optimized for concurrent workloads. It reduces memory leaks by reclaiming unused memory, which is crucial for long-running applications that handle a lot of traffic.</li><li><strong>Minimalist Syntax</strong>: Go’s simple syntax improves readability and maintainability, which is essential for scaling projects and teams.</li><li><strong>Compilation Speed</strong>: Go’s compiler is fast, allowing developers to quickly iterate and deploy, which is valuable in high-growth environments.</li></ul><p>With these foundational features, Go helps companies develop applications that can scale both in terms of performance and development agility.</p><h2>2. Concurrency and Parallelism in Go</h2><p>Concurrency is a cornerstone of scalability, and Go’s concurrency model is one of its standout features.</p><ul><li><strong>Goroutines and Channels</strong>: Goroutines are functions that run independently of the main thread, allowing applications to handle numerous tasks concurrently. Channels provide a means of safe communication between goroutines, enabling structured concurrency that prevents common issues in multithreaded programming.</li><li><strong>Cost-Effective Parallel Processing</strong>: Unlike languages that rely on OS-level threads, goroutines are managed by Go’s runtime. This lowers the cost of context-switching, reducing the overhead of running multiple tasks and making parallel processing efficient for applications with large workloads.</li><li><strong>Practical Example</strong>: In a web server, each request can be handled by a separate goroutine. This allows the server to handle thousands of requests simultaneously without stalling, providing a seamless experience for users even during high-traffic periods.</li></ul><p>By adopting Go’s concurrency tools, developers can build systems that handle concurrent tasks efficiently, meeting high demand without increased complexity or resource consumption.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/golang-and-scalability/goroutines.svg" alt="Hand-drawn diagram of Go concurrency. Each incoming request gets its own goroutine, drawn as many small circles, some talking through a channel. The Go runtime scheduler multiplexes these goroutines onto a few OS threads, which run on the CPU cores. A note compares a goroutine, which starts with a stack of a few kilobytes that grows as needed, with an OS thread, whose fixed stack is often megabytes." width="1000" height="520" loading="lazy" decoding="async" /><figcaption>Many cheap goroutines, a few OS threads, and the Go scheduler in between.</figcaption></figure>
<h2>3. Real-World Scalability with Go: Case Studies</h2><p>Numerous tech giants use Go to scale their applications to millions of users. Let’s explore some notable examples:</p><ul><li><strong>Google</strong>: Google uses Go for many of its internal systems, given Go’s strong support for high-performance, concurrent applications. Services like Kubernetes also rely on Go, demonstrating its effectiveness in handling complex, distributed systems.</li><li><strong>Uber</strong>: Uber chose Go to build its geofence service. Go’s low-latency concurrency allowed Uber to handle location-based traffic efficiently, ensuring ride requests and driver availability updates are processed in real time.</li><li><strong>Dropbox</strong>: Dropbox migrated to Go for its backend infrastructure, leading to more manageable code and improved performance. Go’s simple syntax helped Dropbox developers enhance team productivity as they scaled the service.</li><li><strong>Netflix</strong>: Netflix uses Go for tasks that require high concurrency, such as handling network operations in its streaming services. Go’s lightweight concurrency allows Netflix to manage numerous simultaneous connections, providing smooth streaming experiences.</li></ul><p>These examples illustrate how Go’s concurrency, simplicity, and performance have enabled some of the most popular and high-traffic services to scale effectively.</p><h2>4. Scalable Patterns and Practices in Go</h2><p>Building a scalable application with Go requires adopting design patterns and best practices that suit its strengths. Here are a few key patterns:</p><ul><li><strong>Microservices</strong>: Go is widely used for microservices due to its performance and modularity. Each service can run independently, making it easier to scale horizontally by adding more instances as demand grows.</li><li><strong>Load Balancing and Caching</strong>: Implement caching mechanisms and load balancers to distribute the workload across multiple services. This optimises resources, enabling systems to handle increased load without downtime.</li><li><strong>Graceful Shutdowns and Signal Handling</strong>: Gracefully shutting down services is crucial for scaling applications in production. Using context and signal packages in Go, developers can manage shutdowns, preventing abrupt service interruptions during high loads.</li><li><strong>Testing for Scalability</strong>: Performance testing tools like GoBench and GoTest help identify bottlenecks and optimize performance under load. Writing load tests allows developers to simulate real-world traffic and make adjustments before scaling applications in production.</li></ul><p>With these patterns, you can create scalable Go applications ready to handle growth, adapt to demand, and maintain consistent performance.</p><h2>5. Optimising Performance in Go for Scalability</h2><p>Performance optimisation is crucial for scalability, and Go provides several tools and techniques to streamline this process:</p><ul><li><strong>Profiling Tools</strong>: Go’s pprof tool can profile CPU, memory, and goroutine usage, allowing developers to identify bottlenecks. For more granular insights, the trace package can analyze function call times and memory allocations.</li><li><strong>Reducing Memory Leaks</strong>: Go’s garbage collection helps reduce memory leaks, but developers should still watch for resource-hogging patterns. For instance, using sync.Pool to reuse temporary objects can help minimise memory usage in concurrent applications.</li><li><strong>Optimising Network Calls</strong>: For microservices or web applications, optimising network calls using HTTP keep-alives and persistent connections can reduce latency. Additionally, using efficient JSON encoding/decoding libraries like easyjson can help handle large amounts of data without adding excessive latency.</li></ul><p>These optimisations can ensure your Go applications maintain high performance as they scale, providing users with faster, more responsive services.</p><h2>Conclusion</h2><p>Go’s design philosophy, rooted in simplicity and efficiency, makes it an excellent choice for building scalable systems. Its concurrency model, efficient garbage collection, and straightforward syntax enable developers to build high-performance applications that grow alongside user demand. Companies like Google, Uber, Dropbox, and Netflix have harnessed Go’s capabilities to build and scale their services effectively.</p><p>Whether you’re building a microservice architecture, handling real-time data processing, or just starting with Go, embracing Go’s strengths and best practices will help you create scalable solutions tailored for today’s high-demand environment. The Go ecosystem is continually evolving, so keep exploring and optimizing as you scale your applications to new heights.</p><p><em>Originally published on <a href="https://medium.com/@tarunnahak/golang-and-scalability-building-high-performance-systems-850004b92952">Medium</a>.</em></p>]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
        <item>
            <title><![CDATA[Advanced Tools for Boosting Productivity in Go-lang]]></title>
            <link>https://www.tarunnahak.in/articles/advanced-go-productivity-tools</link>
            <guid>https://www.tarunnahak.in/articles/advanced-go-productivity-tools</guid>
            <pubDate>Fri, 08 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[A tour of Go tools beyond go run and go test: golangci-lint, Testify and Ginkgo, pprof and trace, dependency graphs, code generation and documentation.]]></description>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YKiBLu7RZKsYCK_T6Pzlxw.jpeg" /><figcaption>Advance go lang Tools</figcaption></figure><h2>Introduction</h2><p>In recent years, Go has risen in popularity as a powerful language for backend and cloud-native applications. While many developers are aware of Go’s fundamental tools (go run, go test, go fmt, etc.), fewer know about the advanced tools that can supercharge productivity and elevate the quality of Go applications. This article will introduce a curated list of advanced tools that cover various stages of the Go development lifecycle, from dependency management to code analysis, testing, and performance optimisation.</p><p>By integrating these tools, Go developers can speed up workflows, catch issues early, and fine-tune applications for better performance. Whether you’re building high-scale micro-services or diving into Go for your next project, these tools will help you get the most out of the Go ecosystem.</p>
<figure class="sketch"><img src="https://www.tarunnahak.in/images/articles/advanced-go-productivity-tools/tool-loop.svg" alt="Hand-drawn loop of the Go development cycle with the tools for each stage: dependencies with go mod, generate with Goa and Buffalo, lint with golangci-lint, test with Testify and Ginkgo, profile with pprof and trace, understand the structure with godepgraph and go-callvis, and document with godoc and go-swagger." width="1000" height="560" loading="lazy" decoding="async" /><figcaption>Where each tool in this post fits in the Go development loop.</figcaption></figure>
<h2>1. Dependency Management: Go Modules and Gopkg.lock</h2><p>Go Modules, introduced in Go 1.11, is the go-to (pun intended) dependency management system for Golang. While Go Modules is familiar to many, understanding advanced usage of go.mod and go.sum can simplify complex projects.</p><ul><li><strong>Go Modules Basics</strong>: go mod init initialises a module, while go mod tidy cleans up dependencies.</li><li><strong>Advanced Tips</strong>: Use go mod vendor to create a local vendor folder, ensuring dependencies are locked and offline-ready for CI/CD pipelines.</li><li><strong>Version Control</strong>: Tools like modver can help validate version constraints, avoiding potential dependency issues.</li></ul><p>Beyond Go Modules, some developers find gopkg.lock (from dep) helpful for legacy projects. Using these alongside Go Modules can provide finer control over dependencies in mono-repos or legacy codebases.</p><h2>2. Code Linting and Static Analysis: golangci-lint</h2><p>Writing clean, maintainable code is critical, and linting tools help enforce best practices. golangci-lint is one of the most powerful tools for Go, offering static analysis and linting with minimal configuration.</p><ul><li><strong>Why </strong><strong>golangci-lint?</strong> It combines multiple linters (like errcheck, gosimple, unused) into a single tool, making it easy to catch potential issues.</li><li><strong>Features</strong>: Supports custom rules, plugins, and inline //nolint comments for more control.</li><li><strong>Performance</strong>: golangci-lint caches results and uses concurrent processing, which makes it faster for large projects.</li></ul><p>To integrate, simply run golangci-lint run in your terminal, or configure it in CI/CD pipelines to catch errors before code merges.</p><h2>3. Advanced Testing: Testify and Ginkgo</h2><p>Testing is crucial in production-grade code, and while the standard library’s testing package is robust, tools like Testify and Ginkgo take testing in Go to the next level.</p><ul><li><strong>Testify</strong>: Offers powerful assertion libraries (assert, require) and mock creation, making it ideal for writing expressive tests. It’s a lightweight addition to the standard testing package and fits well in unit testing workflows.</li><li><strong>Ginkgo</strong>: For behavior-driven development (BDD), Ginkgo provides a framework that allows structuring tests in a more expressive way. Use Gomega, an assertion library built for Ginkgo, to create readable, intention-driven tests.</li></ul><p>These tools enhance test readability and make writing, organising, and managing tests more efficient, especially for complex projects.</p><h2>4. Profiling and Performance Optimization: pprof and trace</h2><p>Optimizing Go code is crucial for high-performance applications, and pprof is Go’s built-in tool for profiling CPU, memory, and goroutine performance.</p><ul><li><strong>CPU and Memory Profiling</strong>: By adding import _ &quot;net/http/pprof&quot; and enabling it in your app, you can visualize CPU and memory usage in real time.</li><li><strong>Flame Graphs</strong>: pprof integrates with tools like go-torch to generate flame graphs, making it easy to identify bottlenecks.</li><li><strong>Tracing</strong>: Go’s trace package provides a lower-level analysis of goroutines, system calls, and scheduling events. It’s useful for diagnosing hard-to-track concurrency issues in production environments.</li></ul><p>These profiling tools are crucial for developers focused on high-performance applications and can quickly reveal inefficient code paths.</p><h2>5. Package Management and Dependency Analysis: godepgraph and go-callvis</h2><p>Understanding dependencies and code structure is essential for maintainability. godepgraph and go-callvis are two visualization tools that make it easy to analyze and improve code organization.</p><ul><li><strong>godepgraph</strong>: Generates dependency graphs that help understand module-level relationships and detect dependency cycles.</li><li><strong>go-callvis</strong>: A tool for visualizing function calls and package interactions within a Go program. Ideal for understanding complex interactions, go-callvis generates call graphs that clarify which functions interact and where.</li></ul><p>These tools are invaluable for debugging and can provide insight into potential architectural improvements.</p><h2>6. Code Generation: Goa and Gobuffalo</h2><p>In projects that require a lot of boilerplate code, code generators save time and reduce errors.</p><ul><li><strong>Goa</strong>: A framework for creating RESTful microservices with a design-first approach. Define API contracts, and Goa will generate corresponding server code.</li><li><strong>Gobuffalo</strong>: Primarily known as a web framework, Gobuffalo includes a code generation CLI that creates boilerplate code for web components, middleware, and services.</li></ul><p>Using code generation tools reduces the amount of repetitive code you need to write, especially useful in micro-services and REST API projects.</p><h2>7. Documentation and Code Browsing: godoc and GoSwagger</h2><p>Documentation is key to maintainability, and Go offers built-in and third-party tools to keep it streamlined.</p><ul><li><strong>godoc</strong>: Go’s built-in documentation generator, which serves as a mini web server, letting you browse package documentation locally.</li><li><strong>GoSwagger</strong>: For APIs, GoSwagger reads OpenAPI definitions and generates Go code for clients and servers, making it easier to keep documentation synchronized with code.</li></ul><p>These tools improve code comprehension and are essential for onboarding and maintaining code quality.</p><h2>Conclusion</h2><p>With these advanced tools, Go developers can tackle complex challenges, optimise their workflows, and produce high-quality code. From dependency management to code generation and profiling, these tools cover every aspect of the development cycle. By leveraging these tools effectively, developers can build more robust, scalable, and efficient applications with Go.</p><p>Explore the possibilities and start integrating these tools into your workflow. The Go ecosystem is ever-evolving, and by staying updated with the latest tools, you can ensure that your Go projects are built for the future.</p><p><em>Originally published on <a href="https://medium.com/@tarunnahak/advanced-tools-for-boosting-productivity-in-go-lang-ccd1248afdfd">Medium</a>.</em></p>]]></content:encoded>
            <author>tarunnahak25@gmail.com (Tarun Nahak)</author>
        </item>
    </channel>
</rss>