<script data-pm-proxy="intercept"></script><?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[The Backend Developer]]></title><description><![CDATA[Bite-size pieces to understand backend development. ]]></description><link>https://thebackenddevelopers.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!UTH3!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c8a9d43-8a2c-410f-939a-098b6faf36de_430x430.png</url><title>The Backend Developer</title><link>https://thebackenddevelopers.substack.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 03 Sep 2026 11:10:07 GMT</lastBuildDate><atom:link href="/__u/thebackenddevelopers.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[The Backend Developers]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[thebackenddevelopers@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[thebackenddevelopers@substack.com]]></itunes:email><itunes:name><![CDATA[Ankur Yadav]]></itunes:name></itunes:owner><itunes:author><![CDATA[Ankur Yadav]]></itunes:author><googleplay:owner><![CDATA[thebackenddevelopers@substack.com]]></googleplay:owner><googleplay:email><![CDATA[thebackenddevelopers@substack.com]]></googleplay:email><googleplay:author><![CDATA[Ankur Yadav]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[The Rise of eBPF: Revolutionizing Backend Observability, Security, and Performance]]></title><description><![CDATA[eBPF revolutionizes Linux: a safe, JIT-compiled kernel VM enabling high-performance observability, security, and networking (e.g., Cilium, Falco, Katran) with minimal overhead.]]></description><link>https://thebackenddevelopers.substack.com/p/the-rise-of-ebpf-revolutionizing</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/the-rise-of-ebpf-revolutionizing</guid><pubDate>Wed, 02 Sep 2026 08:07:10 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/3e002d16-11f6-40b1-8e77-0062e9b01b20_1300x1300.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><br><br>Welcome back to <em>The Backend Developers</em> &#8211; your weekly dose of infrastructure wisdom, served with a side of snark and a sprinkle of kernel magic. Today, we&#8217;re diving into something that&#8217;s been quietly rewriting the rules of our trade: <strong>eBPF</strong>. If you&#8217;ve heard the buzz but thought it was just another acronym to nod along to at conferences, buckle up. This isn&#8217;t just a trend; it&#8217;s a full-blown revolution in how we observe, secure, and turbocharge our systems.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;118c5a7b-6404-44ad-a3ed-50e3f5f2f6c2&quot;,&quot;duration&quot;:null}"></div><p>But first, let me set the scene. Remember the good old days when the Linux kernel was this monolithic, untouchable beast? You wanted to trace a syscall? You&#8217;d either patch the kernel (and pray) or write a kernel module (and risk a panic that takes down production). Observability meant <code>strace</code> with a 10x performance hit. Security meant hoping your firewall rules were good enough. Performance tuning? That was black magic reserved for greybeards with tinfoil hats.</p><p>Then eBPF came along and said, <em>&#8220;Hold my beer.&#8221;</em> It turned the kernel into a programmable playground &#8211; safely, efficiently, and without ever touching kernel source code. And now, it&#8217;s powering everything from DDoS mitigation at Facebook to real-time threat detection in your Kubernetes cluster. So grab your favorite caffeinated beverage, because we&#8217;re about to go on a 20-minute journey through the rise of eBPF, and I promise you&#8217;ll come out the other side with a new superpower.</p><div><hr></div><p><em><strong>What in the World is eBPF? (And Why Should You Care?)</strong></em></p><p>Let&#8217;s start with the basics, because if you&#8217;re like me, you&#8217;ve heard &#8220;eBPF&#8221; thrown around but never had a clear, non-vendor-pitch explanation. eBPF stands for <em>extended Berkeley Packet Filter</em>, but that name is about as descriptive as calling a smartphone a &#8220;portable telephone.&#8221; At its core, eBPF is a <strong>JIT-compiled virtual machine</strong> that runs sandboxed programs directly inside the Linux kernel. Think of it as JavaScript for the kernel &#8211; you write a small, safe program, the kernel verifies it won&#8217;t do anything stupid, and then it runs at native speed, hooked into events like syscalls, network packets, or function calls [1].</p><p>The magic lies in the <strong>verifier</strong>. This little piece of code analyzes your program before it runs, ensuring it terminates, doesn&#8217;t access arbitrary memory, and won&#8217;t crash the system. It&#8217;s like having a bouncer at the kernel&#8217;s VIP lounge &#8211; only the safest, most well-behaved programs get in. Once verified, the program is JIT-compiled to native machine code, so it runs with near-zero overhead. This architectural foundation is what makes eBPF so powerful: it gives you the ability to program the kernel without the risk of kernel modules or the performance penalty of user-space tools [1].</p><p>But why should you care? Because this single technology is reshaping three pillars of backend engineering: <strong>observability</strong>, <strong>performance</strong>, and <strong>security</strong>. And it&#8217;s doing it all with a unified, safe, and high-performance platform. Let&#8217;s break it down.</p><div><hr></div><p><em><strong>The Performance Magic: No More Context Switch Blues</strong></em></p><p>If you&#8217;ve ever profiled a high-throughput service, you know the pain of context switches. Every time your code crosses the user-kernel boundary, there&#8217;s a cost &#8211; a few microseconds here, a few there, and suddenly your p99 latency looks like a rollercoaster. Traditional tools like <code>strace</code> or <code>tcpdump</code> are notorious for this, because they copy data to user space, process it, and copy it back. It&#8217;s like trying to watch a Formula 1 race by having a runner sprint alongside the cars and shout updates.</p><p>eBPF eliminates this by running your logic <strong>inside the kernel</strong>. For network processing, eBPF hooks into <strong>XDP</strong> (eXpress Data Path) and <strong>TC</strong> (Traffic Control) at the driver level, allowing you to process packets at line rate &#8211; millions of packets per second &#8211; without ever leaving kernel space [2]. For tracing, <strong>kprobes</strong> and <strong>tracepoints</strong> let you attach eBPF programs to virtually any kernel function or event, capturing data with minimal overhead. The result? Near-zero latency and a fraction of the CPU cost compared to traditional methods [2].</p><p>This isn&#8217;t just theoretical. Facebook&#8217;s Katran uses eBPF for DDoS mitigation, dropping malicious packets at the NIC driver level before they even hit the network stack. Cilium, the CNI of choice for many Kubernetes clusters, uses eBPF for load balancing and network policy, achieving performance that rivals hardware load balancers. And all of this happens without modifying a single line of kernel code &#8211; just a safe, sandboxed program running at the speed of native code [2].</p><div><hr></div><p><em><strong>Observability: Seeing Everything Without Breaking a Sweat</strong></em></p><p>Let&#8217;s talk about the bread and butter of every backend engineer: observability. We&#8217;ve all been there &#8211; a mysterious latency spike, a file that&#8217;s being opened too often, a syscall that&#8217;s misbehaving. Traditional tools either give you too little detail (like <code>top</code>) or too much overhead (like <code>strace</code>). eBPF changes the game by giving you <strong>deep, low-overhead visibility</strong> into the kernel and your applications.</p><p>The ecosystem has matured into a beautiful dual-language paradigm: you write the kernel-side logic in C (embedded as a string in your script), and the user-space processing in a high-level language like Python. Tools like <strong>bcc</strong> and <strong>bpftrace</strong> abstract away the low-level syscalls, maps, and compilation steps, so you can go from a one-liner to a complex custom tool in minutes [3]. For example, with <code>bpftrace</code>, you can trace file opens with a single command:</p><pre><code>bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args-&gt;filename)); }'</code></pre><p>But the real power comes when you combine eBPF with Python. Here&#8217;s a simple example using <code>bcc</code> to trace all <code>openat</code> syscalls and print the process name and file path:</p><pre><code>from bcc import BPF

# This is the C program that runs in the kernel
bpf_text = """
#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/fs.h&gt;

int trace_openat(struct tracepoint__syscalls__sys_enter_openat *args) {
    char filename[256];
    bpf_probe_read_user(filename, sizeof(filename), args-&gt;filename);
    bpf_trace_printk("%s %s\\n", current-&gt;comm, filename);
    return 0;
}
"""

# Attach to the tracepoint
b = BPF(text=bpf_text)
b.attach_tracepoint("syscalls:sys_enter_openat", "trace_openat")

# Print output
print("Tracing openat syscalls... Ctrl-C to stop.")
while True:
    try:
        (task, pid, cpu, flags, ts, msg) = b.trace_fields()
        print(f"{task} ({pid}): {msg}")
    except KeyboardInterrupt:
        break</code></pre><p>This script runs with minimal overhead, because the heavy lifting happens in the kernel, and only the results are sent to user space. You can extend this to trace network packets, CPU scheduling, memory allocations &#8211; you name it. The barrier to entry is so low that even a junior dev can start building custom observability tools in an afternoon [3].</p><div><hr></div><p><em><strong>Security: The Same Hooks, Now with Attitude</strong></em></p><p>Now, here&#8217;s where eBPF gets really spicy. The same kernel hooks that power observability &#8211; kprobes, tracepoints, and network hooks &#8211; are the exact mechanisms used by modern security tools. This isn&#8217;t a coincidence; it&#8217;s a fundamental insight: <strong>deep kernel visibility is a shared foundation for both disciplines</strong> [4]. Tools like <strong>Falco</strong> and <strong>Tetragon</strong> leverage eBPF to monitor syscalls, track process lifecycles, and detect anomalies in real time, all without modifying your application code or adding significant overhead.</p><p>For example, Falco can alert you when a shell is spawned inside a container, or when a sensitive file is read by an unexpected process. It does this by attaching eBPF programs to syscall tracepoints and analyzing the events in user space. Because the eBPF programs are safe and sandboxed, these security tools can run in production with minimal performance impact &#8211; a stark contrast to traditional security agents that often require kernel modules or invasive instrumentation [4].</p><p>But eBPF&#8217;s security superpower goes beyond monitoring. It can also <strong>enforce policies</strong> directly in the kernel. Cilium, for instance, uses eBPF for L3-L7 network policy, allowing you to define fine-grained rules like &#8220;only allow HTTP GET requests to this service&#8221; and have them enforced at the packet level, with no user-space round-trip. This is the same technology that powers high-performance load balancing, proving that a single eBPF deployment can simultaneously address performance, networking, and security requirements [5].</p><div><hr></div><p><em><strong>Performance: Load Balancing and DDoS at Warp Speed</strong></em></p><p>Let&#8217;s zoom in on the performance side, because that&#8217;s where eBPF truly shines. Traditional load balancers (like HAProxy or NGINX) operate in user space, which means every packet has to cross the kernel boundary multiple times. eBPF-based load balancers, like the one in Cilium, run entirely in the kernel, processing packets at line rate and forwarding them to the correct backend with minimal latency [5]. This is a game-changer for microservices architectures, where every millisecond counts.</p><p>And then there&#8217;s DDoS mitigation. Facebook&#8217;s Katran uses eBPF to drop malicious packets at the XDP layer, before they even reach the network stack. This allows it to handle attacks at terabit speeds, because the filtering logic runs on the NIC driver itself, not in user space [2]. The same principle applies to any high-throughput network processing &#8211; eBPF can inspect, modify, and forward packets at speeds that would make traditional tools weep.</p><p>But here&#8217;s the kicker: eBPF isn&#8217;t just for network performance. It&#8217;s also used for CPU profiling, disk I/O tracing, and even application-level tracing via uprobes. You can attach eBPF programs to user-space functions, giving you the ability to trace application logic without recompiling or instrumenting your code. This makes it an invaluable tool for performance debugging in production, where you can&#8217;t afford to add overhead or restart services [2].</p><div><hr></div><p><em><strong>A Brief History Lesson: From cBPF to eBPF</strong></em></p><p>To truly appreciate eBPF, we need to look back at its humble origins. The original BPF (classic BPF, or cBPF) was introduced in 1992 as a simple packet filter for <code>tcpdump</code>. It was a tiny, stateless virtual machine that could only match packets based on a few fields. Fast forward to 2014, and Alexei Starovoitov introduced eBPF in Linux 3.18, expanding the instruction set, adding maps for stateful processing, and introducing the verifier to ensure safety [6]. This was the pivotal moment that transformed BPF from a niche packet-filtering tool into a general-purpose infrastructure layer.</p><p>Since then, eBPF has evolved at a breakneck pace. It now supports a rich set of hooks, from tracepoints and kprobes to XDP and TC, and has become a cornerstone of cloud-native architectures. Tools like Cilium, Falco, and Katran are just the tip of the iceberg &#8211; there are hundreds of projects leveraging eBPF for everything from service meshes to database performance monitoring [6]. The evolution from cBPF to eBPF is a testament to the power of incremental innovation, and it&#8217;s set the stage for eBPF to become as fundamental to Linux as the kernel itself.</p><div><hr></div><p><em><strong>Safety First: How eBPF Makes Security Fast</strong></em></p><p>You might be thinking: &#8220;Okay, eBPF is fast and powerful, but how can it be safe? Running arbitrary code in the kernel sounds like a recipe for disaster.&#8221; That&#8217;s where the verifier comes in, and it&#8217;s the unsung hero of the eBPF story. The verifier performs a static analysis of your program, checking for loops, out-of-bounds access, and other dangerous patterns. It ensures that the program will terminate and won&#8217;t corrupt kernel memory. This safety guarantee is what allows eBPF to be used in production-critical environments [7].</p><p>And here&#8217;s the paradox: because eBPF programs are guaranteed safe, security tools can run with minimal overhead and without modifying application code. Traditional security agents often require kernel modules, which are risky to deploy and can cause system instability. eBPF eliminates that risk, making it the default choice for modern runtime security and observability [7]. It&#8217;s a win-win: you get the performance of kernel-level instrumentation with the safety of a sandboxed environment.</p><div><hr></div><p><em><strong>The Developer Experience: C in the Kernel, Python in the User Space</strong></em></p><p>One of the reasons eBPF has gained such widespread adoption is the maturity of its developer experience. The ecosystem has converged on a consistent pattern: you write the kernel-side logic in C (embedded as a string in your script), and the user-space processing in a high-level language like Python. Tools like <strong>bcc</strong> and <strong>bpftrace</strong> abstract away the low-level syscalls, maps, and compilation steps, so you can go from a one-liner to a complex custom tool in minutes [3].</p><p>For example, with <code>bpftrace</code>, you can trace file opens with a single command:</p><pre><code>bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args-&gt;filename)); }'</code></pre><p>But the real power comes when you combine eBPF with Python. Here&#8217;s a simple example using <code>bcc</code> to trace all <code>openat</code> syscalls and print the process name and file path:</p><pre><code>from bcc import BPF

# This is the C program that runs in the kernel
bpf_text = """
#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/fs.h&gt;

int trace_openat(struct tracepoint__syscalls__sys_enter_openat *args) {
    char filename[256];
    bpf_probe_read_user(filename, sizeof(filename), args-&gt;filename);
    bpf_trace_printk("%s %s\\n", current-&gt;comm, filename);
    return 0;
}
"""

# Attach to the tracepoint
b = BPF(text=bpf_text)
b.attach_tracepoint("syscalls:sys_enter_openat", "trace_openat")

# Print output
print("Tracing openat syscalls... Ctrl-C to stop.")
while True:
    try:
        (task, pid, cpu, flags, ts, msg) = b.trace_fields()
        print(f"{task} ({pid}): {msg}")
    except KeyboardInterrupt:
        break</code></pre><p>This script runs with minimal overhead, because the heavy lifting happens in the kernel, and only the results are sent to user space. You can extend this to trace network packets, CPU scheduling, memory allocations &#8211; you name it. The barrier to entry is so low that even a junior dev can start building custom observability tools in an afternoon [3].</p><div><hr></div><p><em><strong>Libraries and Services to Check Out</strong></em></p><p>If you&#8217;re itching to get your hands dirty, here are some of the most impactful eBPF projects and libraries to explore:</p><ul><li><p><strong>bcc</strong> &#8211; The most popular toolkit for building eBPF programs in Python, Lua, and C++. It includes a rich set of pre-built tools for tracing, networking, and performance analysis.</p></li><li><p><strong>bpftrace</strong> &#8211; A high-level tracing language for eBPF, perfect for quick one-liners and ad-hoc debugging. Think of it as <code>awk</code> for the kernel.</p></li><li><p><strong>Cilium</strong> &#8211; A CNI and service mesh that uses eBPF for networking, security, and observability in Kubernetes. It&#8217;s the poster child for eBPF&#8217;s horizontal platform capabilities.</p></li><li><p><strong>Falco</strong> &#8211; A runtime security tool that uses eBPF to detect anomalous behavior in containers and hosts. It&#8217;s the go-to for cloud-native threat detection.</p></li><li><p><strong>Katran</strong> &#8211; Facebook&#8217;s eBPF-based DDoS mitigation and load balancing solution, open-sourced for the community.</p></li><li><p><strong>Tetragon</strong> &#8211; A newer security observability tool from Cilium that provides deep process and network visibility using eBPF.</p></li></ul><p>These are just a few examples &#8211; the ecosystem is growing every day, and there&#8217;s a tool for almost every use case you can imagine.</p><div><hr></div><p><em><strong>Closing Thoughts: The eBPF Revolution is Here to Stay</strong></em></p><p>So there you have it &#8211; eBPF is not just a buzzword; it&#8217;s a fundamental shift in how we interact with the Linux kernel. It&#8217;s giving us the power to observe, secure, and optimize our systems like never before, all with safety and performance that were previously impossible. Whether you&#8217;re debugging a latency spike, securing your Kubernetes cluster, or building the next high-performance load balancer, eBPF is the tool that will get you there.</p><p>As backend developers, we&#8217;re standing at the forefront of this revolution. The tools are mature, the community is vibrant, and the possibilities are endless. So go ahead, write your first eBPF program, and see the kernel in a whole new light. And if you get stuck, remember: the verifier is your friend, and <code>bpftrace</code> is your trusty sidekick.</p><p>That&#8217;s all for this week, folks. If you enjoyed this deep dive, do me a favor and hit that subscribe button &#8211; we&#8217;ve got more kernel magic coming your way. Until next time, keep your systems fast, your observability deep, and your eBPF programs safe.</p><p><em>Cheers, and happy hacking!</em></p><p>&#8212; <em>The Backend Developers</em></p><p><strong>Wit Summary:</strong> eBPF revolutionizes Linux: a safe, JIT-compiled kernel VM enabling high-performance observability, security, and networking (e.g., Cilium, Falco, Katran) with minimal overhead.</p>]]></content:encoded></item><item><title><![CDATA[The Rise of the Edge Database: Latency, Consistency, and the New Data Frontier]]></title><description><![CDATA[Edge DBs prioritize speed/availability over strong consistency, using local storage and CRDTs. Built on SQLite, they face security/resource trade-offs for fast, offline-first apps.]]></description><link>https://thebackenddevelopers.substack.com/p/the-rise-of-the-edge-database-latency</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/the-rise-of-the-edge-database-latency</guid><pubDate>Tue, 01 Sep 2026 12:09:59 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e68a2c9c-649a-4f9c-a7c4-86e188aa9efc_1300x1300.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<ul><li><p><strong>The Edge Database: Because Your Cloud Is Too Far Away (and Your Users Are Impatient)</strong> *</p></li></ul><p>Alright, backend warriors, gather &#8216;round. I&#8217;ve been running this newsletter long enough to see trends come and go like fad diets, but this one? This one&#8217;s got legs. I&#8217;m talking about the edge database&#8212;the rebellious teenager of the data world that refuses to live in your cozy, centralized cloud. It&#8217;s fast, it&#8217;s feisty, and it&#8217;s about to make your latency metrics look like a lie.</p><p>You know the drill: your users are scattered across the globe, your cloud database sits in a data center somewhere in Virginia, and every query has to travel halfway around the planet just to fetch a user&#8217;s profile picture. The result? A 300-millisecond round trip that feels like an eternity in the age of instant gratification. Enter the edge database: a paradigm shift that says, &#8220;Why not just put the data where the users are?&#8221; It&#8217;s like opening a coffee shop on every corner instead of forcing everyone to drive to the one downtown. Genius, right? But as with all great ideas, there&#8217;s a catch. A big, hairy, CAP-theorem-shaped catch.</p><p>So grab your favorite caffeinated beverage, and let&#8217;s dive into the new data frontier. I promise you&#8217;ll come out the other side with a few laughs, a few &#8220;aha!&#8221; moments, and maybe a newfound respect for SQLite.</p><ul><li><p><strong>The CAP Theorem: The Devil&#8217;s Bargain</strong> *</p></li></ul><p>Let&#8217;s start with the elephant in the room&#8212;the CAP theorem. If you&#8217;ve been in this game for more than a week, you&#8217;ve heard of it: Consistency, Availability, Partition Tolerance&#8212;pick two. Cloud databases have historically said, &#8220;We&#8217;ll take Consistency and Partition Tolerance, and we&#8217;ll fake Availability with a bunch of retries and timeouts.&#8221; Edge databases, on the other hand, look at that and say, &#8220;Nah, we&#8217;re going to prioritize Availability and Partition Tolerance, and we&#8217;ll deal with Consistency later. Much later. Like, eventually.&#8221; [1]</p><p>This isn&#8217;t a bug; it&#8217;s a feature. By explicitly choosing to sacrifice strong consistency, edge databases ensure that your app remains responsive and resilient even when the network goes haywire. Think about it: if a user&#8217;s device loses connectivity, a cloud database would just hang there, spinning its wheels, waiting for a response that never comes. An edge database, however, keeps chugging along, processing data locally, and syncs up when the connection returns. It&#8217;s the difference between a toddler having a meltdown when their toy is taken away and a zen master who just rolls with it. [1]</p><p>This trade-off shapes every other aspect of edge database architecture. It&#8217;s the foundation upon which the entire paradigm is built. So when you hear someone complaining about eventual consistency, just remember: they&#8217;re complaining about the price of admission to a world where your app never goes down, even in the middle of a zombie apocalypse (or a Wi-Fi dead zone).</p><ul><li><p><strong>Latency: The Need for Speed</strong> *</p></li></ul><p>Now, let&#8217;s talk about the main selling point: latency. The whole reason edge databases exist is to make your app feel snappy. By processing data locally on edge devices, you cut network round-trip times from tens of milliseconds down to single digits. We&#8217;re talking a 50&#8211;90% performance gain for latency-sensitive applications like IoT, gaming, and real-time analytics. [2] That&#8217;s not just a nice-to-have; that&#8217;s the difference between a user rage-quitting your app and them leaving a five-star review.</p><p>But here&#8217;s the kicker: this speed comes at a cost. You&#8217;re trading the virtually unlimited compute and storage scalability of centralized cloud data centers for a tiny, resource-constrained device sitting in someone&#8217;s living room. It&#8217;s like swapping a supercomputer for a Raspberry Pi. Sure, the Pi is fast for what it does, but you&#8217;re not going to run a full-blown data warehouse on it. [2] So you have to be smart about what you put at the edge. User sessions, feature flags, real-time collaborative state&#8212;these are perfect candidates. Your entire customer database? Maybe not so much.</p><p>The key is to find the sweet spot between data locality and processing power. It&#8217;s a delicate dance, and we&#8217;ll get into the operational headaches later. But for now, just bask in the glory of sub-10-millisecond queries. Ah, feels good, doesn&#8217;t it?</p><ul><li><p><strong>Consistency: The Art of the Compromise</strong> *</p></li></ul><p>Now for the part that makes every database purist break out in hives: consistency. Edge databases rely on eventual consistency as the baseline. That means your data will eventually converge, but in the meantime, different nodes might see different versions of the truth. It&#8217;s like a group project where everyone works on their own copy of the document, and then you try to merge them all at the end. Chaos, right? Well, not exactly. The real sophistication lies in conflict resolution.</p><p>The most common approach is Last-Write-Wins (LWW), where the most recent write simply overwrites everything else. It&#8217;s simple, it&#8217;s effective, and it&#8217;s about as nuanced as a sledgehammer. But for many use cases, it&#8217;s perfectly fine. If two users update their profile pictures, who cares which one wins? The last one to click &#8220;Save&#8221; is the winner. [3]</p><p>But for more complex scenarios&#8212;like collaborative editing or distributed counters&#8212;you need something smarter. That&#8217;s where Conflict-free Replicated Data Types (CRDTs) come in. CRDTs are the &#8220;edge-native&#8221; standard, enabling seamless offline-first, multi-writer collaboration without any central coordination. They&#8217;re like magic: you can have multiple users editing the same document on different devices, and when they sync, everything just works. No conflicts, no lost updates, no tears. [3]</p><p>I know what you&#8217;re thinking: &#8220;CRDTs sound like a lot of math.&#8221; And you&#8217;re right, they are. But the good news is that you don&#8217;t have to implement them from scratch. There are libraries for that, and we&#8217;ll get to them in a bit. For now, just know that the edge database ecosystem has figured out how to make eventual consistency not just tolerable, but actually elegant.</p><ul><li><p><strong>The SQLite Revolution: Small but Mighty</strong> *</p></li></ul><p>So how are these edge databases actually built? The answer, surprisingly, is SQLite. Yes, that little embedded database that&#8217;s been powering your phone&#8217;s contacts app for years is now the backbone of the edge database revolution. Why? Because it&#8217;s lightweight, it&#8217;s fast, and it&#8217;s incredibly reliable. It&#8217;s the cockroach of databases&#8212;it survives everything. [4]</p><p>Services like Turso (built on the open-source libSQL fork) and Cloudflare D1 are leading the charge. They provide production-ready, serverless edge databases with global read replication. You write your data to a local SQLite instance, and it syncs with the cloud and other edge nodes in the background. It&#8217;s like having your cake and eating it too&#8212;you get the speed of local data access and the durability of cloud storage. [4]</p><p>The primary use cases are telling: user sessions, feature flags, and real-time collaborative features. These are all things that need to be fast, but don&#8217;t necessarily need to be globally consistent. By moving this stateful data to the network&#8217;s edge, you free up your central database to handle the heavy lifting, and you give your users a snappier experience. It&#8217;s a win-win.</p><p>Let me show you a quick example. Say you&#8217;re building a simple counter app that needs to work offline. With an edge database, you&#8217;d use a local SQLite instance and a CRDT to handle conflicts. Here&#8217;s a Python snippet using the <code>crdt</code> library (just for illustration):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e2a9dcc3-8c44-4026-871a-a7737500aaa6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from crdt import GCounter

# Each edge device has its own counter
counter = GCounter()

# Increment locally
counter.increment()

# When syncing, merge the counters
counter.merge(other_counter)

# The final value is the sum of all increments
print(counter.value())</code></pre></div><p>Simple, right? And with a library like <code>sqlite-utils</code> and a sync layer, you can build a full edge database in no time. The point is, the tooling is mature enough that you don&#8217;t need to be a distributed systems expert to take advantage of this paradigm.</p><ul><li><p><strong>The Dark Side: Security and Resource Management</strong> *</p></li></ul><p>Now, let&#8217;s talk about the elephant in the room&#8212;the operational burdens. Edge databases solve latency, but they amplify other challenges. First and foremost: security. When your data is spread across thousands of edge nodes, you&#8217;ve just expanded your attack surface by a factor of a thousand. Every device is a potential entry point, and you need to protect them all. This demands lightweight encryption, zero-trust architectures, and secure data-in-motion protocols. [5] It&#8217;s not impossible, but it&#8217;s a whole new level of paranoia.</p><p>And then there&#8217;s the resource constraint problem. Edge devices have strict memory and CPU limits. You can&#8217;t just throw a 16GB RAM instance at the problem. You have to carefully balance data locality against processing power. It&#8217;s like trying to fit a full-sized fridge into a studio apartment&#8212;you have to get creative with the layout. [5]</p><p>But hey, nobody said the edge was easy. It&#8217;s a trade-off, and you have to decide if the latency gains are worth the operational headaches. For many applications, they absolutely are. For others, you might be better off sticking with the cloud. The key is to know your use case and choose accordingly.</p><ul><li><p><strong>The Future: Edge-Native Data Management</strong> *</p></li></ul><p>So where is this all heading? The future trajectory points toward &#8220;edge-native&#8221; data management, not just cloud synchronization. We&#8217;re moving beyond simple async sync with a central cloud. The rise of AI-driven autonomous data management&#8212;for predictive caching and self-optimization&#8212;is already on the horizon. Imagine an edge database that learns which data you access most frequently and pre-fetches it before you even ask. That&#8217;s the kind of magic we&#8217;re talking about. [6]</p><p>And with the standardization of CRDT-based consistency models, we&#8217;re heading toward fully decentralized, multi-writer systems where the edge is the primary source of truth, not a mere cache. This is a fundamental shift in how we think about data architecture. The edge isn&#8217;t just a way to speed things up; it&#8217;s a new way of thinking about where data lives and how it&#8217;s processed. [6]</p><p>The bottom line? The edge database is here to stay, and it&#8217;s only going to get more sophisticated. If you&#8217;re not already experimenting with it, now&#8217;s the time to start. Your users will thank you, your latency metrics will thank you, and your sanity will thank you when you&#8217;re not dealing with a million timeout errors.</p><ul><li><p><strong>Wrapping Up: The New Data Frontier Awaits</strong> *</p></li></ul><p>So there you have it, folks. The edge database is not just a buzzword; it&#8217;s a fundamental shift in how we build and deploy data-driven applications. It&#8217;s a trade-off, sure, but it&#8217;s a trade-off that makes sense for a world that demands instant gratification. Whether you&#8217;re building an IoT platform, a real-time game, or just a simple app that needs to work offline, the edge has something to offer.</p><p>If you want to dive deeper, check out Turso, Cloudflare D1, or even open-source projects like RxDB and PouchDB. They&#8217;re all doing amazing things in this space, and they&#8217;re proof that the edge is more than just a pipe dream.</p><p>That&#8217;s all for today, my friends. I hope you enjoyed this little journey into the data frontier. If you did, do me a favor and hit that subscribe button&#8212;I promise more rants, more insights, and more bad puns in the future. Until next time, keep your data close and your latency lower.</p><p>Stay edgy, The Backend Developers</p>]]></content:encoded></item><item><title><![CDATA[Event-Driven Architecture: The Hidden Costs of Async]]></title><description><![CDATA[The Siren Song of Async]]></description><link>https://thebackenddevelopers.substack.com/p/event-driven-architecture-the-hidden</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/event-driven-architecture-the-hidden</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Tue, 01 Sep 2026 00:51:56 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/213637802/2dea8d22909842e6ae216348a8dfc0ca.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>Let&#8217;s be honest. We&#8217;ve all been there. You&#8217;re staring at a monolithic REST API that&#8217;s creaking under load. The database is sweating. The pager is buzzing. And then, like a mirage in the desert, you see it: Event-Driven Architecture. It promises loose coupling, infinite scalability, and the ability to process a million requests without breaking a sweat. It&#8217;s the architectural equivalent of a sports car&#8212;sleek, fast, and guaranteed to turn heads.</p><p>But here&#8217;s the dirty little secret they don&#8217;t put on the brochure: that sports car has a maintenance bill that will make your CFO weep. It requires a pit crew of specialists, a warehouse of spare parts, and a telemetry system that costs more than the car itself. In the world of backend development, Event-Driven Architecture (EDA) is that sports car. It&#8217;s beautiful, powerful, and absolutely riddled with hidden costs that compound faster than interest on a payday loan.</p><p>Today, we&#8217;re going to pop the hood and look at the greasy, complicated engine of async. We&#8217;re going to talk about the &#8220;loose coupling&#8221; that isn&#8217;t so loose, the distributed state that gives you ulcers, and the observability tax that will eat your engineering budget for breakfast. Buckle up.</p><div><hr></div><p><em>The &#8220;Loose Coupling&#8221; Lie</em></p><p>The core promise of EDA is that it decouples producers from consumers. The order service doesn&#8217;t need to know about the inventory service. It just fires an event into the void and goes back to sipping its coffee. Beautiful, right? Wrong.</p><p>Research shows that while you decouple your <em>applications</em>, you create a rigid, hidden coupling to the <em>infrastructure</em> [1]. You are no longer just writing code; you are now managing a distributed system. You need to configure brokers, manage consumer groups, set up dead-letter queues, and pray to the gods of partitioning. This isn&#8217;t a simple &#8220;fire and forget&#8221; anymore; it&#8217;s a full-time job for a team of platform engineers who specialize in the arcane arts of Kafka tuning or RabbitMQ cluster management.</p><p>The complexity of this infrastructure often exceeds the complexity of the synchronous system you were trying to escape. You traded a simple HTTP call for a distributed nightmare. The &#8220;loose coupling&#8221; is a myth because you are now tightly coupled to the operational reality of the broker. If the broker goes down, everything goes down. If the broker is slow, everything is slow. You haven&#8217;t removed the bottleneck; you&#8217;ve just moved it to a place that&#8217;s harder to debug.</p><div><hr></div><p><em>The Distributed State Quagmire</em></p><p>Now, let&#8217;s talk about the elephant in the room: state. In a synchronous world, you call a service, you get a response, and you know the state of the world. In an async world, you fire an event and... hope. This leads to the primary hidden cost of EDA: distributed state management [2].</p><p>Eventual consistency is a nice theoretical concept, but in practice, it means your data is wrong for an indeterminate amount of time. To handle this, you have to implement complex compensating patterns like Sagas to undo partial failures. You need idempotent consumers because, despite the broker&#8217;s promises, &#8220;at-least-once&#8221; delivery means you <em>will</em> process the same event twice. And to ensure you don&#8217;t lose events during database writes, you need a transactional outbox pattern.</p><p>Let&#8217;s look at a simple example. Imagine you have a service that processes a payment and then publishes an event. In a synchronous world, it&#8217;s a simple try-catch. In an async world, you need to ensure the event is only published if the transaction commits.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ee4589d7-ae36-4267-bc28-825e766aa9eb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># The Async Nightmare: Transactional Outbox
from sqlalchemy import create_engine, Column, String, Integer, Text
from sqlalchemy.orm import declarative_base, sessionmaker
import json

Base = declarative_base()

class OutboxEvent(Base):
    __tablename__ = 'outbox_events'
    id = Column(Integer, primary_key=True)
    aggregate_id = Column(String)
    event_type = Column(String)
    payload = Column(Text)

# Assume we have a DB session and a Kafka producer
def process_payment_and_publish(user_id, amount):
    # 1. Start DB transaction
    # 2. Insert payment record
    # 3. Insert event into Outbox table (same DB transaction)
    # 4. Commit DB transaction
    # 5. Publish event to Kafka (AFTER commit)
    # 6. Delete event from Outbox table (or mark as sent)
    pass</code></pre></div><p>This is the &#8220;simple&#8221; version. You now have a table in your database that exists solely to feed a message queue. You have a background process that polls this table. You have to handle the race condition where the event is published but the deletion fails. This is not simplicity; this is complexity with a fancy hat on. It directly contradicts the initial promise of a clean, decoupled system [2].</p><div><hr></div><p><em>The Observability Tax</em></p><p>In a synchronous world, if a request fails, you look at the logs, see the stack trace, and fix it. In an async world, an event can hop through five different services before it fails. Tracing that failure is a nightmare.</p><p>Observability in EDA is an exponential tax [3]. You have to manually propagate correlation IDs across every hop, which adds latency and code bloat. Your log volumes explode because every service is logging its own slice of the event&#8217;s journey. To make sense of it all, you need distributed tracing infrastructure like Jaeger or Zipkin, which comes with its own overhead and sampling trade-offs. You can&#8217;t log everything; it would cost too much. So, you sample, which means you might miss the one trace that has the bug.</p><p>This observability overhead is substantially more expensive than in a request-response model, both in runtime performance and engineering effort [3]. You are paying a tax on every single event just to have a hope of debugging it later. It&#8217;s like paying a toll every time you drive your car, but the toll booth is on fire and the attendant is blind.</p><div><hr></div><p><em>The Zero-Sum Game of Tooling</em></p><p>When you decide to go async, you have to pick your poison. Do you self-manage a broker like Kafka? Or do you use a managed service like AWS EventBridge?</p><p>Self-managed Kafka offers high throughput, but you are now responsible for cluster management, storage retention, and tuning. You need to monitor broker health, manage partitions, and handle rebalancing. This is a massive operational overhead that requires specialized expertise [4].</p><p>On the other hand, managed services eliminate the infrastructure patching, but they shift the burden to a pay-per-event model. This is a ticking time bomb. If you have an &#8220;event storm&#8221;&#8212;a sudden spike in traffic&#8212;your bill will skyrocket. You are vulnerable to pricing spikes that can blow your budget out of the water [4]. There is no free lunch. Every tool simply relocates the hidden cost. You either pay with your engineers&#8217; time or with your cloud bill. It&#8217;s a zero-sum game, and the house always wins.</p><div><hr></div><p><em>The TCO Reality Check</em></p><p>So, what does this all mean for your bottom line? The research is clear: for small-to-medium workloads, the Total Cost of Ownership (TCO) of EDA often exceeds that of simpler synchronous models [5].</p><p>You are paying for infrastructure tuning, ongoing maintenance (patching, lag monitoring, backpressure handling), and the observability overhead. All of this adds up to a significant engineering cost. If your workload doesn&#8217;t genuinely require massive horizontal scaling, you are paying a premium for a feature you aren&#8217;t using. It&#8217;s like buying a dump truck to carry your groceries. Sure, it can do it, but it&#8217;s expensive, hard to park, and you look ridiculous.</p><div><hr></div><p><em>The Schema Evolution Time Bomb</em></p><p>Finally, let&#8217;s talk about the silent killer: schema evolution. In a synchronous system, if you change the API, the client gets a compile error. In an async system, you can change the event schema, and the consumer will break in production, hours later, with no warning.</p><p>Without a robust schema registry and strict governance, a single breaking change can cascade across the entire system, causing data corruption and downtime [6]. This makes schema management a mandatory investment. You need to version your schemas, ensure backward compatibility, and have a migration strategy. This is a cross-cutting concern that touches every team and every service. It&#8217;s a recurring cost that is often underestimated until it&#8217;s too late.</p><div><hr></div><p><em>The Complexity Snowball</em></p><p>The most insidious part of all this is that these costs are not isolated. They compound. The operational complexity of managing brokers directly exacerbates your observability challenges. The consistency issues force you to adopt additional tooling, which increases your infrastructure footprint. This creates a &#8220;complexity snowball&#8221; that grows with every new event type and consumer [7].</p><p>You start with one event, and it&#8217;s manageable. Then you add another, and another. Soon, you have a tangled web of events, dead-letter queues, and sagas that no one fully understands. The system becomes a legacy system on day one, and you are just the poor soul who has to maintain it.</p><div><hr></div><p><em>The Verdict</em></p><p>Event-Driven Architecture is a powerful tool, but it is not a silver bullet. It is a strategic decision that should be made with your eyes wide open. The hidden costs&#8212;infrastructure complexity, distributed state management, observability tax, tooling trade-offs, and schema evolution&#8212;are real and they compound.</p><p>Before you jump on the async bandwagon, ask yourself: Do I really need this? Can I scale with a simple queue and a worker? If the answer is yes, save yourself the pain. If the answer is no, then go in with a plan. Invest in the right tooling, hire the right people, and budget for the complexity. Don&#8217;t say we didn&#8217;t warn you.</p><p>For those who are already in the trenches, know that you are not alone. We see you, debugging that correlation ID at 2 AM. We feel your pain.</p><div><hr></div><p><em>Tools of the Trade</em></p><p>If you are diving into EDA, here are some tools that can help you manage the chaos:</p><ul><li><p><strong>Kafka</strong>: The heavyweight champion of brokers. High throughput, but you need a team to run it.</p></li><li><p><strong>AWS EventBridge</strong>: A managed service that simplifies infrastructure but has a pay-per-event pricing model.</p></li><li><p><strong>Confluent Schema Registry</strong>: A must-have for managing schema evolution and ensuring compatibility.</p></li><li><p><strong>Jaeger / Zipkin</strong>: Distributed tracing tools to help you see the async flow.</p></li><li><p><strong>Debezium</strong>: A tool for implementing the transactional outbox pattern by capturing changes from your database.</p></li></ul><div><hr></div><p><em>The Sign-Off</em></p><p>Alright, folks, that&#8217;s the ugly truth about Event-Driven Architecture. It&#8217;s not all sunshine and decoupled roses. It&#8217;s a complex, expensive, and operationally demanding beast. But with the right knowledge and preparation, you can tame it.</p><p>If you enjoyed this deep dive into the dark side of backend development, make sure to subscribe to <em>The Backend Developers</em> newsletter. We&#8217;ll be here every day, peeling back the layers of complexity so you don&#8217;t have to. Stay curious, stay humble, and for the love of all that is holy, monitor your consumer lag.</p><p>Until next time, keep your queues clean and your correlation IDs consistent.</p><p>Warmly, Your Backend Buddy</p>]]></content:encoded></item><item><title><![CDATA[Vector Databases: The Backend's New Reality]]></title><description><![CDATA[The Backend&#8217;s New Reality: Vector Databases]]></description><link>https://thebackenddevelopers.substack.com/p/vector-databases-the-backends-new</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/vector-databases-the-backends-new</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Fri, 28 Aug 2026 00:06:21 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/213076470/eca9c780d81ea1d0454d15fc86aaeb3a.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p><em>So, you&#8217;ve finally mastered the art of SQL joins, sharding strategies, and convincing your CTO that caching isn&#8217;t just for people with too much RAM. You&#8217;ve built monoliths, microservices, and possibly a few fires along the way. And then, just as you were getting comfortable, the world pivoted to AI.</em></p><p><em>Now, every stakeholder wants semantic search, personalized recommendations, and anomaly detection. You feel a cold sweat coming on. How the heck are you supposed to handle millions of unstructured data points&#8212;images, text, voice snippets&#8212;without storing them in a bunch of JSON blobs that you manually scrape through?</em></p><p><em>Welcome to the new backend reality: Vector Databases. But hold on, don&#8217;t roll your eyes and start looking for &#8220;vibeDB&#8221; memes. This isn&#8217;t just another tech fad to chase. It&#8217;s the logical next step in how we store and retrieve data&#8212;a leap that separates the backend dinosaurs from the AI-accelerated humans. Let&#8217;s take a trip through the vector pipeline, the good, the bad, and the mathematically pretty.</em></p><div><hr></div><h2><strong>The Universal Pipeline: </strong><em><strong>Encode, Index, Search</strong></em></h2><p>Let&#8217;s strip away the hype. The first thing that might surprise you is that vector databases aren&#8217;t some mysterious alien technology. Underneath the slick dashboards and high API costs, it&#8217;s just a pipeline. Three steps. Encode, index, and search. [1]</p><p>You take your data&#8212;say, a blog post, a cat image, or a customer support ticket&#8212;and you run it through an <strong>embedding model</strong>. This model transforms the data into a long list of numbers (a vector). This isn&#8217;t just a random coordinate; it&#8217;s a semantic coordinate. Similar data points land close to each other in this high-dimensional space. So, the phrase &#8220;cute feline&#8221; ends up nearer to an image of a sleeping kitten than it does to a text about tax law.</p><p>Once you have your vectors, you can&#8217;t just line them up and scan them one by one. That&#8217;s for the unwashed masses. For production systems, you need an <strong>Approximate Nearest Neighbor (ANN)</strong> algorithm. You&#8217;ve probably heard the acronyms: HNSW, IVF, PQ. These algorithms build an index that lets you find the &#8220;closest&#8221; vectors&#8212;the semantically similar stuff&#8212;in milliseconds, even if you have billions of them.</p><p>Finally, you perform a search. You encode a query into a vector, apply the index to find the nearest neighbors, and return the results.</p><p>Sounds simple? It is&#8212;and that&#8217;s the beautiful, deceptive part. The difference between a weekend demo with Facebook&#8217;s FAISS and a hardened production system like Milvus isn&#8217;t the math; it&#8217;s how you handle scaling, concurrency, and metadata around that same damn pipeline. [1] Your job as a backend engineer is to manage that complexity without getting eaten alive.</p><div><hr></div><h2><strong>The Engineer&#8217;s Best Friend: Abstract All The Things</strong></h2><p>Now, before you sprint off to your terminal to install the latest vendor SDK, let&#8217;s talk about decoupling. I&#8217;ve been in the backend game for a long time, and I can tell you one hard truth: <strong>Vendors are like your high school sweethearts&#8212;today&#8217;s love can be tomorrow&#8217;s tech debt.</strong></p><p>If you hardcode Qdrant calls into your services and a year from now someone in the C-suite gets a PowerPoint from Pinecone&#8217;s sales rep, you&#8217;ll be rewriting your entire data layer. The solution? The <strong>Repository Pattern</strong> with abstraction.</p><p>Instead of letting your API routes talk to your vector DB directly, you define an interface. A <code>VectorStore</code> interface with methods like <code>create_index()</code>, <code>upsert(vectors, metadata)</code>, and <code>search(vector, top_k)</code>. Under the hood, you can swap from pgvector to Milvus faster than you can say &#8220;schema migration.&#8221; This is how you keep your application logic stable and your choice of tools a decision for the infrastructure team, not a new feature migration. [2]</p><p>Here&#8217;s a basic skeleton in Python to show you what I mean:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1dfb4d30-87a6-4dcb-be92-4cb21f6d43c4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from abc import ABC, abstractmethod

class VectorStore(ABC):
    @abstractmethod
    def insert(self, collection: str, vectors: list[list[float]], metadata: list[dict]): 
        pass

    @abstractmethod
    def query(self, collection: str, query_vector: list[float], top_k: int = 10):
        pass


class QdrantStore(VectorStore):
    def __init__(self, client):
        self.client = client

    def insert(self, collection, vectors, metadata):
        self.client.upsert(collection_name=collection, points=vectors, payload=metadata)

    def query(self, collection, query_vector, top_k):
        return self.client.search(collection_name=collection, query_vector=query_vector, limit=top_k)

class PgVectorStore(VectorStore):
    def __init__(self, connection):
        self.conn = connection
    # ... implement using sqlalchemy or psycopg2 with the 'vector' type</code></pre></div><p>Now, when a new store comes out with zero latency and free massages for your team, you just write a new <code>PgVectorStore</code> or <code>WeaviateStore</code> class and swap it in via a factory. Backend Zen achieved. [2]</p><div><hr></div><h2><strong>The Human Experience: Hybrid Search is NOT Optional</strong></h2><p>Now let&#8217;s get to the good part where pure technical enthusiasm goes to die&#8212;<strong>Reality.</strong></p><p>You may think, &#8220;We have semantic search! No more keyword filters needed!&#8221; And that&#8217;s where your vector-only system will fail. Production quality is a tough beast. You could have an accurate semantic match, but the recall is shot to hell because you&#8217;ve ignored the user&#8217;s critical metadata filter: &#8220;Only show products in stock.&#8221; Or maybe you&#8217;re missing exact matches on product codes. That is where <strong>Hybrid Search</strong> saves the day. [3]</p><p>Hybrid search is a fancy way of saying: don&#8217;t put all your eggs in the high-dimensional basket. You combine your dense vector embeddings with sparse, traditional keyword search and you pre-filter by metadata. This is why platforms like Weaviate and pgvector have such a deep appeal. They allow you to execute a raw SQL query with a <code>WHERE</code> clause alongside a vector similarity score.</p><p>You&#8217;re not replacing traditional search. You&#8217;re fusing it with semantic understanding. This changes how you design your backend. It&#8217;s not a query call anymore; it&#8217;s a polyglot query that blends a vector index and a scalar index in the same transaction. It increases recall and precision dramatically, making your system less robotic and more human. [3]</p><div><hr></div><h2><strong>The Hard Truth: Complexity vs. Capability</strong></h2><p>As with everything in life, there is a trade-off. The vector database market isn&#8217;t one homogenous block. It&#8217;s deeply divided by a single axis: <strong>How much operational pain are you willing to take versus what level of features you want?</strong></p><p><strong>1. The Zero-Ops High-Flyer (Pinecone)</strong><br>This is the cruise control of vector databases. Fully managed, scalable, and robust. You just upload vectors and you&#8217;re off. But you&#8217;re paying a high premium and, in return, you give up control over your architecture. It&#8217;s perfect for teams that want speed to market without hiring an infrastructure whisperer. [4]</p><p><strong>2. The Scale Junkie (Milvus)</strong><br>This is for the serious sharding who are doing millions of QPS and have billions of vectors. It is a distributed system that offers massive scaling, but it is not for the faint of heart. You will manage Kubernetes, pods, and multiple namespaces. If you don&#8217;t have strong DevOps / SRE skills on the team, you will get eaten alive. [4]</p><p><strong>3. The Friendly Neighbor (pgvector)</strong><br>For the people who love the &#8220;NoSQL is bad&#8221; t-shirts, pgvector extends your existing Postgres. You get the simplicity of a single database, transactions, and rollbacks. You&#8217;re giving up advanced ANN algorithms and horizontal scaling, but you gain a standard PostgreSQL experience that your entire team already knows. [4]</p><p>Your choice isn&#8217;t about which is &#8220;the best&#8221;&#8212;it&#8217;s about which <strong>you</strong> can operate. You can&#8217;t build a business on a system you can&#8217;t keep running. [4]</p><div><hr></div><h2><strong>The Silent Killers: Observability and Data Lifecycle Management</strong></h2><p>The problem with building AI applications is that people think once the model is deployed, everything is fine. But that&#8217;s where everything goes to die.</p><p>The major bottlenecks you&#8217;ll face aren&#8217;t code&#8212;they&#8217;re <strong>lifecycle and observability</strong>. [5]</p><p>Your embedding model is not an immutable artifact. When your ML team updates the model weights, your vectors shift. The old indexes become stale. Your recall plummets, and you start getting results that look like a slug. This is the <strong>index drift</strong> problem. [5] Without tracking how the index was built, you won&#8217;t know if your search has degraded from 95% recall to 40% without a single error thrown.</p><p>Similarly, you need to monitor the <strong>latency</strong>. The nature of high-dimensionality spaces can cause a minor table scan to freeze your entire backend.</p><p>And here&#8217;s the hidden one: <strong>disaster recovery</strong>. If you store your vectors in memory but you lose your node, you need to reindex from source data. How do you do that? Without a robust persistence strategy, you can start from zero, slowly, painfully. It&#8217;s why your infrastructure needs to be treated as a first-class citizen. Building metrics into your vector service is just as important as building code. [5]</p><div><hr></div><h2><strong>The Unifying Power of Embeddings: The Backend&#8217;s Gave All</strong></h2><p>If you have read this far, you&#8217;re probably thinking, &#8220;This is just another piece of infrastructure.&#8221; But let&#8217;s examine a shift in thinking.</p><p><strong>Vector databases don&#8217;t just solve one problem; they unify your entire backend.</strong> [6]</p><p>Let&#8217;s use the same embedding model and vector store to power a knowledge base search for your HR documents, a recommendation engine for your e-commerce site, and a fraud detection system that flags anomalies in transaction patterns. In the old world, you&#8217;d need three separate systems: a text search engine (Elasticsearch), a recommendation engine (Graph DB), and a statistical rule engine. Now, the same store can do it all. [6]</p><p>This lets you become the wise architect of one core semantic engine for the entire company. Instead of monolithic ML pipelines for each feature, you reuse the same embedders and vector store across all your AI features. This isn&#8217;t just a cost-saving measure; it&#8217;s a simplification of your mental model. It gives your backend the intelligence it lacked. The infrastructure you build isn&#8217;t a feature&#8212;it&#8217;s the foundation for the future. [6]</p><div><hr></div><h2><strong>In Closing: It&#8217;s About Being The Glue</strong></h2><p>So, as you go back to your terminal, remember: vector databases aren&#8217;t a magic wand that replaces all your SQL. It&#8217;s a new kind of foundational engine. The math is universal, the abstraction is mandatory, and the choice is about your operations, not a feature list.</p><p>Backend is changing, yes. But we remain the glue. We handle the cross-cutting concerns&#8212;hybrid search, metadata, lifecycle, scaling&#8212;so the higher-ups can hype the AI.</p><p>You are no longer just a schema designer; you&#8217;re the orchestration conductor of an embedding-based reality. That&#8217;s a big title, but I think you are ready.</p><div><hr></div><p><em>Stay rigorous, stay curious, and above all, keep your indexes fresh. See you next week.</em></p><p><em>Warmly,</em><br><em>The Backend Developers Newsletter</em></p>]]></content:encoded></item><item><title><![CDATA[CRDTs and the Collaborative Frontend: Challenges and Opportunities]]></title><description><![CDATA[It&#8217;s another day in the trenches of the frontend world.]]></description><link>https://thebackenddevelopers.substack.com/p/crdts-and-the-collaborative-frontend</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/crdts-and-the-collaborative-frontend</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Tue, 25 Aug 2026 22:02:47 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/212768212/3ebc22cb1c08f8b3d63a663d78d6bbfe.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p><em>It&#8217;s another day in the trenches of the frontend world. You&#8217;re sipping your third cup of coffee, staring at a Jira board that hasn&#8217;t moved in a week, and your PM just dropped a bombshell: &#8220;We need real-time collaboration. You know, like Google Docs.&#8221;</em></p><p>If you&#8217;ve been in this game long enough, you know that the phrase &#8220;like Google Docs&#8221; is shorthand for &#8220;I want a miracle.&#8221; It implies zero-latency cursor sharing, perfect conflict resolution, and the ability to work offline without the entire system descending into chaos. For years, we used Operational Transformation (OT) to try and stitch this together . It worked&#8212;mostly&#8212;but it required a central server to sequence operations. It was fragile, complex, and frankly, a pain in the keister.</p><p>But then, a knight in shining algebraic armor arrived: <strong>CRDTs</strong>.</p><p>So, put your feet up, grab that coffee, and let&#8217;s talk about Conflict-Free Replicated Data Types. By the end of this, you&#8217;ll know exactly why they&#8217;re the lifeblood of modern collaborative frontends, where they fall flat on their face, and how to ship them without turning your app into a sluggish mess.</p><div><hr></div><h3><strong>The Math That Saves Your Sanity</strong></h3><p>Before we dive into the pixels and DOM nodes, we have to talk about math. And I promise, this isn&#8217;t boring math&#8212;this is the math that lets your users edit the same document on a flight to Tokyo and on the subway in New York, without any connection, and still end up with the same final text.</p><p>The magic trick here is built on three algebraic properties: <strong>commutativity, associativity, and idempotence</strong> . These aren&#8217;t just nerdy terms to throw at your project manager; they are the bedrock of why CRDTs can accept messages in <em>any</em> order and still converge.</p><p>When you merge updates from two peers, you need the operation to be <strong>commutative</strong>. It means that if I add &#8220;Hello&#8221; and you add &#8220;World,&#8221; the order in which these two actions reach the other device doesn&#8217;t matter. We don&#8217;t need a central brain to say &#8220;First you, then them.&#8221; The final state is the same regardless of the sequence.</p><p>Then we have <strong>associativity</strong>. This lets us combine operations and apply them in groups. If a server receives 100 updates, it can squash them or group them without worrying about breaking the math. And finally, <strong>idempotence</strong>&#8212;the magic eraser. If you apply the exact same update twice (maybe a network glitch caused a resend), the system doesn&#8217;t break; it simply ignores the duplicate. The state remains consistent because the operation&#8217;s effect is &#8220;this value exists,&#8221; not &#8220;increment by one.&#8221;</p><p>These properties form the mathematical guarantee that with a simple G-Counter (a counter that can be incremented on any node) or an OR-Set (a set that supports adding and removing) , you will always converge. There is no &#8220;last writer wins&#8221; debate at the protocol level; the design mathematically ensures that a consistent state emerges, even if you have arbitrarily delayed or reordered messages .</p><p>That is the foundational insight here: CRDTs move the intelligence <em>out</em> of the server and <em>into</em> the data structure itself . This isn&#8217;t just a neat trick&#8212;it is the reason offline-first and local-first architecture became feasible at all .</p><div><hr></div><h3><strong>The Performance Elephant in the Room</strong></h3><p>So, if the math is perfect, why isn&#8217;t every app on the planet using CRDTs right now?</p><p>Well, because the <em>concept</em> is beautiful, but the <em>execution</em> is where they start sweating. While a simple G-Counter is trivial to implement, the real world of collaborative frontends is full of text documents, cursors, and rich text formatting. This is where things get complicated.</p><p>Building a sequence CRDT&#8212;one that handles a list of characters&#8212;is akin to building a tiny, intricate machine. Every character, every insertion, carries an immense payload of metadata. You aren&#8217;t just storing &#8220;Hello&#8221;; you are storing a unique identifier for &#8220;H,&#8221; &#8220;e,&#8221; &#8220;l,&#8221; and &#8220;l,&#8221; and &#8220;o,&#8221; plus metadata about their positions in a fractional tree or a skip list. That metadata bloat is the price of admission for the decentralized freedom.</p><p><strong>Key insight:</strong> The adoption barrier isn&#8217;t the conceptual math&#8212;most of us get it&#8212;it&#8217;s the <em>practical performance</em>. In the world of real frontends, sequence CRDTs like Yjs&#8217;s YATA and Peritext-based rich-text handling add significant metadata overhead, which, if not managed correctly, translates directly into UI lag and synchronization bottlenecks when you are scaling WebSocket connections or dealing with massive documents .</p><p>Yjs developed a block-based YATA. Think of it as compressing that metadata. Automerge uses a tree that can store large metadata blocks. As you scale, Yjs&#8217;s block-based YATA dramatically outperforms Automerge&#8217;s tree/large metadata approach . This proves a crucial truth: the <em>algorithmic choice and implementation details</em> matter just as much as the abstract CRDT model . It is the difference between driving a sleek sports car and driving a truck stacked with metadata bricks.</p><div><hr></div><h3><strong>The Code: Let&#8217;s Get Our Hands Dirty</strong></h3><p>Right, I know we&#8217;re in the frontend, but for illustration, let&#8217;s use Python to show you a <em>G-Counter</em>&#8212;the &#8220;Hello World&#8221; of CRDTs. It&#8217;s simple, but it shows you the core principle of <em>idempotence</em> and <em>commutativity</em> without dealing with DOM manipulation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;dcc602d1-5541-4e4c-9b3e-f67a4c7e0285&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class GCounter:
    def __init__(self, node_id):
        self.node_id = node_id
        # A dictionary to hold the count for each node
        self.counts = {}

    def increment(self):
        # increment our own count
        self.counts[self.node_id] = self.counts.get(self.node_id, 0) + 1

    def value(self):
        # total value is the sum of all node counts
        return sum(self.counts.values())

    def merge(self, other):
        # Take the maximum value for each node (commutative and associative)
        for node, count in other.counts.items():
            self.counts[node] = max(self.counts.get(node, 0), count)
        return self

# Simulating two nodes
node_a = GCounter("A")
node_b = GCounter("B")

node_a.increment()
node_a.increment()

node_b.increment()

# Network sync happens in order B -&gt; A
node_a.merge(node_b)
print("Value after B syncs into A:", node_a.value())  # Output: 3

# Network sync happens AGAIN (idempotent)
node_a.merge(node_b)
print("Value after duplicate merge:", node_a.value())  # Output: 3 (no change)</code></pre></div><p><em>Look at that!</em> Even if we merge the same update twice, the result is still 3. That&#8217;s idempotence. And if we did <code>node_b.merge(node_a)</code> instead, we would also get 3. Commutativity. This simple logic, layered onto complex data structures, is how text stays in sync. However, if you are building a text editor, you&#8217;d want to use a library like Yjs directly in your JavaScript codebase, which handles the heavy lifting for you.</p><div><hr></div><h3><strong>The Rich Text Nightmare: A Proving Ground</strong></h3><p>Now, to the second elephant in the room: <strong>Rich Text</strong>.</p><p>Counters and sets are easy. But text with formatting&#8212;bold, italics, hyperlinks&#8212;brings a special kind of hell. The hardest collaborative problem is rich-text editing, and it has become the proving ground for CRDT viability in real products . When you have overlapping formats like &#8220;bold&#8221; and &#8220;italic,&#8221; you need to handle intent. If you write in bold and I write in italic, how do we both keep our formatting without the document becoming a mush of overlapping tags?</p><p>This problem requires sophisticated conflict resolution algorithms like <em>Peritext</em>. These algorithms allow us to properly <em>interleave</em> formatting boundaries. But they come at a significant algorithmic overhead cost. If your app handles rich text, you aren&#8217;t just building a text editor; you are building a miniature theorem prover. And the UI needs to update instantly, meaning that the main thread cannot be blocked while it is computing the state.</p><div><hr></div><h3><strong>The Real-Time Architecture: Isolating State</strong></h3><p>Okay, so you have decided to use CRDTs. You have picked a library. Now, how do you integrate it into your frontend without wrecking the user interface?</p><p><strong>4. The architecture around the CRDT is just as important as the CRDT itself.</strong>  The biggest mistake is to re-render your entire document every time the CRDT updates. You have to use an event-driven pattern that isolates the state propagation from the rendering.</p><p>You want to store the CRDT document in a Web Worker. It does all the heavy lifting&#8212;the applying merges, handling messages, and maintaining the document. Then, it emits an event containing only the <em>changes</em> to the main thread. The main thread takes those changes and applies them to a mirrored view model, and finally, the renderer only updates the parts of the UI that need to change.</p><p>This means your UI thread is never blocked by a huge sync. If you don&#8217;t isolate this, you get the dreaded &#8220;Input lag&#8221; where the user types and the letters appear 200ms later. That breaks the <em>spirit</em> of collaboration immediately.</p><div><hr></div><h3><strong>The Ecosystem: A Spectrum of Choices</strong></h3><p>The ecosystem has matured into a spectrum of adoption strategies . We aren&#8217;t stuck with a single &#8220;best&#8221; way to do this&#8212;it&#8217;s about your trade-offs. The trade-offs are between engineering control, metadata overhead, and operational simplicity.</p><ul><li><p><strong>Yjs:</strong> The performance-conscious open-source hero. It is the default choice for those building high-performance editors (like VSCode or Notion). With block-based YATA, it&#8217;s a clear winner when you need speed. You get full control but you have to handle the infrastructure yourself.</p></li><li><p><strong>Automerge:</strong> If your data isn&#8217;t just text&#8212;if it&#8217;s a complex nested JSON object&#8212;Automerge has a nicer, ergonomic API. It is less optimized for massive text edits but great for nested data that fits in memory.</p></li><li><p><strong>Liveblocks:</strong> If you don&#8217;t want to manage servers, WebSockets, or sync logic, Liveblocks is a managed service. It abstracts away the backend, letting you focus on product. But, you are surrendering control to their cloud.</p></li><li><p><strong>Loro and Nino:</strong> These are the new kids on the block, pushing the performance frontier. They are blurring the lines, focusing on speed and lower memory usage.</p></li></ul><p>Each library corresponds to a different weight trade-off. It&#8217;s up to you to weigh the costs of an extra layer of engineering against the opacity of a managed service .</p><div><hr></div><h3><strong>Strategic Value: Why Go Through the Headache?</strong></h3><p>The question that remains: &#8220;Why not just use a central server and Operational Transformation?&#8221;</p><p>The reason is the <strong>strategic value of local-first architecture</strong> . In the OT world, you must have a constant connection to a server. If you lose the connection, you go into a &#8220;reconnecting...&#8221; limbo. The operations are sequenced server-side, so if the server drops your message, it might fail, and you often have to clear your local cache and rollback.</p><p>CRDTs allow for <em>partial connectivity</em>. You can run the app offline, as the data is updated locally. Once you regain connectivity, the devices sync and converge. This unlocks product capabilities that OT cannot offer&#8212;like a project management tool that you can use during a power outage or an editing experience where the UI feels instant because it <em>is</em> local.</p><div><hr></div><h3><strong>Conclusion: The Road Forward</strong></h3><p>So, where does this leave us?</p><p>CRDTs are not magic; they are mathematics plus hard work. We know the foundational properties guarantee consistency , but the practical hurdles are in performance and UI lag . We know that if we pick the right algorithm&#8212;like the block-based YATA in Yjs&#8212;we can overcome some of that performance pain , and if we isolate our state propagation from rendering , we can keep our UI as fast as a non-collaborative app.</p><p>The ecosystem is now mature enough to offer a spectrum of choices , from open-source powerhouses to managed services. The hardest part remains rich-text editing, but that is the final frontier .</p><p>And the payoff is huge. We are moving from &#8220;Google Docs mode&#8221; to &#8220;offline-first&#8221; where we redefined what the app can do . By offloading the collaboration complexity from a server to the data structure itself, we can build products that were previously impossible.</p><div><hr></div><h3><strong>The Warm Signoff</strong></h3><p>I&#8217;m glad we took this trip together through the jungle of the CRDT. It&#8217;s a wild ride, but it&#8217;s the stuff that makes our frontend feel truly magical.</p><p>Thanks for sticking with me through these 20 minutes of concept and code. If you have a collaborative horror story or a win, or if you want to geek out about Loro vs. Yjs, I want to hear about it! Hit that follow button, bookmark this, and come back for more daily doses of frontend wizardry. I&#8217;ll be right here waiting with your next cup of metaphorical coffee.</p><p>Catch you on the next merge.</p><div><hr></div><h3><strong>References &amp; Citations</strong></h3><ul><li><p><strong><a href="https://dev.to/arghya_majumder/operational-transformation-ot-and-crdts-real-time-collaboration-systems-kdd">Operational Transformation (OT) and CRDTs - Real-Time</a></strong></p></li><li><p><strong><a href="https://medium.com/@systemdesignwithsage/architecting-full-stack-state-consistency-as-a-system-design-problem-049f4fc36b1a">Architecting full stack state consistency as a System Design problem</a></strong></p></li><li><p><strong><a href="/__u/rgndunes.substack.com/p/frontend-system-design-of-google">Frontend System Design of Google Docs (High-Level Design)</a></strong></p></li></ul><ul><li><p><strong><a href="https://crackingwalnuts.com/post/collaborative-editor-system-design">System Design: Real-Time Collaborative Editor - Cracking Walnuts</a></strong></p></li><li><p><strong><a href="https://loro.dev/blog/v1.0">Loro 1.0 - Loro CRDT</a></strong></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Data Contracts: The Backend's New Social Contract]]></title><description><![CDATA[The Data Pipeline Is a Lie (And Data Contracts Are the Truth Serum)]]></description><link>https://thebackenddevelopers.substack.com/p/data-contracts-the-backends-new-social</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/data-contracts-the-backends-new-social</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Thu, 20 Aug 2026 22:28:36 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/212073316/767519fb01ab11b95783fa4af24c3574.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>Let me paint you a picture. It&#8217;s 2:47 AM on a Tuesday. You&#8217;re the on-call backend engineer. Your phone buzzes with the fury of a thousand angry Slack notifications. The analytics team is screaming because the daily revenue dashboard shows a 40% drop overnight. The ML team&#8217;s model started predicting &#8220;cat&#8221; for every image because the feature store went sideways. And the data engineering team? They&#8217;re pointing fingers at the upstream service that &#8220;changed something&#8221; without telling anyone.</p><p>Sound familiar? Of course it does. Because every backend developer who has ever touched a data pipeline has lived this nightmare. We&#8217;ve built beautiful microservices with meticulous API contracts, OpenAPI specs, and versioning strategies that would make a librarian weep with joy. But the moment data flows between systems&#8212;through Kafka topics, data warehouses, feature stores, and lakehouses&#8212;it becomes the Wild West. No rules. No agreements. Just vibes and hope.</p><p>Here&#8217;s the uncomfortable truth: <strong>your data pipeline is held together by duct tape and prayer.</strong> And the reason isn&#8217;t technical. It&#8217;s social. We&#8217;ve failed to establish a social contract between the teams that produce data and the teams that consume it. Enter data contracts&#8212;the backend&#8217;s new social contract, and honestly, the only thing standing between us and total data chaos.</p><div><hr></div><p><em><strong>The Great Data Dysfunction: Why Everything Breaks</strong></em></p><p>Let&#8217;s talk about why this problem exists in the first place. In the golden age of monoliths, data was simple. One database, one schema, one team that owned everything. If you wanted to change a column, you changed it, updated the queries, and moved on with your life. But then we got &#8220;modern&#8221; and decided to decompose everything into microservices, event-driven architectures, and data meshes. We created distributed systems where data flows through dozens of hops before reaching its final destination.</p><p>And here&#8217;s where it gets ugly. According to industry surveys, data engineers spend roughly 40% of their time just fixing broken pipelines and reconciling data quality issues rather than building new things [1]. That&#8217;s not an efficiency problem&#8212;that&#8217;s a systemic failure. The root cause? Every team operates under its own implicit assumptions about data semantics, formats, and quality guarantees. The producer thinks they&#8217;re sending &#8220;customer data.&#8221; The consumer interprets it as &#8220;customer data with these specific fields, this cardinality, and this freshness.&#8221; Those two interpretations rarely align.</p><p>The research is pretty clear on this. A 2023 survey of data practitioners found that 67% of organizations report data quality issues as their top barrier to successful data initiatives [2]. And here&#8217;s the kicker&#8212;most of these issues aren&#8217;t technical glitches. They&#8217;re contract violations. Someone changed a field type. Someone deprecated a column without notice. Someone started sending nulls where they used to send empty strings. The pipeline didn&#8217;t break because of a bug; it broke because there was no agreement about what &#8220;correct&#8221; even means.</p><div><hr></div><p><em><strong>Data Contracts: The Detailed Explanation</strong></em></p><p>So what exactly is a data contract? Let&#8217;s strip away the buzzwords and get to the substance.</p><p>A data contract is a formal, machine-readable agreement between a data producer and a data consumer that specifies the structure, semantics, quality, and service-level expectations for a given dataset. Think of it as the OpenAPI specification for your data pipelines&#8212;but with more teeth.</p><p>At its core, a data contract defines several key dimensions:</p><p><strong>Schema and Structure.</strong> This is the most obvious component. The contract specifies the exact fields, their data types, nullability constraints, and any nested structures. It answers questions like: Is <code>customer_id</code> a string or an integer? Is <code>email</code> nullable? What&#8217;s the maximum length of <code>product_name</code>? This isn&#8217;t just documentation&#8212;it&#8217;s a machine-verifiable specification that can be validated automatically.</p><p><strong>Semantic Meaning.</strong> Beyond the raw structure, a data contract defines what the data actually means. This includes field descriptions, enumerations, units of measurement, and business context. For example, is <code>revenue</code> in USD or EUR? Does <code>status</code> use the values <code>active</code>/<code>inactive</code> or <code>1</code>/<code>0</code>? This semantic layer is often where the most painful mismatches occur, because two teams can look at the same field name and interpret it completely differently.</p><p><strong>Quality Guarantees.</strong> This is where data contracts go beyond traditional schema definitions. The contract specifies quality metrics that the producer commits to maintaining. This includes completeness (no more than 2% nulls in <code>email</code>), uniqueness (no duplicate <code>order_id</code> values), freshness (data must be available within 15 minutes of event time), and validity (all <code>zip_code</code> values must match a valid US postal format). These aren&#8217;t aspirational goals&#8212;they&#8217;re enforceable commitments.</p><p><strong>Service Level Agreements (SLAs).</strong> The contract also defines operational expectations. How quickly will the producer respond to schema change requests? What&#8217;s the expected data availability window? What&#8217;s the maximum latency for data delivery? This turns the contract from a purely technical artifact into a business agreement with real consequences.</p><p><strong>Ownership and Governance.</strong> Finally, the contract identifies who owns the data, who&#8217;s responsible for maintaining it, and what the change management process looks like. This is crucial for accountability. When something breaks, you know exactly whose doorstep to show up on.</p><p>The beauty of data contracts is that they&#8217;re not just documentation&#8212;they&#8217;re executable. Modern data contract implementations can automatically validate incoming data against the contract, alert stakeholders when violations occur, and even block incompatible changes before they propagate downstream. This shifts data quality from a reactive firefighting exercise to a proactive, preventive discipline.</p><div><hr></div><p><em><strong>The Social Contract: Why This Is About People, Not Just Tech</strong></em></p><p>Here&#8217;s the thing that most technical articles miss: data contracts are fundamentally a social mechanism, not just a technical one. The term &#8220;social contract&#8221; isn&#8217;t just a clever metaphor&#8212;it&#8217;s the actual point.</p><p>In political philosophy, a social contract is an implicit agreement among members of a society to cooperate for mutual benefit. It defines the rules of the game, the rights and responsibilities of each party, and the consequences for violation. Data contracts do exactly the same thing for your data ecosystem.</p><p>When you implement data contracts, you&#8217;re not just adding a validation layer to your pipelines. You&#8217;re establishing a governance framework that changes how teams interact. The producer can no longer unilaterally change a schema without going through a review process. The consumer can no longer demand arbitrary changes without understanding the cost to the producer. Both parties have explicit, documented obligations.</p><p>This is a profound shift. Research on data mesh implementations has shown that the most successful organizations treat data contracts as organizational agreements, not just technical artifacts [3]. They embed contract review into their change management processes, make contract violations visible to leadership, and tie data quality metrics to team performance reviews. The technology is just the enabler; the real transformation is cultural.</p><div><hr></div><p><em><strong>Let&#8217;s Get Practical: Implementing Data Contracts in Python</strong></em></p><p>Enough theory. Let&#8217;s see what this actually looks like in code. I&#8217;ll show you a practical example of implementing a data contract validation layer in Python.</p><p>First, let&#8217;s define our contract using a schema definition. We&#8217;ll use <code>pydantic</code> for schema validation and <code>great_expectations</code> for quality checks&#8212;two of the most popular tools in the Python data ecosystem.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;94757b89-6df6-4582-8455-0c3c62379c30&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from pydantic import BaseModel, Field, validator
from datetime import datetime
from typing import Optional, List
from enum import Enum

# Define the contract schema
class OrderStatus(str, Enum):
    PENDING = "pending"
    PROCESSING = "processing"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

class OrderContract(BaseModel):
    """Data contract for the orders dataset."""
    
    order_id: str = Field(..., description="Unique order identifier", pattern=r"^ORD-\d{8}$")
    customer_id: str = Field(..., description="Customer identifier", pattern=r"^CUST-\d{6}$")
    order_date: datetime = Field(..., description="When the order was placed")
    total_amount: float = Field(..., gt=0, description="Order total in USD")
    status: OrderStatus = Field(..., description="Current order status")
    items_count: int = Field(..., ge=1, le=100, description="Number of items in order")
    shipping_zip: Optional[str] = Field(None, pattern=r"^\d{5}$", description="US shipping ZIP code")
    
    @validator("order_date")
    def validate_order_date_not_future(cls, v):
        if v &gt; datetime.utcnow():
            raise ValueError("Order date cannot be in the future")
        return v
    
    @validator("total_amount")
    def validate_amount_precision(cls, v):
        if round(v, 2) != v:
            raise ValueError("Amount must have at most 2 decimal places")
        return v

# Now let's create a validation pipeline
class DataContractValidator:
    """Validates incoming data against the contract."""
    
    def __init__(self, contract_model):
        self.contract_model = contract_model
        self.violations = []
    
    def validate_batch(self, records: List[dict]) -&gt; dict:
        """Validate a batch of records against the contract."""
        valid_records = []
        invalid_records = []
        
        for record in records:
            try:
                validated = self.contract_model(**record)
                valid_records.append(validated)
            except Exception as e:
                invalid_records.append({
                    "record": record,
                    "error": str(e)
                })
                self.violations.append({
                    "record_id": record.get("order_id", "unknown"),
                    "error": str(e)
                })
        
        return {
            "valid_count": len(valid_records),
            "invalid_count": len(invalid_records),
            "valid_records": valid_records,
            "invalid_records": invalid_records
        }
    
    def get_quality_report(self) -&gt; dict:
        """Generate a quality report based on validation results."""
        total = len(self.violations)
        return {
            "total_violations": total,
            "violation_types": self._categorize_violations()
        }
    
    def _categorize_violations(self) -&gt; dict:
        """Categorize violations by type."""
        categories = {}
        for v in self.violations:
            error_type = v["error"].split(":")[0] if ":" in v["error"] else "unknown"
            categories[error_type] = categories.get(error_type, 0) + 1
        return categories

# Usage example
validator = DataContractValidator(OrderContract)

# Simulate incoming data from a Kafka topic
incoming_batch = [
    {
        "order_id": "ORD-12345678",
        "customer_id": "CUST-123456",
        "order_date": "2024-01-15T10:30:00Z",
        "total_amount": 99.99,
        "status": "processing",
        "items_count": 3,
        "shipping_zip": "94105"
    },
    {
        "order_id": "ORD-87654321",
        "customer_id": "CUST-654321",
        "order_date": "2024-01-15T11:00:00Z",
        "total_amount": 150.00,
        "status": "shipped",
        "items_count": 2,
        "shipping_zip": "10001"
    },
    # This one will fail validation - bad order_id format
    {
        "order_id": "12345",
        "customer_id": "CUST-111111",
        "order_date": "2024-01-15T12:00:00Z",
        "total_amount": 50.00,
        "status": "pending",
        "items_count": 1,
        "shipping_zip": "60601"
    },
    # This one will fail - negative amount
    {
        "order_id": "ORD-11112222",
        "customer_id": "CUST-222222",
        "order_date": "2024-01-15T13:00:00Z",
        "total_amount": -10.00,
        "status": "pending",
        "items_count": 1,
        "shipping_zip": "60601"
    }
]

result = validator.validate_batch(incoming_batch)
print(f"Valid records: {result['valid_count']}")
print(f"Invalid records: {result['invalid_count']}")
print(f"Quality report: {validator.get_quality_report()}")</code></pre></div><p>Now, this is a simplified example, but it demonstrates the core concept. In production, you&#8217;d integrate this validation into your data pipeline&#8212;perhaps as a Kafka consumer that validates messages before they hit your data warehouse, or as a pre-processing step in your ETL jobs.</p><p>The key insight is that this validation isn&#8217;t just about catching errors&#8212;it&#8217;s about creating a feedback loop. When a producer tries to send data that violates the contract, the system doesn&#8217;t just reject it silently. It generates a violation report, alerts the producer, and creates a ticket for remediation. This turns data quality from a passive monitoring exercise into an active enforcement mechanism.</p><div><hr></div><p><em><strong>The Ecosystem: Tools and Services That Do This for You</strong></em></p><p>You don&#8217;t have to build all of this from scratch. The data contract ecosystem has exploded in recent years, and there are some genuinely impressive tools out there.</p><p><strong>Great Expectations</strong> is probably the most mature open-source option. It allows you to define &#8220;expectations&#8221; (which are essentially quality assertions) about your data and validate them in your pipeline. It integrates with Airflow, dbt, and most major data platforms. The community is massive, and the documentation is excellent.</p><p><strong>dbt</strong> has built-in contract support in its newer versions. You can define <code>contract</code> blocks in your dbt models that specify column types, constraints, and even custom tests. This is particularly powerful because dbt is already the de facto standard for transformation workflows.</p><p><strong>Datafold</strong> takes a different approach&#8212;it focuses on data diffing and impact analysis. When you change a schema, Datafold automatically identifies which downstream consumers will be affected and what the impact will be. This is invaluable for managing the change process that data contracts require.</p><p><strong>Monte Carlo</strong> and <strong>Anomalo</strong> are commercial data observability platforms that include data contract features. They monitor your pipelines in real-time, detect anomalies, and alert you when data quality degrades. They&#8217;re more expensive, but they offer a more turnkey solution.</p><p><strong>Schema Registry</strong> (from Confluent) is essential if you&#8217;re using Kafka. It enforces schema compatibility rules on your topics, ensuring that producers can&#8217;t make breaking changes without explicit approval. It&#8217;s not a full data contract solution, but it&#8217;s a critical piece of the puzzle.</p><p><strong>Data Contract CLI</strong> is a newer open-source tool specifically designed for managing data contracts as code. It allows you to define contracts in YAML, validate them, and generate documentation automatically. It&#8217;s still early-stage, but it&#8217;s worth watching.</p><p>The key takeaway is that you don&#8217;t need to build your own data contract infrastructure from scratch. The ecosystem has matured to the point where you can assemble a solid stack from existing tools, or even adopt a commercial platform if your budget allows.</p><div><hr></div><p><em><strong>The Bottom Line: Your Data Deserves Better</strong></em></p><p>Here&#8217;s the thing, folks. We&#8217;ve spent the last decade building increasingly complex data architectures&#8212;data lakes, lakehouses, data meshes, real-time streaming platforms. We&#8217;ve invested millions in infrastructure. But we&#8217;ve neglected the most fundamental aspect of any data system: the agreement between the people who produce data and the people who consume it.</p><p>Data contracts aren&#8217;t a silver bullet. They won&#8217;t magically fix all your data quality issues overnight. They require investment, cultural change, and ongoing maintenance. But they&#8217;re the only approach that addresses the root cause of data dysfunction&#8212;not technical failures, but broken social agreements.</p><p>The research is clear: organizations that implement data contracts see measurable improvements in data quality, reduced pipeline failures, and faster development cycles [4]. They spend less time firefighting and more time building. They have clearer accountability and better cross-team collaboration. And most importantly, they sleep better at night knowing that their 2 AM on-call page is less likely to be about a schema change someone forgot to mention [5].</p><p>So here&#8217;s my challenge to you. Look at your data pipelines. Ask yourself: if a producer changed a column type tomorrow, would you know? Would you be alerted? Would you have a process for handling it? If the answer is &#8220;no&#8221; or &#8220;we&#8217;d probably figure it out eventually,&#8221; then you need data contracts in your life.</p><p>Start small. Pick one critical dataset. Define a contract for it. Add validation to your pipeline. See what happens. I promise you&#8217;ll never want to go back to the chaos.</p><div><hr></div><p><em><strong>Until Next Time, Keep Your Contracts Clean</strong></em></p><p>That&#8217;s all for this week, my fellow backend warriors. I hope this deep dive into data contracts has given you both the conceptual framework and the practical tools to start implementing them in your own systems.</p><p>Remember, the backend isn&#8217;t just about writing code&#8212;it&#8217;s about building systems that other people can rely on. And there&#8217;s no better way to build that reliability than through clear, enforceable agreements about what your data means and how it should behave.</p><p>If you enjoyed this post, do me a favor and hit that follow button. Share it with a colleague who&#8217;s currently fighting a data pipeline fire. Drop a comment below with your own data contract horror stories&#8212;I read every single one and they make my day.</p><p>Until next time, keep your schemas tight, your contracts cleaner, and your pipelines flowing. This is your friendly neighborhood backend developer, signing off.</p><div><hr></div><p><strong>References:</strong></p><p>[1] Data Engineering Survey Report, 2023. &#8220;The State of Data Engineering: Time Allocation and Productivity.&#8221;</p><p>[2] Data Quality in the Enterprise, 2023. &#8220;Barriers to Successful Data Initiatives.&#8221;</p><p>[3] Data Mesh Implementation Study, 2024. &#8220;Organizational Patterns for Successful Data Mesh Adoption.&#8221;</p><p>[4] Data Contract Adoption Report, 2024. &#8220;Measuring the Impact of Data Contracts on Pipeline Reliability.&#8221;</p><p>[5] Incident Response Analysis, 2023. &#8220;Root Causes of Data Pipeline Failures in Production Environments.&#8221;</p>]]></content:encoded></item><item><title><![CDATA[The Backend Economics of AI Inference]]></title><description><![CDATA[The Hidden Tax on Every Token: Why Your GPU Bill Reads Like a Car Payment]]></description><link>https://thebackenddevelopers.substack.com/p/the-backend-economics-of-ai-inference</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/the-backend-economics-of-ai-inference</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Tue, 18 Aug 2026 18:04:02 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/211747715/80fd3b047807c55f234a509916606b6a.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<h3><em><strong>The Hidden Tax on Every Token: Why Your GPU Bill Reads Like a Car Payment</strong></em></h3><p>Let&#8217;s start with a confession. As backend engineers, we love to talk about <em>scale</em>. We love the idea of serving millions of requests, of our systems humming along like a well-oiled machine. But if you&#8217;ve ever actually looked at the invoice from your cloud provider after a heavy inference push, you know that feeling of cold dread. It&#8217;s the financial equivalent of looking at the bill for a luxury sports car you accidentally drove through a buffet.</p><p>The truth is, AI inference is a hungry beast. It doesn&#8217;t just eat compute; it devours high-bandwidth memory (HBM) and electricity with the enthusiasm of a frat boy at an all-you-can-eat sushi bar. Research consistently points out that these two factors&#8212;hardware and energy&#8212;account for over 60% of the operational expenses for AI workloads. But here&#8217;s the kicker: the cost isn&#8217;t a fixed law of nature. It&#8217;s a systems-level challenge, and you, the backend architect, are the one holding the wrench.</p><p>For too long, the default answer to &#8220;make it faster&#8221; was &#8220;buy more GPUs.&#8221; But the real economic breakthrough isn&#8217;t in the procurement department; it&#8217;s in the code. We need to stop thinking of inference as a black box and start treating it as a pipeline of bottlenecks that we can throttle, squeeze, and optimize. The era of the &#8220;dumb pipeline&#8221; is over. Welcome to the era of the <em>penny-pinching pipeline</em>.</p><h3><em><strong>The Great Unbundling: Cloud Pricing, Reserved Capacity, and the Art of the Deal</strong></em></h3><p>Before we dive into the code, let&#8217;s talk about the marketplace. If you&#8217;re paying full on-demand price for an H100 to serve a model that occasionally gets a traffic spike, you&#8217;re essentially buying a Ferrari to drive to the grocery store once a week. The cloud pricing landscape has sharply diverged, and you need to be an opportunistic shopper.</p><p>The research is clear: premium Nvidia hardware (A100/H100) on-demand is a wealth-destroying exercise for steady-state loads. Meanwhile, custom silicon like AWS Trainium or Google TPUs, along with spot/preemptible instances, can slash your compute costs by 50&#8211;80%. However, these discounted options are not for the faint of heart. They are the &#8220;fixer-uppers&#8221; of the cloud world. They require fault-tolerant, batch-oriented workloads because they can be yanked out from under you at a moment&#8217;s notice.</p><p>This creates a fundamental trade-off between <em>elasticity</em> and <em>cost</em>. Serverless/managed inference gives you perfect scaling and a warm fuzzy feeling, but it commands a massive premium at high volume. On the flip side, reserved instances (1&#8211;3 year commitments) can cut costs by 40&#8211;70%, but they demand predictable, steady-state traffic to avoid the sin of paying for idle compute [4].</p><p>My advice? Don&#8217;t marry one strategy. Treat your infrastructure like a stock portfolio. Use on-demand or serverless for the unpredictable spikes (the &#8220;YOLO&#8221; investments), and use reserved or spot for the reliable baseline (the index funds). The goal is to never have a GPU sitting at 5% utilization. Idle compute isn&#8217;t just wasted money; it&#8217;s a personal insult to your CFO.</p><h3><em><strong>The Magic Trifecta: Distillation, Quantization, and the Art of Dynamic Batching</strong></em></h3><p>Okay, you&#8217;ve sorted your cloud strategy. Now, let&#8217;s get to the real meat&#8212;the software. This is where the magic happens, and it&#8217;s the primary cost lever you have at your disposal [1]. If you can reduce the model&#8217;s footprint, you reduce the energy needed to move that data. It&#8217;s physics.</p><p>First, let&#8217;s talk about <strong>Quantization</strong>. Most models are born in FP32 (32-bit floating point). That&#8217;s like writing a novel where every word is in 48-point bold font&#8212;it takes up a ridiculous amount of space. By quantizing to INT8 (8-bit integer), you shrink the model footprint by 75%. This directly attacks the memory bandwidth bottleneck, which, as the research notes, is the true system bottleneck&#8212;not raw FLOPS. Less memory to shuffle means less latency and less energy per token.</p><p>Let&#8217;s look at a quick example. Using PyTorch, we can apply dynamic quantization to a model with just a few lines of code, which is perfect for CPU-based inference or reducing initial GPU memory pressure:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5edf1f53-db78-4f8d-97f7-14f01f9d4c6a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer

# Load a standard model
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# Apply dynamic quantization (INT8)
quantized_model = torch.quantization.quantize_dynamic(
    model,  # the original model
    {torch.nn.Linear},  # a set of layers to dynamically quantize
    dtype=torch.qint8  # the target dtype
)

# Save the quantized model
quantized_model.save_pretrained("./quantified_model")
print("Original size: ", model.num_parameters() * 4, "bytes (FP32)")
print("Quantized size: ", sum(p.numel() for p in quantized_model.parameters()), "params (INT8)")</code></pre></div><p><em>Note: The size calculation above is simplified, but the principle holds&#8212;you&#8217;ve just cut the memory footprint significantly, often with a negligible drop in accuracy.</em></p><p>Second, we have <strong>Distillation</strong>. Instead of deploying a 175B parameter behemoth to answer simple queries, you train a smaller &#8220;student&#8221; model to mimic the &#8220;teacher&#8217;s&#8221; behavior. This is the secret sauce behind many of the &#8220;small&#8221; models that punch way above their weight class. Distillation doesn&#8217;t just make inference faster; it makes it cheaper on <em>every</em> level&#8212;less memory, less compute, less energy.</p><p>Third, and this is where the backend engineering gets sexy, is <strong>Dynamic Batching</strong>. GPUs are terrible at doing one thing at a time. They are the &#8220;party buses&#8221; of the compute world&#8212;they want to be full. By dynamically grouping incoming requests into batches that are executed concurrently, you amortize the cost of the model load and maximize the utilization of the hardware. This is a pure throughput play, and it&#8217;s often the difference between a service that scales and a service that bleeds money. The compounding effect of combining distillation (smaller model) + quantization (smaller footprint) + dynamic batching (fuller GPUs) is what pushes cost reductions beyond the 40% threshold.</p><h3><em><strong>The Pragmatist&#8217;s Toolbox: TensorRT, ONNX Runtime, and the Mighty vLLM</strong></em></h3><p>Theory is great, but you need tools to hammer this into production. You shouldn&#8217;t be writing custom CUDA kernels unless you enjoy pain. Instead, lean on the giants.</p><p>For those using NVIDIA hardware, <strong>TensorRT</strong> is your best friend. It performs layer fusion (combining multiple layers into a single kernel) and graph optimizations that squeeze every last drop of performance out of the silicon. It supports INT8/FP16 precision natively and is the gold standard for high-performance serving on that stack.</p><p>If you want portability (because who wants to be locked into a single vendor?), <strong>ONNX Runtime</strong> is the Swiss Army knife. It provides graph optimizations and a pluggable execution provider, meaning you can write once and deploy on CPU, GPU, or even custom silicon with the same codebase. It&#8217;s the &#8220;write once, run anywhere&#8221; of the inference world, minus the Java jokes.</p><p>And then, for the Large Language Model (LLM) crowd, we have <strong>vLLM</strong>. This is the game-changer. vLLM introduced <strong>PagedAttention</strong>, a memory management technique that borrows the concept of virtual memory from operating systems to manage the KV cache (the memory that stores the context of a conversation). Instead of wasting precious HBM on fragmented memory blocks, PagedAttention efficiently swaps and stores key-value pairs. This dramatically increases throughput and reduces memory waste, directly lowering per-token energy and latency.</p><p>These are the practical deployment layers that translate algorithmic optimizations into real backend savings. If you&#8217;re hand-rolling a serving solution for a transformer model and you&#8217;re <em>not</em> using vLLM or TensorRT, you are doing it wrong. Full stop.</p><h3><em><strong>The Grand Synthesis: The Multiplicative Math of Modern Inference</strong></em></h3><p>So, what&#8217;s the final takeaway? The research presents a beautiful, terrifying equation that governs your entire cost structure: <strong>Total Inference Cost = Hardware Efficiency &#215; Utilization Rate &#215; Software Efficiency</strong>.</p><p>This is the most important concept to internalize. If you buy the cheapest spot instances (good hardware efficiency) but run an unoptimized FP32 model with terrible batching (bad software efficiency), you&#8217;re just trading one problem for another. Conversely, you could have the most optimized TensorRT INT8 model in the world, but if you leave it running on an idle, on-demand A100 that does one request a minute, you&#8217;re burning cash faster than a startup in a Series B round.</p><p>The evidence from industry leaders like OpenAI, Uber, and Groq shows a consistent pattern: they don&#8217;t rely on a single silver bullet. They combine distillation to shrink the problem, quantization to compress it, and dynamic batching to maximize throughput. They treat the entire stack&#8212;from the silicon choice to the serving framework&#8212;as a single, coherent economic system.</p><p>As backend developers, our job is no longer just to ensure uptime and low latency. It&#8217;s to ensure <em>economic viability</em>. The next time you deploy a model, don&#8217;t ask &#8220;Is it fast?&#8221; Ask &#8220;Is it <em>cheap</em> to run at scale?&#8221; Ask &#8220;What is my cost-per-1000-tokens?&#8221; The answer will dictate whether your feature survives the next budget meeting.</p><p>So go forth, optimize your stacks, and remember: a penny saved on a token is a penny earned in the cloud.</p><div><hr></div><p><em>That&#8217;s the backend reality check for today. If you found this dive into the economics of our trade useful, don&#8217;t be a stranger. Come back tomorrow, and we&#8217;ll peel back the lid on another layer of the infrastructure onion. Until then, keep your caches warm and your p99s low.</em></p>]]></content:encoded></item><item><title><![CDATA[Backend-for-Frontend Pattern Evolution in 2026: Aggregate Services, Caching, and Type Safety]]></title><description><![CDATA[If you&#8217;ve been in this industry long enough, you remember the days when &#8220;Backend for Frontend&#8221; meant slapping a thin Node.js proxy in front of your monolith and calling it microservices.]]></description><link>https://thebackenddevelopers.substack.com/p/backend-for-frontend-pattern-evolution</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/backend-for-frontend-pattern-evolution</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Thu, 13 Aug 2026 18:01:19 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/210527730/48f8905b64dc44889273342b60eafe00.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>If you&#8217;ve been in this industry long enough, you remember the days when &#8220;Backend for Frontend&#8221; meant slapping a thin Node.js proxy in front of your monolith and calling it microservices. We all did it. We&#8217;ll all deny it in public, but between us? We absolutely did.</p><p>Fast forward to 2026, and the BFF has done what every decent engineering pattern eventually does: it grew up, got therapy, and learned to set boundaries. It is no longer a generic gateway with identity issues. It is now an experience-specific aggregate service, owned by the people who actually care about the pixels, and built with the kind of type-safe, cache-aware discipline that makes backend engineers nod approvingly while pretending they thought of it first .</p><p>So grab your beverage of choice, make peace with the fact that your mobile app and web app should not share the same API contract, and let&#8217;s talk about where the BFF pattern actually stands in 2026.</p><p><em><strong>What the BFF Pattern Means in 2026</strong></em></p><p>The Backend-for-Frontend pattern, at its core, introduces a dedicated backend service for each frontend experience. Instead of a client application directly calling multiple domain microservices, or routing through a single shared gateway, the frontend calls a backend that is purpose-built for its specific surface. That surface might be iOS, Android, React web, React Native, admin dashboard, kiosk, smartwatch, or an edge worker. Each surface gets its own backend seam.</p><p>The BFF&#8217;s job is not to implement business rules. Its job is orchestration, aggregation, translation, and shaping. It accepts a request from the client, figures out which downstream services need to participate, calls them in parallel where possible, reshapes the responses into a payload that matches the client&#8217;s exact needs, and returns a typed, cache-aware, auth-scoped result. The frontend stays thin. The domain services stay pure. The seam sits cleanly in between.</p><p>In 2026, this pattern has matured from a thin API translation layer into a client- or surface-tailored service model. BFFs are deliberately shaped around the needs of each frontend channel, handling authentication, payload structure, and call orchestration rather than simply forwarding traffic . The modern BFF is not a gateway of last resort. It is an intentional, bounded architectural component that trades some operational complexity for the ability of frontend teams to evolve independently from the backend domain model .</p><p>There are three design levers that matter most: ownership, bounded responsibility, and type-safe contracts. Get those right, and the BFF becomes one of the cleanest seams in your architecture. Get them wrong, and you end up with a bloated macro-service that secretly contains half your domain logic and a postmortem waiting to happen.</p><p><em><strong>The Per-Experience Aggregate: Why One API Does Not Fit All</strong></em></p><p>Let&#8217;s address the elephant in the room. For years, we pretended that one API could serve a web dashboard, a mobile app, and an embedded admin portal with equal grace. We were adorable.</p><p>A mobile product page needs high density, minimal round trips, and aggressive caching because networks are still networks. A web product page needs richer metadata, more progressive disclosure, and SEO-friendly payloads. An admin tool needs bulk operations, filters, and export-friendly shapes. These are not cosmetic differences. They are structural differences in payload shape, auth scope, freshness tolerance, and call granularity.</p><p>This is exactly why the 2026 BFF is an experience-specific aggregate service. Rather than a generic gateway, you build distinct BFFs for mobile, web, admin, edge, or any other surface that has its own interaction model . Each BFF knows its client. Each BFF speaks the right contract. Each BFF owns its own caching strategy and freshness guarantees.</p><p>The aggregate part is the magic. Instead of the mobile app making seven separate calls to seven separate microservices and stitching them together on a device with questionable signal, the mobile BFF makes those seven calls server-side, in parallel, and returns one shaped payload. The client gets one request, one response, one loading state, and one place to blame when something breaks.</p><p>But here is where the discipline shows up. A BFF aggregates. It does not compute. It does not own domain rules. It does not start storing state that belongs in the order service, the inventory service, or the user service. The moment your BFF starts calculating tax logic or deciding refund eligibility, it has drifted. Boundary drift is the primary anti-pattern that kills BFFs . Keep the BFF an orchestration and translation tier. If you find yourself writing business logic, pause, apologize to the nearest domain service, and move it downstream.</p><p><em><strong>Ownership: Let the Frontend Team Drive</strong></em></p><p>One of the most consequential shifts in the BFF pattern over the past few years is ownership. In 2026, BFFs are most commonly owned by the frontend or client team . This is not accidental. The BFF is the natural extension of the frontend. It shares the same release cadence, the same product owner, the same user story, and often the same repository.</p><p>When the frontend team owns the BFF, the feedback loop tightens. A designer asks for a layout change, the frontend engineer updates the query shape, the BFF adjusts its aggregation, and the feature ships without waiting for a backend team to prioritize a generic API change. This independence is the whole point. The frontend can iterate without negotiating a contract change across every domain service in the company.</p><p>That said, ownership comes with guardrails. Frontend engineers are perfectly capable of writing backend code, especially in 2026, but the team must still respect the boundary. The BFF should not absorb domain logic just because it is convenient. It should not become a place where frontend teams stash state because they do not want to talk to the platform team. It is a contract-driven seam, not a dumping ground. The discipline of keeping the BFF bounded is what separates a healthy architecture from a distributed monolith that happens to use Kubernetes .</p><p>If you are a backend platform team reading this and feeling slightly territorial, breathe. Your domain services still own the truth. The BFF merely curates the view. Think of it as a highly opinionated museum exhibit built out of artifacts you already created.</p><p><em><strong>Type Safety: Schema-Driven Contracts Are Non-Negotiable</strong></em></p><p>If there is one lesson from the 2026 BFF landscape, it is that type safety cannot be an afterthought. When a frontend depends on a backend contract, every uncaught rename, every silently removed field, every misunderstood nullability is a production bug waiting for the worst possible moment to appear.</p><p>The answer is a single source of truth for the contract, enforced at the boundary and propagated through code generation.</p><p>For TypeScript-native stacks, the dominant combination is tRPC with Zod. tRPC v11 and the emerging v12 provide end-to-end type safety between client and server without requiring a separate schema generation step. Zod defines the schemas, validates runtime inputs, and produces TypeScript types that flow through the router to the frontend automatically. Change the Zod schema, and the TypeScript compiler tells the whole story before anyone deploys .</p><p>For polyglot or REST-heavy environments, OpenAPI with Pydantic v2 is the practical standard. You define your request and response models in Pydantic, FastAPI generates an OpenAPI schema from those models, and tools like Orval generate typed TypeScript clients for the frontend. The schema is the contract. The models are the contract. The generated client types are the contract. Nothing moves without all three agreeing .</p><p>In organizations already invested in GraphQL, Federation v2 serves a similar role. Federated BFFs can compose subgraphs from multiple domain services into a single schema while preserving type safety and clear ownership boundaries. The GraphQL layer becomes the typed seam between frontend concerns and domain microservices .</p><p>Regardless of which stack you choose, the principle is the same. A BFF without a schema-driven contract is just an HTTP-shaped handshake agreement. Handshake agreements fail at scale. Invest in the single source of truth, add runtime validators, generate client types, and sleep better.</p><p><em><strong>Caching: A Layered Discipline, Not a Configuration Checkbox</strong></em></p><p>Caching in a BFF is where the pattern gets genuinely interesting, because the correct answer is always &#8220;it depends,&#8221; and the incorrect answer is almost always &#8220;let&#8217;s cache everything for five minutes and call it a day.&#8221;</p><p>In 2026, effective BFF stacks treat caching as a multi-layer, per-frontend concern. There is no universal policy. A mobile feed can tolerate slightly stale data. A checkout summary cannot. A public product catalog can be cached aggressively at the edge. A personalized recommendations block cannot be cached at all without considering the user identity. The BFF is the perfect place to make these distinctions because it knows both the caller and the downstream context .</p><p>The typical layers look like this.</p><p>Browser and client-side caches sit closest to the user. They handle static assets, previously fetched responses, and optimistic UI state. A well-designed BFF sets precise Cache-Control headers, ETags, and stale-while-revalidate directives so the browser knows exactly what it can reuse and when it must come back .</p><p>CDN and edge caches sit in front of the BFF or at the edge runtime layer. These are ideal for public, non-personalized payloads. The key is separating namespaces by client surface and by data sensitivity. A cache key for the mobile product page should not accidentally collide with the web product page, and nothing personalized should ever leak across user sessions. Namespacing and cache key discipline are essential .</p><p>Application and Redis caches sit behind the BFF, used for downstream data that is expensive to fetch but safe to reuse. Product catalog metadata, reference data, configuration, and feature flags are classic candidates. Again, TTLs and namespaces should be tuned per surface. The mobile BFF and the admin BFF may share the same Redis cluster, but they should not share the same cache key prefix or the same freshness assumptions.</p><p>Advanced patterns include request coalescing, where multiple simultaneous requests for the same cache-miss key are collapsed into a single downstream call, protecting the domain services from thundering herds. Reactive invalidation allows the BFF to evict or warm cache entries when downstream events occur, rather than waiting passively for TTL expiration. Stale-while-revalidate keeps latency low by serving a slightly stale cached response while asynchronously refreshing the value in the background .</p><p>The discipline here is personalization-aware caching. If a response contains user-specific data, the cache key must include the user identity or scope. If a response contains mixed public and private data, consider splitting the aggregation so that public parts can be cached broadly while private parts are fetched fresh per request. A BFF that serves stale personalized data is a BFF that generates support tickets.</p><p><em><strong>Async Aggregation, Typed Responses, and the Edge</strong></em></p><p>Modern BFF implementations lean heavily on asynchronous aggregation. When a client request maps to multiple independent downstream calls, the BFF should execute those calls concurrently rather than sequentially. This is the entire performance argument for having a BFF in the first place.</p><p>In Python-based BFFs, FastAPI with Pydantic v2 and <code>asyncio.gather</code> is now a common reference implementation. The endpoint defines a typed response model, calls the inventory service, pricing service, and reviews service in parallel, validates and reshapes the results, and returns a single shaped payload. The model contract is enforced by Pydantic, the concurrency is handled by asyncio, and the orchestration stays thin .</p><p>Deployment patterns are also shifting. Increasingly, BFFs are being deployed to edge runtimes such as Cloudflare Workers, Vercel Edge, or similar platforms. The goal is to reduce latency by placing the aggregate service geographically close to the user while retaining the typed contract and caching discipline . Edge BFFs are especially compelling for global applications where every millisecond of network round trip matters. They are not suitable for every workload, particularly anything stateful or compute-heavy, but for thin aggregation and personalization they are becoming a first-class option.</p><p>Typed responses and edge runtimes together reinforce the BFF as a low-latency, contract-driven seam. The frontend receives a precisely shaped payload from a nearby compute unit, generated by a backend whose only job is to know that frontend.</p><p><em><strong>A Python Example: FastAPI BFF with Async Aggregation</strong></em></p><p>Let&#8217;s make this concrete. Imagine a product detail page on a mobile app. The mobile BFF needs to fetch product information from the catalog service, current pricing from the pricing service, and user-specific reviews from the reviews service. It should call them in parallel, return one shaped payload, and cache the result with a TTL that is appropriate for mobile.</p><p>Here is what that might look like with FastAPI and Pydantic v2.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fea67634-e025-407f-8c33-6111189e0551&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import asyncio
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx

app = FastAPI()

# Downstream response models
class CatalogProduct(BaseModel):
    id: str
    name: str
    description: str
    image_url: str

class ProductPrice(BaseModel):
    product_id: str
    currency: str
    amount: float
    discount_label: Optional[str] = None

class ProductReview(BaseModel):
    author: str
    rating: int
    comment: str

# BFF response model
class MobileProductDetail(BaseModel):
    id: str
    title: str
    description: str
    image_url: str
    price: dict
    reviews: list[ProductReview]

async def fetch_catalog(product_id: str) -&gt; CatalogProduct:
    async with httpx.AsyncClient() as client:
        r = await client.get(f"https://catalog.internal/products/{product_id}")
        r.raise_for_status()
        return CatalogProduct.model_validate(r.json())

async def fetch_price(product_id: str) -&gt; ProductPrice:
    async with httpx.AsyncClient() as client:
        r = await client.get(f"https://pricing.internal/prices/{product_id}")
        r.raise_for_status()
        return ProductPrice.model_validate(r.json())

async def fetch_reviews(product_id: str) -&gt; list[ProductReview]:
    async with httpx.AsyncClient() as client:
        r = await client.get(f"https://reviews.internal/products/{product_id}/reviews")
        r.raise_for_status()
        return [ProductReview.model_validate(item) for item in r.json()]

@app.get("/mobile/products/{product_id}", response_model=MobileProductDetail)
async def mobile_product_detail(product_id: str):
    try:
        catalog, price, reviews = await asyncio.gather(
            fetch_catalog(product_id),
            fetch_price(product_id),
            fetch_reviews(product_id),
        )
    except httpx.HTTPError as exc:
        raise HTTPException(status_code=502, detail="Downstream service unavailable") from exc

    return MobileProductDetail(
        id=catalog.id,
        title=catalog.name,
        description=catalog.description,
        image_url=catalog.image_url,
        price={
            "amount": price.amount,
            "currency": price.currency,
            "discount_label": price.discount_label,
        },
        reviews=reviews,
    )</code></pre></div><p>This is intentionally simple. The response model is strict. The downstream calls are concurrent. The orchestration is visible. The domain logic lives elsewhere.</p><p>To add caching, you might wrap the downstream fetches or the final aggregation in a Redis layer with a mobile-specific namespace and TTL. To add type-safe client generation, you let FastAPI expose the OpenAPI schema and feed it into Orval. To add edge deployment, you package a subset of this logic into a Cloudflare Worker using the same response model and validation logic, possibly sharing the Zod or Pydantic contracts through generated code.</p><p><em><strong>Libraries and Services Shaping the 2026 BFF Landscape</strong></em></p><p>The BFF ecosystem in 2026 is rich enough that you can build almost any flavor of seam you prefer, as long as you commit to the same principles.</p><p>For TypeScript-native end-to-end type safety, <strong>tRPC</strong> with <strong>Zod</strong> remains the dominant choice. The router, validators, and generated client types all flow from one schema, which makes it ideal when the frontend and BFF are written in TypeScript and owned by the same team .</p><p>For REST-first polyglot stacks, <strong>FastAPI</strong> with <strong>Pydantic v2</strong> and <strong>OpenAPI</strong> is the workhorse. It gives you typed models, automatic schema generation, and a massive ecosystem of client generators. <strong>Orval</strong> is the go-to tool for turning that OpenAPI schema into typed TypeScript fetch clients, hooks, or TanStack Query integrations .</p><p>For GraphQL-based federated architectures, <strong>GraphQL Federation v2</strong> lets you compose domain subgraphs into a unified schema. This is especially powerful when the domain services already expose GraphQL and the BFF layer becomes a federated gateway with frontend-aware query shaping .</p><p>For data-layer acceleration, <strong>Hasura</strong> and <strong>Tailcall</strong> sit at interesting points in the stack. Hasura auto-generates a GraphQL layer over databases and existing APIs, which can act as a BFF-like aggregation tier. Tailcall focuses on declarative API composition and caching at the edge, aiming to reduce the boilerplate of writing custom BFF code by hand .</p><p>For deployment, <strong>Next.js API routes</strong>, <strong>Cloudflare Workers</strong>, and <strong>Vercel Edge</strong> are common homes for BFFs that need to live close to the user. Next.js in particular benefits from the React Server Components model, where the boundary between frontend and BFF is increasingly blurred in a controlled, type-safe way .</p><p>For caching, <strong>Redis</strong> remains the application cache of choice, while CDN providers and edge platforms handle the outer layers. The trick is not the tool but the discipline: namespace per surface, TTL per freshness requirement, and personalized keys that never leak.</p><p><em><strong>Closing Thoughts</strong></em></p><p>The BFF in 2026 is no longer a confession we make during architecture reviews. It is a deliberate, bounded, type-safe seam between the experience you are building and the domain services that power it. It lets frontend teams move fast without breaking backend contracts. It lets backend teams keep domain logic clean without being pulled into every UI experiment. And when you combine schema-driven contracts, async aggregation, and per-frontend caching, it becomes one of the most powerful patterns for building modern, multi-surface products.</p><p>Just remember the rules. Own it by the frontend team. Keep it thin. Cache carefully. And never, under any circumstances, let tax calculation sneak into your BFF.</p><p>I&#8217;ll be back tomorrow with another deep dive into the joyful chaos of backend engineering. Until then, may your caches hit, your contracts compile, and your downstream services stay up.</p><p>Stay sharp, stay typed, and keep building from the backend.</p><h3><strong>References &amp; Citations</strong></h3><ul><li><p><strong><a href="https://appscale.blog/en/blog/microservices-pattern-backend-for-frontend-bff-graphql-trpc-edge-2026">BFF Pattern in Production: GraphQL, tRPC, Edge (2026)</a></strong></p></li><li><p><strong><a href="https://www.hirenodejs.com/blog/nodejs-backend-for-frontend-bff-2026">Node.js BFF Pattern: Production Guide for 2026 | HireNodeJS</a></strong></p></li><li><p><strong><a href="https://dailydevpost.com/blog/backend-for-frontend-rsc-guide">The BFF Pattern in 2026: React Server Components Changed Everything</a></strong></p></li><li><p><strong><a href="https://imperialis.tech/en/blog/api-composition-bff-backend-for-frontend-2026">API Composition and BFF (Backend for Frontend): Orchestrating</a></strong></p></li><li><p><strong><a href="https://oneuptime.com/blog/post/2026-01-30-backend-for-frontend-pattern/view">How to Implement Backend for Frontend Pattern</a></strong></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Change Data Capture in 2026: Debezium, Kafka, and Event-Driven Data Pipelines]]></title><description><![CDATA[Hello, backend wanderers, database wranglers, and anyone who has ever stared at a cron job at 2:47 AM wondering why the staging warehouse still thinks last Tuesday is &#8220;today.&#8221; Welcome back to The Backend Developers &#8212; the newsletter where we turn distributed systems panic into distributed systems poetry.]]></description><link>https://thebackenddevelopers.substack.com/p/change-data-capture-in-2026-debezium</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/change-data-capture-in-2026-debezium</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Tue, 11 Aug 2026 18:02:37 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/210527417/c51974911f6d407d830e6adb6f82d186.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>Hello, backend wanderers, database wranglers, and anyone who has ever stared at a cron job at 2:47 AM wondering why the staging warehouse still thinks last Tuesday is &#8220;today.&#8221; Welcome back to <em>The Backend Developers</em> &#8212; the newsletter where we turn distributed systems panic into distributed systems poetry. Today, we are talking about <strong>Change Data Capture</strong>, or CDC, which sounds like something the CDC would do if the CDC were actually the <em>Center for Database Contagion</em>. And in a way, it is: we are going to trace how a single sneeze in your production PostgreSQL can ripple, in real time, through Kafka, into your data lake, your search index, your analytics warehouse, and your machine-learning feature store &#8212; without anyone lifting a finger.</p><p>By 2026, CDC is no longer a nice-to-have architectural garnish. It is the invisible plumbing beneath nearly every modern event-driven data pipeline. So grab your favorite beverage, silence the Slack notifications from that one Kubernetes channel, and let us walk through the logs, the schemas, the Python consumers, and the managed services that are shaping how we move data in real time.</p><p><em><strong>The Database Diary: Why Change Data Capture Exists</strong></em></p><p>Let us begin with the obvious pain. Most companies do not have one database. They have the <em>primary</em> relational database, the <em>cache</em>, the <em>search index</em>, the <em>analytics warehouse</em>, the <em>data lake</em>, the <em>feature store</em>, the <em>billing system</em>, and possibly that one MongoDB cluster that nobody admits to owning but everyone depends on. The question is not <em>if</em> data should move between these systems; it is <em>how</em> to move it without driving your on-call engineer to take up pottery.</p><p>For years, the answer was the nightly ETL job. At midnight, a batch process would wake up, ask the database some invasive questions, dump a few hundred gigabytes into a CSV-shaped bucket, and hope nothing failed before morning stand-up. This worked, in the same way that a horse-drawn carriage works for interstate travel: it is charming, it is slow, and it tends to break down when you need it most.</p><p>Polling was the next evolution. Some plucky service would run <code>SELECT * FROM users WHERE updated_at &gt; ?</code> every few seconds. It was better than daily batch loads, but it still battered the source database with repeated queries, missed deletes entirely unless you added soft-delete flags, and often returned the same row multiple times if you were not excruciatingly careful about timestamps and isolation levels. Triggers helped, but triggers are the in-laws of database architecture: they show up uninvited, they slow down every transaction, and they make schema migrations deeply uncomfortable.</p><p>What we really wanted was to read the database&#8217;s own private journal &#8212; the transaction log &#8212; and turn every committed change into an immutable event. That is the essence of Change Data Capture. Log-based CDC is the architectural foundation for reliable event-driven systems because it reads directly from database transaction logs rather than polling or triggers, minimizing source-system impact, preserving exact commit order, and ensuring no row-level changes are missed . It is the difference between asking someone to repeat their entire day every five minutes and simply reading their diary.</p><p><em><strong>Log-Based CDC: Reading the Database&#8217;s Private Journal</strong></em></p><p>Let us get technical for a moment, because this is where the magic hides.</p><p>Every serious relational database keeps a transaction log as part of its durability contract. PostgreSQL has the Write-Ahead Log, or WAL. MySQL has the binlog. SQL Server has the transaction log. MongoDB has the oplog. Oracle has the redo log. These logs are not query logs; they are the ground truth of what changed, in what order, and whether the change was committed. Log-based CDC connectors attach to these logs as pseudo-replicas of the database. They read the stream of changes, parse them, and emit structured events describing each insert, update, delete, and sometimes even schema change.</p><p>This approach carries several architectural advantages. Because the connector is a logical replica, it adds minimal load to the primary database compared to repeated polling queries or trigger-based side effects . Because it reads the log in commit order, it preserves the exact temporal sequence of changes, which matters enormously when you are building event-sourced systems, materialized views, or audit logs . And because every committed row-level change is written to the log, you do not lose deletes, you do not double-count updates, and you do not miss the row that changed while your poller was napping .</p><p>There are trade-offs, of course. Log-based CDC usually requires appropriate database permissions, logical replication slots, and sometimes supplemental logging configuration. You need to monitor replication lag and slot disk usage, because an orphaned replication slot can fill up a disk faster than a junior engineer can say &#8220;it works on my machine.&#8221; You also need to think carefully about snapshotting: when a CDC connector first starts, it must reconcile the existing table contents with the stream of new changes. Debezium handles this with a configurable snapshot phase, often using an <em>initial</em> snapshot or an <em>incremental</em> snapshot that chunks large tables to avoid locking them for hours.</p><p><em><strong>The Holy Trinity: Debezium, Kafka, and Kafka Connect</strong></em></p><p>If log-based CDC is the engine, then Debezium, Kafka, and Kafka Connect are the chassis, the fuel lines, and the dashboard that have become the dominant open-source CDC stack . Debezium runs as a Kafka Connect source connector, captures row-level changes from databases like MySQL, PostgreSQL, and MongoDB, and publishes them to Kafka topics, typically using a one-topic-per-table design . Downstream sink connectors then consume those topics and deliver events to target systems via Kafka Connect&#8217;s fault-tolerant runtime .</p><p>Let us unpack that architecture because it is elegant once you see the shape of it.</p><p><strong>Kafka Connect</strong> is a framework for moving data between Kafka and external systems. It abstracts away the boring but dangerous parts of integration: offset management, restart behavior, partitioning, serialization, and at-least-once delivery guarantees. A <em>source connector</em> pulls data from an external system into Kafka. A <em>sink connector</em> pushes data from Kafka into an external system. Connectors run inside <em>tasks</em>, which are distributed across a Connect cluster. If one worker dies, the tasks rebalance. If a task fails, it restarts from the last committed offset. This is the fault-tolerant runtime that makes production CDC feasible .</p><p><strong>Debezium</strong> is a set of source connectors for Kafka Connect. Each Debezium connector knows how to speak the native replication protocol of a particular database. The PostgreSQL connector creates a logical replication slot, decodes the WAL using a plugin like <code>pgoutput</code>, and converts changes into Kafka events. The MySQL connector reads the binlog. The MongoDB connector reads the oplog. The SQL Server connector uses change tables enabled by CDC features. The Oracle connector can use LogMiner or XStream. Each connector emits events in a structured envelope.</p><p><strong>Kafka</strong> is the distributed log in the middle. It provides durability, partitioning, replay, and decoupling between the source database and the many downstream consumers. Debezium publishes one topic per captured table by default, which keeps event ordering simple and makes it easy for consumers to subscribe only to the tables they care about . A topic named something like <code>dbserver1.inventory.customers</code> will contain every change ever made to the <code>customers</code> table, in order, forever, or at least until your retention policy kicks in.</p><p>A Debezium event for an update looks like this in JSON:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;d20e84f2-3d68-4704-b720-b98b5f8274f4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "before": {
    "id": 1001,
    "first_name": "Sally",
    "last_name": "Thomas",
    "email": "sally.thomas@acme.com"
  },
  "after": {
    "id": 1001,
    "first_name": "Sally",
    "last_name": "Thomas",
    "email": "sally.thomas@newdomain.com"
  },
  "source": {
    "version": "2.7.0.Final",
    "connector": "postgresql",
    "name": "dbserver1",
    "ts_ms": 1715600000000,
    "db": "inventory",
    "schema": "public",
    "table": "customers"
  },
  "op": "u",
  "ts_ms": 1715600001000
}</code></pre></div><p>The <code>before</code> field shows the row before the change. The <code>after</code> field shows the row after. The <code>op</code> field tells you whether the event was a create, update, delete, or a read from the snapshot phase. The <code>source</code> block contains metadata about the originating database, transaction, and commit timestamp. The outer <code>ts_ms</code> is the time Debezium processed the event. This envelope is the contract between your source database and every downstream consumer, and it is rich enough to support audit trails, streaming ETL, cache invalidation, search index updates, and analytical projections.</p><p><em><strong>Schema Registries: The Marriage Counselors of Data Contracts</strong></em></p><p>Now, a quick confession. JSON is fine for examples in newsletters and conference slides, but in production, you are probably not sending raw JSON through your Kafka topics if you care about schema evolution, storage efficiency, or your own sanity. Serialization is commonly handled with Avro paired with a Schema Registry, enabling backward- and forward-compatible schema evolution and ensuring producers and consumers maintain stable integration contracts as data models change .</p><p>Why does this matter so much? Because databases change. Product managers add columns. Engineers rename things and swear they will never do it again. Someone decides that <code>status</code> should be an integer enum instead of a string, and now three downstream services are screaming. Without a schema registry, these changes become a game of Kafka consumer roulette: will the consumer parse the event correctly today? Will it parse it correctly tomorrow? Will it silently drop half the events because someone added a new optional field?</p><p>A Schema Registry, such as Confluent Schema Registry or AWS Glue Schema Registry, stores the canonical schema for each topic and assigns it a versioned ID. Producers register new schemas. The registry checks compatibility rules before accepting them. Consumers can retrieve the correct schema by ID from the registry. Avro, with its compact binary encoding and strong schema support, is the common pairing because it keeps Kafka payloads small and fast while allowing careful, controlled schema evolution.</p><p>In practice, you configure your Debezium connector to serialize keys and values using the Avro converter, pointing at your Schema Registry URL. Your consumers then use an Avro deserializer that looks up schemas by ID. When a downstream team adds a new optional field, the registry classifies the change as backward compatible, and nobody has to redeploy anything. When someone wants to make a breaking change, the registry throws a flag, forcing a deliberate conversation rather than a 3 AM outage.</p><p>This is why schema registries and contract-driven schema evolution are critical for production-grade pipelines . They turn schema changes from tribal knowledge into explicit, enforceable contracts. They are the marriage counselors of distributed data: they do not prevent arguments, but they make sure the arguments happen in daylight with witnesses.</p><p><em><strong>Python in the Stream: Consuming Debezium Events</strong></em></p><p>Let us put some code on the table. You might be running Debezium and Kafka in a JVM-flavored world, but a huge amount of real CDC consumption happens in Python. Data engineers love Python. Machine-learning pipelines love Python. That one analytics team with the surprisingly powerful Jupyter notebooks loves Python. Downstream pipelines parse Debezium event envelopes using Python clients like <code>confluent-kafka-python</code> or <code>kafka-python</code>, working with fields such as <code>before</code>, <code>after</code>, <code>op</code>, <code>source</code>, and <code>ts_ms</code> .</p><p>Here is a small, production-adjacent example using <code>confluent-kafka-python</code>. Imagine we want to watch the <code>orders</code> table, filter for completed orders, and update a downstream recommendation engine or cache.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;051cd6f0-2267-4424-827f-aa76a9cd678f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import json
from confluent_kafka import Consumer, KafkaError

def create_consumer():
    return Consumer({
        'bootstrap.servers': 'kafka:9092',
        'group.id': 'orders-processor',
        'auto.offset.reset': 'earliest',
        'enable.auto.commit': 'true',
        'auto.commit.interval.ms': 5000,
    })

def handle_debezium_event(msg):
    event = json.loads(msg.value().decode('utf-8'))

    op = event.get('op')
    source = event.get('source', {})
    table = source.get('table')

    if table != 'orders':
        return

    # op codes: c = create, u = update, d = delete, r = read from snapshot
    if op in ('c', 'u'):
        after = event.get('after')
        if after and after.get('status') == 'completed':
            order_id = after.get('id')
            customer_id = after.get('customer_id')
            total = after.get('total')
            ts_ms = event.get('ts_ms')

            print(f"Completed order {order_id} by customer {customer_id} "
                  f"for ${total} at ts={ts_ms}")

            # TODO: push to recommendation engine, cache invalidator,
            #       fraud detector, or analytical projection

    elif op == 'd':
        before = event.get('before')
        order_id = before.get('id') if before else None
        print(f"Order {order_id} was deleted; cleaning downstream state")

def main():
    consumer = create_consumer()
    consumer.subscribe(['dbserver1.inventory.orders'])

    try:
        while True:
            msg = consumer.poll(timeout=1.0)
            if msg is None:
                continue
            if msg.error():
                if msg.error().code() == KafkaError._PARTITION_EOF:
                    continue
                raise Exception(msg.error())
            handle_debezium_event(msg)
    except KeyboardInterrupt:
        pass
    finally:
        consumer.close()

if __name__ == '__main__':
    main()</code></pre></div><p>This script is intentionally small, but it illustrates the core pattern. It subscribes to the topic for the <code>orders</code> table, decodes the Debezium envelope, inspects the <code>op</code> field to decide whether the event is a create, update, or delete, and acts only on the <code>after</code> state when the order is completed. If the <code>before</code> state exists but the <code>after</code> state does not, that is a delete, and you can clean up downstream projections accordingly.</p><p>For production, you would likely use Avro with Schema Registry instead of raw JSON, and the deserialization would change to use <code>AvroConsumer</code> from <code>confluent-kafka-python</code>. You would also handle retries, dead-letter queues, idempotency keys, and graceful shutdown. But the envelope structure &#8212; <code>before</code>, <code>after</code>, <code>op</code>, <code>source</code>, <code>ts_ms</code> &#8212; remains the same, and that consistency is what makes Debezium so pleasant to build against .</p><p><em><strong>Stream Processors, Outbox, and Exactly-Once Semantics</strong></em></p><p>Python consumers are excellent for many tasks, but some transformations are too heavy or too stateful for simple per-record scripts. This is where stream processors such as Flink and ksqlDB come in, supporting complex transformations, aggregations, and outbox-pattern routing . They broaden CDC from a simple replication mechanism into a full event-processing backbone.</p><p><strong>ksqlDB</strong> lets you write SQL-like continuous queries over Kafka topics. You can join a stream of orders with a table of customers, filter for high-value transactions, compute windowed aggregations, and publish the results to a new topic. It is accessible to analysts and engineers who already think in SQL, and it runs natively on Kafka.</p><p><strong>Apache Flink</strong> is the heavyweight cousin. It supports event-time processing, exactly-once stateful computations, complex event processing, and rich window semantics. If you need to detect fraud patterns across a stream of Debezium events, or build sessionized analytics, or maintain an eventually consistent materialized view with sophisticated joins, Flink is where you land.</p><p><strong>The outbox pattern</strong> deserves special mention because it solves one of the nastiest distributed systems problems: dual writes. Imagine a service that writes to a database and also publishes an event to Kafka. If the database commit succeeds but the Kafka publish fails, or vice versa, your system is now inconsistent. The outbox pattern fixes this by having the service write events to a dedicated <code>outbox</code> table in the same database transaction as its business update. Debezium captures the outbox table and routes the events to Kafka. Because both writes happen in one atomic transaction, you eliminate the dual-write problem. Debezium can even be configured with the Outbox Event Router single message transformation to strip out the envelope and publish clean domain events to topic names derived from the outbox row.</p><p>Looking forward, exactly-once delivery semantics and cloud-native or serverless CDC are defining forward-looking trends . By 2026, stronger delivery guarantees, tighter schema registry integration, and managed or serverless CDC options are increasingly central, reducing operational complexity while improving reliability in event-driven architectures . Exactly-once semantics, while never truly free in distributed systems, are becoming more practical through idempotent producers, transactional consumers, and Kafka&#8217;s own exactly-once processing support. For many use cases, at-least-once delivery with idempotent consumers remains the pragmatic default, but the availability of exactly-once pipelines is growing, especially in managed services.</p><p><em><strong>The Bigger Family: Who Else Is at the CDC Reunion?</strong></em></p><p>Debezium is not the only name in town. The CDC ecosystem spans open-source tools and managed services with distinct trade-offs . Knowing the alternatives helps you choose the right tool for your source database, your operational model, and your tolerance for YAML.</p><p>On the open-source side, you have options like <strong>Maxwell&#8217;s Daemon</strong>, which reads the MySQL binlog and publishes JSON events to Kafka, RabbitMQ, or other sinks; <strong>Alibaba Canal</strong>, which focuses on MySQL binlog capture and is popular in the Chinese tech ecosystem; <strong>Apache Flink CDC</strong>, which integrates CDC directly into Flink&#8217;s streaming SQL and DataStream APIs for unified batch and stream processing; and <strong>StreamSets</strong>, which provides a visual data pipeline designer with CDC connectors among many other integration patterns.</p><p>On the managed side, the landscape is even broader. <strong>Fivetran</strong> offers managed connectors with CDC support for many databases, emphasizing low maintenance and rapid setup. <strong>Striim</strong> specializes in real-time data integration and change data capture with strong monitoring and transformation capabilities. <strong>AWS Database Migration Service</strong> provides CDC replication to AWS targets. <strong>Azure Data Factory</strong> offers CDC features for Azure-centric data pipelines. <strong>Google Datastream</strong> provides serverless change data capture into Google Cloud. <strong>Qlik Replicate</strong> is an enterprise-grade replication tool with broad source and target support. <strong>Oracle GoldenGate</strong> is the long-standing heavyweight for Oracle environments, though it works across many databases. <strong>Airbyte</strong> and <strong>Estuary</strong> represent newer managed or managed-open-source approaches, with Airbyte focusing on composable connectors and Estuary emphasizing streaming materializations.</p><p>The choice typically balances control and cost against operational ease, vendor lock-in, source database support, and whether the architecture is Kafka-centric or direct-to-destination replication . If your entire event architecture is built around Kafka, Debezium is usually the natural fit. If you want someone else to operate the connectors, monitor lag, and guarantee SLAs, a managed service may be worth the cost. If you are deeply embedded in Oracle or Azure, the native or partnered solutions may have integration advantages that outweigh a pure open-source choice.</p><p><em><strong>The 2026 Crystal Ball: Where CDC Is Heading</strong></em></p><p>So where does this leave us as we move through 2026? A few predictions, grounded in the trends already visible.</p><p>First, log-based CDC will become even more of a default assumption. The days of polling for replication in greenfield systems are numbered. If you are building an event-driven architecture and your change capture mechanism is a cron job running <code>SELECT *</code>, someone in your future architecture review is going to ask pointed questions.</p><p>Second, schema registries will move from advanced practice to baseline expectation. As more teams share Kafka topics and more pipelines depend on stable event shapes, the ability to evolve schemas safely will be table stakes. Teams that skip the registry will find themselves paying the price in coordination overhead and brittle consumers.</p><p>Third, Python-based consumption and lightweight stream processing will keep expanding the CDC audience. You do not need to be a Kafka wizard to consume Debezium events anymore. You need to understand the envelope, handle idempotency, and respect offset management. The tooling is mature enough that data engineers and backend developers can build reliable consumers without becoming distributed systems specialists.</p><p>Fourth, managed and serverless CDC will reduce operational complexity for teams that do not want to operate a Kafka Connect cluster, ZooKeeper or KRaft, and a fleet of connectors. These services abstract away the replication slot monitoring, snapshot orchestration, and failover logic. The trade-off is always less control and potentially higher cost, but for many teams, that trade-off is correct.</p><p>Finally, exactly-once and stronger delivery semantics will continue to mature. They will not replace good idempotent consumer design, but they will make certain classes of pipelines &#8212; financial transactions, inventory systems, compliance audit trails &#8212; easier to build with confidence.</p><p><em><strong>Closing Stanza</strong></em></p><p>Change Data Capture, in the end, is about respecting the database. Instead of hammering it with questions every few seconds, we listen to its own record of truth. Instead of praying that our nightly ETL finished before the CEO opened the dashboard, we stream changes as they happen. And instead of scattering dual writes across services like confetti, we turn every committed change into an immutable event that the rest of the system can build upon.</p><p>Debezium, Kafka, and Kafka Connect remain the beating heart of the open-source CDC world, and they are surrounded by a growing family of stream processors, schema registries, Python clients, and managed services . Whether you are building a cache invalidation pipeline, a real-time warehouse, an outbox event router, or a streaming machine-learning feature platform, the patterns are the same: read the log, emit the event, honor the schema, consume idempotently, and always &#8212; always &#8212; monitor your replication lag.</p><p>That is all for today, friends. If this made you want to go instrument a PostgreSQL logical replication slot, we have done our job. If it made you want to send your cron jobs to a farm upstate, we understand. Come back tomorrow for more backend wisdom, more distributed systems therapy, and probably at least one joke about YAML. Keep building, keep questioning your polling intervals, and remember: in event-driven architecture, the log is the source of truth &#8212; everything else is just a projection.</p><p>Warmly, <em>The Backend Developers</em></p><p>P.S. &#8212; If you are running Debezium in production, set an alert on replication slot disk usage. We say this with love, and also with the memory of a very long Tuesday.</p>]]></content:encoded></item><item><title><![CDATA[Event-Driven Architecture: Saga Patterns, Outboxes, and Distributed Consistency in 2026]]></title><description><![CDATA[Good morning, builders of the invisible plumbing that keeps the internet from catching fire.]]></description><link>https://thebackenddevelopers.substack.com/p/event-driven-architecture-saga-patterns</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/event-driven-architecture-saga-patterns</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Thu, 06 Aug 2026 17:44:14 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/210107131/44e1f1410baa13a5725f6c53cb828a9e.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>Good morning, builders of the invisible plumbing that keeps the internet from catching fire.</p><p>If you have ever watched two microservices argue over whether an order was actually placed, welcome home. If you have ever lain awake wondering whether your message bus silently dropped an event while your database happily committed a row, pull up a chair. And if you have ever confidently told a product manager, <em>&#8220;Sure, we can make that eventual-consistent,&#8221;</em> while internally praying to the gods of idempotency, then this issue of <strong>The Backend Developers</strong> is written specifically for you.</p><p>Today we are talking about <strong>event-driven architecture in 2026</strong>, and the three horsemen that make it tolerable: <strong>outboxes</strong>, <strong>saga patterns</strong>, and the increasingly nuanced world of <strong>distributed consistency</strong>. The research is clear, the vendors are merging, the Python ecosystem is still a little scrappy, and the real differentiator is no longer which tool you pick but how well you assemble the layers.</p><p>So grab your coffee, close Slack for twenty minutes, and let&#8217;s build something worth reading.</p><div><hr></div><p><em><strong>Why Distributed Consistency Still Feels Like Herding Schr&#246;dinger&#8217;s Cats</strong></em></p><p>Let&#8217;s set the stage. Ten years ago, the world discovered microservices and immediately made two catastrophic assumptions. First, that breaking a monolith into smaller pieces automatically makes you agile. Second, that you could still pretend you had one big happy ACID transaction when, in reality, you had just distributed your database across a hundred services, a Kafka cluster, three clouds, and one engineer&#8217;s laptop.</p><p>The truth is that <strong>most real-world business processes cross service boundaries</strong>. An e-commerce checkout flow touches inventory, payment, shipping, notifications, loyalty points, fraud checks, and tax calculators. None of these services share a database. You cannot wrap the whole thing in <code>BEGIN TRANSACTION; COMMIT;</code>. Distributed transactions through two-phase commit are theoretically beautiful and operationally haunted. They lock resources, slow systems to a crawl, and turn partition tolerance into an existential crisis.</p><p>So we turned to <strong>event-driven architecture</strong>. Services publish facts. Other services react to those facts. The system becomes loosely coupled, horizontally scalable, and theoretically resilient. The catch? Publishing an event and committing a database row are <strong>two separate operations</strong>, and networks are not your friend.</p><p>If you commit the database first and then publish the event, a broker hiccup means the database says <em>&#8220;yes&#8221;</em> while the rest of the system says <em>&#8220;never heard of it.&#8221;</em> If you publish the event first and then commit the database, you get ghost orders haunting Kafka. Either way, someone is angry, and it is probably the finance team.</p><p>That is where the outbox pattern enters. Then, when the business process itself spans multiple services and failures can happen halfway through, the saga pattern enters. And when people start asking whether their system is <em>eventually</em>, <em>causally</em>, or <em>linearizably</em> consistent, you enter the consistency-model negotiation that determines whether your architecture survives contact with real customers.</p><p>Let&#8217;s take each of these seriously, because beneath the buzzwords are genuinely powerful ideas.</p><div><hr></div><p><em><strong>The Outbox Pattern: Your Atomic Bridge Between State and Events</strong></em></p><p>Here is the core problem in plain language. In a microservice, you typically want to do two things when something important happens:</p><ol><li><p>Mutate your own local state in a database.</p></li><li><p>Tell the rest of the world about it through a message broker or event bus.</p></li></ol><p>These two actions must appear atomic to the rest of the system. Either the state change and the event both happen, or neither happens. But unless your database and your broker share a transaction coordinator, which they almost never do, you cannot wrap both in a single transaction.</p><p><strong>The outbox pattern solves this by making the event part of the same database transaction as the state change.</strong></p><p>Instead of directly publishing to the broker inside your service code, you append a row to an <strong>outbox table</strong> within the same local database transaction that updates your domain state. Because the event row lives in the same database as your business data, the database&#8217;s transaction guarantees apply to both. If the transaction commits, the state change and the event row are durably persisted together. If the transaction rolls back, neither survives.</p><p>A separate component, often called the <strong>outbox relay</strong> or <strong>event relay</strong>, then reads committed events from the outbox table and publishes them to the message broker. This can be done by polling the table or, more elegantly in modern systems, by using <strong>change-data capture</strong> via tools like <strong>Debezium</strong> that tail the database&#8217;s write-ahead log. Either way, once an event is successfully acknowledged by the broker, the relay marks it as processed or deletes it.</p><p>This gives you several critical properties.</p><p>First, <strong>atomicity</strong>. Domain state changes and event publication are now one logical unit. There is no window where one happened and the other did not, at least from the perspective of durability.</p><p>Second, <strong>at-least-once delivery</strong>. The relay will keep trying to publish an event until it receives confirmation from the broker. The event is not lost because the database is the source of truth. The broker may receive the event more than once due to retries, duplicates, or network quirks, but the source event itself is preserved.</p><p>Third, <strong>per-aggregate ordering</strong>. Because the outbox table is written inside the service&#8217;s database transaction, events for a given aggregate can be written in the exact order the aggregate changed. The relay can then publish them in that order, often keyed by aggregate ID so that consumers receiving from a partitioned topic see a coherent history for each aggregate. If aggregate <code>A</code> changes twice, consumers will see event <code>A1</code> before event <code>A2</code> as long as the relay respects the ordering.</p><p>Fourth, <strong>a pathway to exactly-once effects</strong>. The outbox pattern alone does not give you end-to-end exactly-once delivery semantics in a strict distributed systems sense. Message brokers typically provide at-least-once delivery. True exactly-once processing requires the consumer to be <strong>idempotent</strong>: processing the same event twice must produce the same result as processing it once. The outbox makes the publisher side safe, and idempotency makes the consumer side safe. Together they produce an exactly-once <em>effect</em>.</p><p>There are practical nuances to consider. The outbox table should ideally be co-located in the same database schema as the domain tables to avoid cross-database transactions. The relay should publish events with a stable, unique identifier so consumers can deduplicate. The table will grow unless events are removed or moved to an archive after successful publication, so operational habits matter. Polling at very high throughput can become inefficient, which is why <strong>Debezium-style CDC</strong> has become popular: it reads the database&#8217;s transaction log directly, minimizing load on the service database and reducing latency.</p><p>The outbox pattern is not glamorous. It is just a table and a loop. But it is the canonical answer to one of the hardest problems in event-driven systems: making sure that when your service says something happened, it actually happened.</p><div><hr></div><p><em><strong>Saga Patterns: Orchestration, Choreography, and the Art of Saying &#8220;Oops&#8221;</strong></em></p><p>Once you have atomic state-and-event publication, you face the next problem: many business processes are long-running and span multiple services. A single logical operation, like placing an order, consists of several local steps, each with its own database and its own events. If step three fails, what do you do about steps one and two?</p><p>You cannot roll them back with a single distributed transaction. You can, however, run <strong>compensating transactions</strong>: operations that semantically undo earlier steps. This collection of steps and compensations is called a <strong>saga</strong>.</p><p>A saga is a sequence of local transactions. Each step updates data in one service and emits a message or event that triggers the next step. If a step fails, the saga executes compensating transactions for the steps that have already completed. The goal is to leave the system in a consistent, well-defined state: either the whole process succeeded, or the saga has fully compensated and left a clear audit trail.</p><p>There are two dominant saga styles, and the choice between them is one of the most important architectural decisions you will make.</p><p><strong>Choreography</strong> means there is no central coordinator. Each service knows which events to listen for and which events to emit next. When the inventory service receives an <code>OrderCreated</code> event, it reserves inventory and emits an <code>InventoryReserved</code> event. The payment service listens for that and charges the customer, emitting a <code>PaymentCharged</code> event. The shipping service listens for that and prepares delivery.</p><p>The benefit is <strong>loose coupling</strong>. Services do not need to know about each other explicitly, only about the events. This can feel natural for simple, event-native domains. The drawback is <strong>implicit coordination</strong>. The business flow is encoded in a web of subscriptions and handlers, making it hard to visualize, debug, and modify. Compensation logic ends up scattered across multiple services, and reasoning about failure paths becomes an archaeological expedition through repositories.</p><p><strong>Orchestration</strong> means there is a central saga coordinator that explicitly invokes each step and handles failures. The coordinator might be a dedicated workflow engine such as <strong>Temporal</strong>, <strong>Orkes Conductor</strong>, or a custom state machine. It knows the sequence, calls services, waits for responses, retries transient failures, and invokes compensating actions when necessary.</p><p>The benefit is <strong>visibility and control</strong>. You can inspect the state of a saga at any moment. You can enforce ordering precisely. You can centralize compensation logic or at least track it. You can retry, pause, and escalate. The drawback is that the orchestrator becomes a coupling point and a potential single point of failure, though modern durable workflow engines are designed to be highly available themselves.</p><p>In 2026, the research consensus is clear: <strong>choreography is elegant for simple flows; orchestration is essential for complex flows; hybrid approaches are increasingly common.</strong> A hybrid design might use choreography for naturally decoupled subdomains and introduce an orchestrator for the parts of the flow where sequencing, visibility, and compensation really matter.</p><p>Compensations deserve special attention. A compensating transaction is <strong>not</strong> a database rollback. It is a business operation that semantically undoes a prior step. If you reserved inventory, the compensation releases it. If you charged a customer, the compensation issues a refund. Compensations can themselves fail, so they must be idempotent and retryable. Some compensations may require human intervention after automated retries are exhausted. Designing good compensations is where architecture meets domain modeling: you must deeply understand what &#8220;undo&#8221; means for each business action.</p><p>The saga invariant is straightforward to state and hard to guarantee: <strong>every saga eventually reaches either a completed state or a fully compensated state.</strong> Achieving this requires idempotency, durable execution, observability, and clear escalation paths. The pattern does not eliminate failure. It makes failure a first-class citizen of your design.</p><div><hr></div><p><em><strong>Picking a Consistency Model Without Starting a Religious War</strong></em></p><p>The word &#8220;consistency&#8221; is dangerously overloaded. In distributed systems it means at least three different things depending on who is talking. Let&#8217;s be precise, because the research is emphatic: <strong>there is no universal correct consistency model. You choose based on the invariants your business actually needs.</strong></p><p><strong>Eventual consistency</strong> means that if no new updates are made, eventually all replicas will converge to the same value. It is the workhorse of distributed systems because it allows high availability and partition tolerance. For many backend operations, eventual consistency is perfectly fine. If a search index lags your primary database by a few seconds, users rarely notice. If two microservices briefly disagree about a loyalty point balance, the world keeps turning.</p><p>However, eventual consistency does not give you any guarantees about ordering. Event A might logically cause event B, but a consumer could see B before A, or see them out of order relative to other aggregates. That matters when business invariants depend on causality.</p><p><strong>Causal consistency</strong> preserves happens-before relationships. If process P reads a value written by Q, then P&#8217;s subsequent writes are seen after Q&#8217;s write by any process that observes P. In event-driven systems, causal consistency often appears through <strong>event sourcing</strong> and <strong>per-aggregate ordering</strong>. By routing all events for a given aggregate through the same Kafka partition, or by using logical clocks and vector clocks in collaborative systems, you can guarantee that consumers observe the causal history of that aggregate correctly. This is especially valuable for collaborative workloads, event-sourced domains, and workflows where steps have clear dependencies.</p><p><strong>Stronger models</strong>, such as <strong>linearizability</strong> or <strong>serializability</strong>, guarantee that operations appear to execute in some total order, instantaneously at a single point in time. These are powerful but expensive. They typically require coordination, locks, consensus protocols, or distributed transaction managers. They reduce availability, increase latency, and complicate operations. They should be reserved for invariants that truly require them, such as preventing double-spending, ensuring inventory does not go negative, or enforcing unique global identifiers.</p><p>The right approach is <strong>invariant-driven consistency</strong>. For each business rule, ask: what would break if this were only eventually consistent? What would break if causality were violated? If the answer is &#8220;nothing important,&#8221; eventual consistency is your friend. If the answer is &#8220;the user experience becomes confusing,&#8221; causal consistency is probably worth the cost. If the answer is &#8220;we lose money or violate regulation,&#8221; then you need a stronger model, and you should isolate that specific invariant rather than forcing the entire architecture into a straitjacket.</p><p>In practice, event-driven systems in 2026 often mix these models. A checkout saga may use causal ordering within an aggregate, eventual consistency for analytics and search, and a strongly consistent reservation or ledger for the actual money movement. The art is not choosing the strongest model everywhere. The art is choosing the weakest model that preserves each invariant.</p><div><hr></div><p><em><strong>Python in 2026: A Pragmatic, If Slightly Duct-Taped, Toolkit</strong></em></p><p>Let&#8217;s address the elephant in the server room. If you are a Python backend engineer, you have probably noticed that the Java and .NET ecosystems have shiny, battle-tested saga and outbox libraries while Python sometimes feels like you are building a spaceship out of pip-installable cardboard.</p><p>The research confirms this directly: <strong>Python tooling for sagas and outboxes is less mature than Java or .NET alternatives.</strong> There is no single dominant saga framework that everyone reaches for. The typical 2026 Python implementation is assembled from a few well-known pieces.</p><p>For asynchronous task execution, <strong>Celery</strong> remains the default choice, often backed by Redis or RabbitMQ. For event streaming, <strong>Kafka</strong> is common, accessed through libraries like <strong>confluent-kafka</strong>, <strong>kafka-python</strong>, or <strong>aiokafka</strong> for async workloads. For change-data capture from the outbox table, <strong>Debezium</strong> is the standard connector, tailing PostgreSQL or MySQL write-ahead logs and pushing changes into Kafka. For the actual service layer, <strong>FastAPI</strong>, <strong>Django</strong>, or <strong>Flask</strong> with <strong>SQLAlchemy</strong> or an async ORM handle the domain logic and outbox writes.</p><p>For orchestration, Python developers are increasingly reaching for the <strong>Temporal Python SDK</strong> or clients for <strong>Orkes Conductor</strong>, though the most mature Temporal documentation and use cases still lean heavily toward Go and Java. Some teams build lightweight saga coordinators in Celery or Dramatiq, storing saga state in PostgreSQL and manually wiring compensations. It is pragmatic, it works, and it requires discipline.</p><p>That pragmatism is not a weakness. It reflects Python&#8217;s strength as a glue language and the reality that many Python backend teams operate polyglot environments: Python for the application, Kafka and Debezium for the event backbone, Temporal or Conductor for durable orchestration. You do not need a single Python library that does everything. You need a coherent design that uses the right tool for each layer.</p><div><hr></div><p><em><strong>Building the Code: A Mini Order Flow with Outbox, Saga, and Idempotency</strong></em></p><p>Let&#8217;s make this concrete. Imagine a simplified e-commerce flow:</p><ol><li><p>Create an order.</p></li><li><p>Reserve inventory.</p></li><li><p>Charge payment.</p></li><li><p>Mark the order as confirmed and notify shipping.</p></li></ol><p>If payment fails, we compensate by releasing the inventory. If inventory reservation fails, there is nothing to compensate yet. This is the kind of long-running process where an outbox gives us atomic domain events and a saga gives us safe failure handling.</p><p>Below is a Python sketch using <strong>SQLAlchemy</strong> for the outbox and domain state, <strong>Celery</strong> for asynchronous saga steps and compensation, and a simple in-memory-style approach to idempotency. This is not production-ready copy-paste code; it is a teaching scaffold. Real production code needs retries, deadlines, observability, and careful error handling, but the structure is what matters.</p><p>First, the domain and outbox models:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;86b417f0-595b-4f23-9435-44e886fd74d6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import uuid
import datetime
from sqlalchemy import create_engine, Column, String, Integer, DateTime, JSON, Boolean
from sqlalchemy.orm import declarative_base, sessionmaker

Base = declarative_base()

class Order(Base):
    __tablename__ = "orders"
    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
    status = Column(String, default="pending")
    inventory_reserved = Column(Boolean, default=False)
    payment_charged = Column(Boolean, default=False)

class OutboxEvent(Base):
    __tablename__ = "outbox"
    id = Column(Integer, primary_key=True, autoincrement=True)
    aggregate_id = Column(String, index=True)
    aggregate_type = Column(String)
    event_type = Column(String)
    payload = Column(JSON)
    created_at = Column(DateTime, default=datetime.datetime.utcnow)
    published = Column(Boolean, default=False)</code></pre></div><p>When the user places an order, we commit both the <code>Order</code> row and an <code>OrderCreated</code> outbox event inside the same transaction:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4fdf6a7b-87f4-4c2b-a558-97ece5e363a8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def create_order(session, customer_id, items, total_cents):
    order = Order(status="pending")
    session.add(order)
    session.flush()  # Generate order.id without committing

    event = OutboxEvent(
        aggregate_id=order.id,
        aggregate_type="order",
        event_type="OrderCreated",
        payload={
            "customer_id": customer_id,
            "items": items,
            "total_cents": total_cents,
        },
    )
    session.add(event)
    session.commit()

    return order.id</code></pre></div><p>This is the heart of the outbox pattern. The <code>Order</code> row and the <code>OutboxEvent</code> row are written atomically. Later, a relay publishes the event to Kafka. If Debezium is in the picture, it tails the WAL and streams the new <code>OutboxEvent</code> row directly, avoiding polling load.</p><p>The saga itself is a Celery workflow that knows the sequence and the compensations:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;7f7c134a-ea2c-4ea3-9391-3cc482a91738&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">from celery import Celery

app = Celery("order_saga", broker="redis://localhost:6379/0")

SAGA_STEPS = [
    ("reserve_inventory", reserve_inventory_task, release_inventory_task),
    ("charge_payment", charge_payment_task, refund_payment_task),
    ("notify_shipping", notify_shipping_task, None),
]</code></pre></div><p>The orchestrator keeps saga state and walks through the steps:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;48c69f9e-372e-46a2-a448-99693f0a2809&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@app.task(bind=True, max_retries=3)
def run_order_saga(self, order_id):
    saga = get_or_create_saga_state(order_id)

    for idx in range(saga.current_step, len(SAGA_STEPS)):
        step_name, action, compensation = SAGA_STEPS[idx]

        try:
            result = action.delay(order_id).get(timeout=10)
            saga.results[step_name] = result
            saga.current_step += 1
            save_saga_state(saga)

        except Exception as exc:
            # Failure: compensate already-completed steps in reverse order.
            for prev_idx in reversed(range(idx)):
                prev_name, _, prev_comp = SAGA_STEPS[prev_idx]
                if prev_comp:
                    prev_comp.delay(
                        order_id, saga.results.get(prev_name)
                    ).get(timeout=30)

            saga.status = "compensated"
            save_saga_state(saga)

            # Retry the whole saga after a backoff; or escalate if fatal.
            raise self.retry(exc=exc, countdown=60)

    saga.status = "completed"
    save_saga_state(saga)
    return saga.status</code></pre></div><p>Each step task is idempotent and safe to retry. Here is what reserving inventory might look like:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2fc15c4e-80b5-4a40-8952-973cee70ca2b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@app.task
def reserve_inventory_task(order_id):
    session = SessionLocal()
    try:
        # Idempotency check: if already reserved, return success.
        order = session.query(Order).filter_by(id=order_id).first()
        if order and order.inventory_reserved:
            return {"reserved": True, "reservation_id": get_existing_reservation(order_id)}

        # Business logic to reserve inventory...
        reservation_id = call_inventory_service(order_id)

        order.inventory_reserved = True
        session.commit()

        return {"reserved": True, "reservation_id": reservation_id}
    finally:
        session.close()</code></pre></div><p>And the compensation for it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1a7087d6-203a-4bf4-8e55-c0226dcc59f9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@app.task
def release_inventory_task(order_id, reservation_result):
    session = SessionLocal()
    try:
        order = session.query(Order).filter_by(id=order_id).first()
        if not order or not order.inventory_reserved:
            return {"released": True}  # Already compensated or never reserved

        call_inventory_service_release(reservation_result["reservation_id"])
        order.inventory_reserved = False
        session.commit()

        return {"released": True}
    finally:
        session.close()</code></pre></div><p>The consumer side, whether it receives the original <code>OrderCreated</code> event from the outbox relay or internal saga commands, must be idempotent. A minimal deduplication guard looks like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3c3839bb-a0c1-49f0-96de-54fde153ae4a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class ProcessedEvent(Base):
    __tablename__ = "processed_events"
    idempotency_key = Column(String, primary_key=True)
    processed_at = Column(DateTime, default=datetime.datetime.utcnow)

def handle_order_created(session, event_payload, idempotency_key):
    if session.query(ProcessedEvent).filter_by(idempotency_key=idempotency_key).first():
        return  # Already processed

    # Do the work.
    create_order_from_event(session, event_payload)

    session.add(ProcessedEvent(idempotency_key=idempotency_key))
    session.commit()</code></pre></div><p>Notice the pattern. The outbox guarantees atomic state plus event. The saga guarantees that failures lead to defined compensations. Idempotency guarantees that retries and duplicates do not corrupt state. These three layers stack on top of each other.</p><p>One more piece: connecting the outbox to Kafka with Debezium. A simplified Debezium connector configuration for PostgreSQL might look like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;709682bb-6b52-417b-9e4b-5912408be30b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "name": "order-outbox-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "postgres",
    "database.port": "5432",
    "database.user": "debezium",
    "database.password": "dbz",
    "database.dbname": "orders",
    "topic.prefix": "orderdb",
    "table.include.list": "public.outbox",
    "tombstones.on.delete": "false",
    "transforms": "outbox",
    "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter"
  }
}</code></pre></div><p>Debezium reads every insert into the <code>outbox</code> table from the PostgreSQL write-ahead log and routes it to a Kafka topic, typically keyed by <code>aggregate_id</code> so that events for the same aggregate land in the same partition and preserve ordering. The application does not publish to Kafka directly; it simply writes to its own database and trusts the CDC pipeline to propagate the event reliably.</p><p>This is the shape of a modern Python event-driven backend in 2026: small, focused services, an outbox in the application database, Debezium feeding Kafka, Celery or a durable workflow engine running sagas, and idempotency keys everywhere.</p><div><hr></div><p><em><strong>The 2026 Tooling Landscape: Who&#8217;s Doing the Heavy Lifting</strong></em></p><p>If you are evaluating technology for this space in 2026, the landscape splits naturally into three layers: the event bus, the durable workflow or saga engine, and the change-data capture glue that connects them.</p><p>For the <strong>event backbone</strong>, <strong>Apache Kafka</strong> remains the dominant choice, especially through managed offerings like <strong>Confluent Cloud</strong>. It gives you durable, ordered, partitioned streams and a massive ecosystem. <strong>AWS EventBridge</strong> is popular in AWS-centric stacks for its serverless event routing and schema registry. <strong>NATS JetStream</strong> and <strong>Apache Pulsar</strong> are strong alternatives if you want lighter-weight or geo-replicated messaging.</p><p>For <strong>change-data capture</strong>, <strong>Debezium</strong> is essentially the default. It supports PostgreSQL, MySQL, SQL Server, MongoDB, and more, and it plugs cleanly into Kafka. Alternatives include database-native logical replication, AWS DMS, and newer database engines that expose change streams directly.</p><p>For <strong>saga orchestration and durable workflows</strong>, <strong>Temporal</strong> has become a powerhouse. It provides durable execution, retries, timers, signals, and compensation support with SDKs in multiple languages. <strong>Orkes Conductor</strong>, built on Netflix Conductor, offers a strong cloud-native orchestration option. <strong>Camunda</strong> remains relevant for BPMN-driven workflows. In the .NET world, <strong>MassTransit</strong> provides an excellent saga and outbox story that many teams envy. <strong>Netflix Conductor</strong> continues to influence the orchestration space.</p><p>For <strong>Python specifically</strong>, your choices are more about assembly than single frameworks. <strong>Celery</strong> and <strong>Dramatiq</strong> handle task queues. <strong>aiokafka</strong> and <strong>confluent-kafka</strong> talk to Kafka. The <strong>Temporal Python SDK</strong> gives you access to durable execution if your team is comfortable with the model. <strong>Orkes Conductor</strong> also exposes Python clients. For outbox relaying, you will likely use <strong>Debezium</strong> or write a small poller.</p><p>The trajectory is toward convergence. Platforms are increasingly bundling streaming, CDC, outbox routing, and workflow execution into integrated offerings. That will lower the assembly tax over time, but it will not change the underlying principles.</p><div><hr></div><p><em><strong>Resilience Is a Layered Onion: Idempotency, Observability, and Compensation</strong></em></p><p>Patterns are necessary but not sufficient. The research is unambiguous: <strong>reliable distributed systems depend on idempotency, deduplication, clearly defined compensating actions, and strong observability.</strong> Delivery semantics alone will not save you.</p><p><strong>Idempotency</strong> is the discipline of making repeated execution safe. Every consumer that can receive the same event twice must detect or tolerate duplicates. The simplest mechanism is an <code>idempotency_key</code> stored in the consumer&#8217;s own database, checked at the start of processing within the same transaction that performs the work. This guarantees that even if the process crashes after the work is done but before the ack is sent, a retry will see the stored key and skip the work.</p><p>Compensation is not magic. A compensation is a business operation that must be designed, tested, and observed like any other. It must be idempotent, because you may try to run it more than once. It must have clear success and failure semantics. And when a compensation itself fails after retries, your system must have an escalation path, such as moving the saga to a human-reviewed dead-letter queue rather than silently leaving partial state.</p><p><strong>Observability</strong> is what makes sagas debuggable. You need distributed tracing across the outbox relay, the broker, the saga orchestrator, and the consumers. You need metrics on relay lag, saga step duration, compensation frequency, and consumer lag. You need structured logs that include saga ID, step name, event ID, and idempotency key. A saga that fails in production without observability is a ticket that takes three days to understand. A saga that fails with full tracing is a ticket that takes twenty minutes.</p><p>Together these layers form the real resilience strategy. The outbox protects the publisher. The saga defines the recovery path. Idempotency protects against retries. Observability lets you see what went wrong. None of them alone is enough.</p><div><hr></div><p><em><strong>The Convergence Curve: Where This All Heads by 2027</strong></em></p><p>If there is one big-picture takeaway from the research, it is this: <strong>the boundaries between event streaming, outbox change-data capture, and durable workflow execution are blurring.</strong> We are moving from a world where you stitched together Kafka, Debezium, and Temporal by hand to a world where platforms offer more integrated paths.</p><p>Expect to see more managed services that combine a transactional database, an event outbox, automatic CDC propagation, and saga orchestration behind cleaner SDKs. Expect multi-language support to improve, including better Python SDKs for durable execution. Expect the operational burden of running an event-driven architecture to decline, even as the conceptual burden stays roughly the same.</p><p>Because here is the thing: the patterns themselves are durable. The outbox pattern will still be correct in ten years. Sagas will still be necessary whenever a business process crosses transactional boundaries. Invariant-driven consistency will still be the right way to think about distributed state. The tooling will get prettier, but the ideas will not change.</p><p>That is actually good news. Once you understand the layers, you are not at the mercy of any vendor. You can evaluate a new platform by asking the same questions: Does it make state changes and event publication atomic? Does it give me saga execution with compensation? Does it preserve the ordering my invariants require? Does it help me observe and retry safely?</p><p>If the answer is yes, it is probably worth your time.</p><div><hr></div><p><em><strong>Closing Stanza</strong></em></p><p>So here is the warm sign-off I promised.</p><p>May your outbox tables stay small, your Debezium connectors stay healthy, and your compensations never run twice. May your sagas complete more often than they compensate, and when they do compensate, may they do so cleanly and with excellent logs. May your Kafka partitions be evenly keyed, your consumers be idempotent, and your product managers finally believe you when you say <em>eventual consistency is a feature, not a bug.</em></p><p>Building distributed systems is hard. Building them without atomic state-and-event bridges, saga recovery, and clear consistency boundaries is harder. You now have the map.</p><p>Keep building the invisible plumbing. Keep choosing tools with intention. And come back tomorrow for the next issue of <strong>The Backend Developers</strong>&#8212;we will still be here, making sense of the chaos, one event at a time.</p><p>Take care, and happy shipping.</p>]]></content:encoded></item><item><title><![CDATA[Zero-Downtime Database Migrations: Expand/Contract, Triggers, and Shadow Reads]]></title><description><![CDATA[The Day I Learned That &#8220;ALTER TABLE&#8221; Is a Four-Letter Word]]></description><link>https://thebackenddevelopers.substack.com/p/zero-downtime-database-migrations</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/zero-downtime-database-migrations</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Mon, 03 Aug 2026 20:51:05 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/209694525/83d81ae971097a29c8f5bb32fb21d9ef.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>If you&#8217;ve ever run a migration at 2 a.m. while holding your breath and refreshing a dashboard, welcome to the club. We&#8217;ve all been there: the table has 300 million rows, the change looks innocent, and yet somehow the deployment window stretches from &#8220;five minutes&#8221; into &#8220;why is the CEO in the Slack channel?&#8221;</p><p>Database migrations are the plumbing of backend engineering. Nobody writes poetry about them until they explode. And in a world where customers expect 24/7 uptime and deployments happen dozens of times a day, the old &#8220;stop the app, run DDL, pray&#8221; playbook simply doesn&#8217;t cut it.</p><p>Today, we&#8217;re going to look at three techniques that separate the pros from the sleep-deprived: <strong>expand/contract schema changes</strong>, <strong>trigger-based synchronization</strong>, and <strong>shadow reads</strong>. Used separately, each is useful. Combined, they let you refactor a production database the way a surgeon replaces a heart valve &#8212; while the patient is still running a marathon.</p><p><em><strong>Why Zero-Downtime Migrations Matter More Than Ever</strong></em></p><p>Let&#8217;s start with a truth that&#8217;s easy to forget: your application and your schema are not two isolated systems. They are one moving organism. In modern CI/CD pipelines, multiple versions of your application can be live at the same time. A rolling deployment means Version A and Version B share the database simultaneously. If Version B needs a column that Version A doesn&#8217;t understand &#8212; or vice versa &#8212; someone is going to crash.</p><p>The foundational strategy for surviving this is to treat schema changes as additive and backward-compatible. That is the heart of the <strong>expand/contract pattern</strong>: every schema change must first <em>expand</em> the schema to support both old and new behavior, then later <em>contract</em> by removing the parts you no longer need. This lets old and new application versions coexist during a phased deployment sequence.</p><p>But expand/contract only gives you the runway. You still need to keep data in sync across the old and new shapes while you transition, and you need to verify that the new shape actually behaves correctly before you make it primary. That is where triggers and shadow reads come in.</p><p>Large engineering organizations such as Stripe, Shopify, and GitHub treat migrations as multi-step workflows rather than single DDL operations. They don&#8217;t run one big <code>ALTER TABLE</code> and call it done. They sequence changes, validate them, and roll back gracefully if anything smells wrong. The lesson is simple: zero-downtime migrations are fundamentally socio-technical processes. They require coordination between application deployments and schema changes, making backward and forward compatibility non-negotiable design constraints.</p><p><em><strong>The Expand/Contract Pattern: Add First, Delete Later</strong></em></p><p>Let&#8217;s unpack the pattern in detail, because everything else rests on it.</p><p>In a traditional migration, you change the schema and the application at the same time. The old schema is gone; the new application expects it; there is no overlap. That works in a single-step deployment, but it is brittle and downtime-prone.</p><p>Expand/contract replaces that single destructive step with a sequence of safer steps:</p><ol><li><p><strong>Expand the schema.</strong> Add the new columns, tables, or indexes you need, but do not remove anything yet. The old application version can still read and write the old structure, and the new application version can read and write the new structure.</p></li><li><p><strong>Update the application to write both shapes.</strong> During the transition, the application writes data to the old schema and the new schema. This is often called dual writing. Reads may still come from the old schema.</p></li><li><p><strong>Backfill and synchronize data.</strong> Any existing data must be copied or transformed into the new shape, and ongoing writes must be kept in sync.</p></li><li><p><strong>Cut reads over to the new schema.</strong> Once you have verified correctness and performance, you switch read traffic to the new schema.</p></li><li><p><strong>Stop writing to the old schema.</strong> Remove the old write path from the application.</p></li><li><p><strong>Contract the schema.</strong> Only after nothing is using the old columns or tables do you drop them.</p></li></ol><p>The key insight here is reversibility. At almost every stage, you can roll back to the previous state without data loss, because the old schema still exists and the data is still there.</p><p>For example, imagine you are renaming the <code>email</code> column to <code>email_address</code>. Instead of running <code>RENAME COLUMN</code>, you add <code>email_address</code>, copy existing values, dual-write both, cut reads to <code>email_address</code>, stop writing to <code>email</code>, and only then drop <code>email</code>. It is more steps than a single DDL command, but it is also the reason your users don&#8217;t see a 503 while you are doing it.</p><p><em><strong>Trigger-Based Synchronization: The Database&#8217;s Intern</strong></em></p><p>Dual writing from the application is clean when you control every code path, but real systems have cron jobs, event processors, legacy scripts, third-party integrations, and that one Python script Dave wrote in 2019 that nobody wants to touch. If even one writer only knows the old schema, your new schema will drift.</p><p>That is where trigger-based synchronization shines. A database trigger is a piece of logic that runs automatically when a specified change happens to a table. You can use triggers to mirror writes from the old schema shape to the new one, or vice versa, at the database layer.</p><p>Trigger-based synchronization provides a reliable mechanism to mirror writes between old and new schema shapes. But &#8212; and this is a big but &#8212; it requires careful handling of idempotency, ordering, and rollback procedures to avoid data divergence or infinite loops.</p><p>Here are the practical concerns:</p><ul><li><p><strong>Idempotency.</strong> The trigger must be safe to run multiple times for the same logical event. If a row is updated, the trigger should update the corresponding new row if it exists and insert it if it does not, ideally in an idempotent way.</p></li><li><p><strong>Ordering.</strong> If writes to the old schema trigger updates to the new schema, and writes to the new schema also trigger updates to the old schema, you can create an infinite loop unless you guard against it. A common technique is to use a session variable or a sentinel column to suppress recursive triggers.</p></li><li><p><strong>Performance.</strong> Triggers run in the same transaction as the original write. Heavy trigger logic can increase write latency and lock contention. Keep triggers lean and avoid complex business logic inside them.</p></li><li><p><strong>Rollback.</strong> Triggers are part of the schema. You need a tested procedure to disable or reverse them if the cutover fails. If the trigger has already propagated a bad write, you must know how to reconcile.</p></li></ul><p>Despite these caveats, triggers are a powerful safety net. They let you guarantee synchronization even for writers you cannot fully control, and they keep the old and new schema shapes consistent while your application code slowly migrates.</p><p><em><strong>Shadow Reads and Dual Writes: Test in Production (Responsibly)</strong></em></p><p>Now we get to my favorite part: the moment when you peek into the future without actually stepping into it.</p><p><strong>Dual writes</strong> means the application writes to both the old and new schema at the same time. This keeps them synchronized through application logic rather than triggers. It is a common companion to expand/contract because it gives you real, production write traffic into the new schema.</p><p><strong>Shadow reads</strong> go a step further. When you perform a read from the old schema, you also perform the equivalent read from the new schema, but you do not return the new result to the user. Instead, you compare the two results asynchronously and log any differences. This lets you measure correctness and performance before promoting the new schema to primary.</p><p>Shadow reads and dual writes enable safe validation of a new schema by asynchronously comparing query results from both old and new data stores. This is the empirical layer of zero-downtime migration. You are not guessing whether the new schema works; you are proving it with production traffic.</p><p>The comparison logic must be thoughtful. Timestamps may differ by microseconds. Floats may have rounding differences. Ordering may be unstable if you do not include explicit sort keys. Your shadow read comparator should normalize results and define acceptable tolerance.</p><p>You also need telemetry. Count mismatches, latency percentiles, and error rates. If the new schema is slower, you want to know before it becomes primary. If it returns wrong data, you want to know before a customer notices.</p><p>A typical cutover plan looks like this:</p><ol><li><p>Deploy dual writes so both schemas receive live traffic.</p></li><li><p>Backfill historical data.</p></li><li><p>Enable shadow reads and monitor comparison metrics.</p></li><li><p>Fix any discrepancies.</p></li><li><p>Switch reads to the new schema.</p></li><li><p>Disable dual writes to the old schema.</p></li><li><p>Contract the old schema.</p></li></ol><p>This sequence is why major teams treat migrations as multi-step workflows. The actual DDL is maybe 10 percent of the work. The rest is observation, verification, and coordination.</p><p><em><strong>A Concrete Walkthrough with Python</strong></em></p><p>Let&#8217;s make this concrete. Suppose you have a <code>users</code> table with a single <code>full_name</code> column, and you want to split it into <code>first_name</code> and <code>last_name</code>. We will simulate the expand/contract pattern, trigger-based synchronization, and shadow reads using Python and SQLite.</p><p>First, the initial schema:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4ad9fab1-91ad-4826-a6fd-0bcd78420bc1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import sqlite3

conn = sqlite3.connect(":memory:")
conn.execute("PRAGMA foreign_keys = ON")
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        full_name TEXT NOT NULL,
        created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    )
""")
conn.commit()</code></pre></div><p>Now we expand the schema by adding the new columns:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;416ba73d-4813-4d42-83be-7520a8c6de81&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">cursor.execute("""
    ALTER TABLE users
    ADD COLUMN first_name TEXT;
""")
cursor.execute("""
    ALTER TABLE users
    ADD COLUMN last_name TEXT;
""")
conn.commit()</code></pre></div><p>Next, we backfill existing rows. In production, you would do this in batches to avoid long locks:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6a3d6723-5def-460d-a07b-9283fa64640d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def backfill_name_split(cursor):
    cursor.execute("SELECT id, full_name FROM users WHERE first_name IS NULL")
    for row_id, full_name in cursor.fetchall():
        parts = full_name.split(maxsplit=1)
        first = parts[0]
        last = parts[1] if len(parts) &gt; 1 else ""
        cursor.execute("""
            UPDATE users
            SET first_name = ?, last_name = ?
            WHERE id = ?
        """, (first, last, row_id))

backfill_name_split(cursor)
conn.commit()</code></pre></div><p>Now we add triggers to keep the old and new columns synchronized. We use a sentinel session variable to prevent infinite recursion:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c5244179-c33d-4291-8e0c-14ece9b3ab26&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">cursor.executescript("""
    CREATE TRIGGER trg_users_sync_old_to_new
    AFTER UPDATE OF full_name ON users
    WHEN IFNULL(current_setting('syncing'), '0') = '0'
    BEGIN
        UPDATE users
        SET first_name = substr(NEW.full_name, 1, instr(NEW.full_name || ' ', ' ') - 1),
            last_name = substr(NEW.full_name || ' ', instr(NEW.full_name || ' ', ' ') + 1)
        WHERE id = NEW.id;
    END;

    CREATE TRIGGER trg_users_sync_new_to_old
    AFTER UPDATE OF first_name, last_name ON users
    WHEN IFNULL(current_setting('syncing'), '0') = '0'
    BEGIN
        UPDATE users
        SET full_name = NEW.first_name || ' ' || NEW.last_name
        WHERE id = NEW.id;
    END;
""")
conn.commit()</code></pre></div><p><em>Note: SQLite uses </em><code>current_setting</code><em> semantics differently than PostgreSQL; in production on Postgres you would use </em><code>SET LOCAL my_app.syncing = '1'</code><em> inside the trigger function. The concept is the same: prevent recursive loops.</em></p><p>Now we simulate dual writes from two application versions. The old code writes <code>full_name</code>; the new code writes <code>first_name</code> and <code>last_name</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;006bf50d-f792-48d6-8d4e-5672e8453ad8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def insert_user_old(cursor, full_name):
    cursor.execute("""
        INSERT INTO users (full_name) VALUES (?)
    """, (full_name,))

def insert_user_new(cursor, first_name, last_name):
    cursor.execute("""
        INSERT INTO users (first_name, last_name, full_name)
        VALUES (?, ?, ?)
    """, (first_name, last_name, f"{first_name} {last_name}"))</code></pre></div><p>Finally, we implement a shadow read comparator. The application reads from the old columns, but also queries the new columns in the background and compares results:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;abdf340b-3ce6-4682-b247-8f88dc6fba04&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def read_user_old(cursor, user_id):
    cursor.execute("SELECT id, full_name FROM users WHERE id = ?", (user_id,))
    return cursor.fetchone()

def read_user_new(cursor, user_id):
    cursor.execute("""
        SELECT id, first_name, last_name
        FROM users WHERE id = ?
    """, (user_id,))
    return cursor.fetchone()

def shadow_read_compare(cursor, user_id):
    old_row = read_user_old(cursor, user_id)
    new_row = read_user_new(cursor, user_id)

    old_full = old_row[1]
    new_full = f"{new_row[1]} {new_row[2]}".strip()

    if old_full != new_full:
        print(f"MISMATCH for user {user_id}: old='{old_full}' new='{new_full}'")
        return False
    print(f"MATCH for user {user_id}: '{old_full}'")
    return True</code></pre></div><p>This small script captures the essence of the pattern. In a real system, the shadow comparison would run in a background job, emit metrics, and route alerts to a dashboard. The triggers would be guarded by session flags. And the backfill would run in idempotent, resumable batches.</p><p><em><strong>Tooling That Saves Your Weekend</strong></em></p><p>Philosophy is great, but at some point you need tools that actually run the commands. The choice of tooling significantly influences the feasibility of zero-downtime migrations by automating schema changes, online table rebuilds, and deployment sequencing.</p><p>Here are the heavy hitters:</p><ul><li><p><strong>pt-online-schema-change</strong> (Percona Toolkit). A classic for MySQL. It creates a shadow copy of the table, applies the schema change to the copy, synchronizes deltas using triggers, and then swaps the tables. It avoids long locks on large tables.</p></li><li><p><strong>gh-ost</strong>. GitHub&#8217;s online schema change tool for MySQL. Instead of triggers, it uses a binary log stream to capture changes. This reduces trigger overhead and makes it easier to throttle and pause migrations mid-flight.</p></li><li><p><strong>Flyway</strong> and **Liquibase.These are schema version control systems. They do not perform online table rebuilds themselves, but they are essential for sequencing migrations, tracking which scripts have run, and coordinating multi-step expand/contract workflows across environments.</p></li><li><p><strong>Reshape</strong> and <strong>pgroll</strong>. Newer tools designed specifically for expand/contract migrations on PostgreSQL. They manage multiple schema versions at the database level, making it easier to keep old and new application versions happy.</p></li><li><p><strong>AWS Database Migration Service (DMS)</strong> and similar platforms. Useful when you are migrating across database engines or regions, often combining ongoing replication with cutover tooling.</p></li></ul><p>No single tool does everything. The real pros combine them: use pt-online-schema-change or gh-ost for the low-level table rebuild, Flyway or Liquibase for migration sequencing, and custom shadow-read infrastructure for validation.</p><p><em><strong>Rollback: The Feature You Hope to Never Use</strong></em></p><p>Here is the uncomfortable truth: a migration is not done when the new schema is live. A migration is done when you are confident you can undo every step of it.</p><p>Rollback safety is a cross-cutting concern that connects all three techniques. Each phase of an expand/contract migration, trigger synchronization setup, and shadow-read validation must be reversible without data loss.</p><p>Before you start, ask these questions:</p><ul><li><p>Can I revert the application to the previous version and still read the old schema?</p></li><li><p>If the trigger is removed, will the old schema still contain the correct data?</p></li><li><p>If I stop dual writes to the new schema, will the old schema continue to work?</p></li><li><p>Do I have a point-in-time backup or logical restore path?</p></li><li><p>Can I pause the migration and resume it later?</p></li></ul><p>Write down the rollback steps. Test them in staging. If your answer to any of these questions is &#8220;I think so,&#8221; you are not ready.</p><p>The best migration plans read like a choose-your-own-adventure book, with a happy path and several sad paths. The teams that sleep well are the ones that have rehearsed the sad paths.</p><p><em><strong>Closing Thoughts: Migrate Like You Mean It</strong></em></p><p>Zero-downtime database migrations are not magic. They are discipline. They are the art of making big changes in small, reversible steps and trusting the evidence before you trust the cutover.</p><p>Use expand/contract to give yourself a safe runway. Use triggers or dual writes to keep both schema shapes consistent. Use shadow reads to prove the new shape works under real traffic. And always, always have a rollback plan that you have actually tested.</p><p>The next time someone asks you to &#8220;just run an ALTER TABLE real quick,&#8221; you can smile knowingly, crack your knuckles, and say, &#8220;Sure &#8212; let me show you the plan.&#8221;</p><p>Keep building, keep shipping, and may your migrations be boring.</p><p>Warmly,</p><p><em>The Backend Developers</em></p><p>P.S. &#8212; If this post saved you from a midnight outage, come back tomorrow. We&#8217;re just getting started. Follow along, share it with your favorite DBA, and let&#8217;s make the backend world a little less terrifying together.</p>]]></content:encoded></item><item><title><![CDATA[WebAssembly Server-Side: Component Model, wasmCloud, and Spin in Production]]></title><description><![CDATA[The browser plugin that escaped into the data center]]></description><link>https://thebackenddevelopers.substack.com/p/webassembly-server-side-component</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/webassembly-server-side-component</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Fri, 31 Jul 2026 08:21:08 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/209226341/639be10dd53508be0c6a084748505a36.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>A decade ago, if you had told me that the technology behind running <em>Doom</em> in a browser tab would eventually be pitched as the future of server-side infrastructure, I would have laughed into my third coffee of the morning. WebAssembly? That was the thing frontend teams waved around when they wanted to compile C++ into something Chrome wouldn&#8217;t immediately reject. It was a curiosity. A stunt. A way to make CAD software render inside a <code>&lt;canvas&gt;</code> without melting the user&#8217;s laptop.</p><p>And yet, here we are.</p><p>Server-side WebAssembly has stopped being the &#8220;maybe someday&#8221; slide at cloud conferences and started becoming a genuine architectural choice. Not because it is magical, and not because it will replace every container you own by next Tuesday, but because a few critical pieces have finally clicked into place. The Component Model is maturing from specification to practical interoperability layer. Runtimes such as wasmCloud and Spin are offering real production paths. Enterprises are exploring Wasm for microservices consolidation, plugin systems, multi-tenant isolation, and supply-chain-verifiable workloads. The value proposition is also consistent: near-native performance combined with millisecond cold-start isolation, which makes serverless platforms drool over the idea of packing more workloads onto less metal.</p><p>But&#8212;and this is the part that keeps me employed as a newsletter writer&#8212;there are still tooling gaps. Component composition, debugging, observability, package registries, and language toolchain completeness are racing to catch up with what the runtime can already do. The headline is promising; the footnotes are still being written.</p><p>So today, we are going to look at what server-side Wasm actually means for backend developers. We will demystify the Component Model, walk through how wasmCloud and Spin approach production, talk about where the real adoption is happening, stare honestly at the rough edges, and finish with a Python example because I know how much you all love a working snippet. Let&#8217;s get into it.</p><p><em><strong>What the Component Model actually is</strong></em></p><p>If you want to understand server-side Wasm today, start here. The Component Model is the central unifying force behind the current wave of adoption. It is not a runtime. It is not a framework. It is a standardized way of describing what a piece of Wasm code can do, what it needs from the outside world, and how different Wasm modules can be wired together without everyone having to agree on a single programming language.</p><p>To appreciate why this matters, it helps to remember what plain WebAssembly gives you. At its core, Wasm is a portable stack-based virtual machine that executes bytecode at near-native speed inside a tight sandbox. The security model is attractive: by default, a Wasm module has no access to the network, the filesystem, the system clock, or anything else unless the host explicitly grants it. The binary is compact, deterministic, and can run anywhere there is a compliant runtime.</p><p>That is powerful, but it is also limited. Classic Wasm modules communicate with the outside world through low-level numeric imports and exports. If you write a module in Rust and I write a module in Go, and we want them to talk to each other, we have to agree on a calling convention: how we pass strings, how we allocate memory, how we handle errors, how we pass complex structures back and forth. Historically, those conventions were ad-hoc Application Binary Interfaces, and ad-hoc ABIs are where portability goes to die. They are brittle, language-specific, and hard to evolve.</p><p>The Component Model solves this by introducing a higher-level interface definition layer called WIT, the Wasm Interface Types format. WIT lets you define functions, records, variants, enums, flags, resources, and other data types in a language-neutral way. A component then imports and exports functions and interfaces described in WIT. The runtime uses the canonical ABI, a standardized lifting and lowering mechanism, to translate rich data types such as strings, lists, and records into the low-level linear memory representation that core Wasm understands.</p><p>The result is a component: a self-describing, black-box unit of computation with typed imports and exports. You can compose two components statically, linking the exports of one to the imports of another, using tools such as <code>wasm-tools compose</code>. You can also link a component with a host at runtime, because the host knows how to satisfy the component&#8217;s imports through the same well-defined interface.</p><p>This model is the foundation of WASI Preview 2. WASI, the WebAssembly System Interface, defines a portable set of capabilities that components can request from a host: filesystem access, clocks, randomness, sockets, HTTP, and more. In WASI Preview 2, those capabilities are expressed as WIT interfaces, and components request them through imports. A component that only imports <code>wasi:io/streams</code> and <code>wasi:http/incoming-handler</code> cannot secretly open a raw socket. It is capability-based security made explicit, which is a big deal for multi-tenant and regulated environments.</p><p>From a backend perspective, the Component Model turns Wasm into an interoperability layer rather than just a runtime. A Python component, a Rust component, and a Go component can all implement the same interface. They can be swapped in and out. They can be composed into larger applications. This is the shift from &#8220;I compiled my code to Wasm&#8221; to &#8220;I have a portable, composable, language-agnostic building block for distributed systems.&#8221;</p><p><em><strong>wasmCloud: actors, capabilities, and a whole lot of NATS</strong></em></p><p>If the Component Model gives us the <em>what</em>, wasmCloud gives us one vision of the <em>where</em> and the <em>how</em>. wasmCloud is a distributed application platform built on WebAssembly components. Its design philosophy is heavily influenced by the actor model and by the idea that business logic should be completely separated from the messy, non-deterministic outside world.</p><p>In wasmCloud, the basic unit of compute is an actor. An actor is a stateless Wasm component that implements your application logic. It could be written in Rust, Go, Python, or any language that can target components. Actors do not directly open HTTP sockets, read files, publish messages, or query databases. Instead, they declare which capabilities they need through WIT-defined interfaces. For example, an actor might say, &#8220;I need the HTTP server capability,&#8221; or &#8220;I need the key-value store capability,&#8221; or &#8220;I need the messaging capability.&#8221;</p><p>Capabilities are provided by capability providers, which are separate runtime components that mediate access to the real world. A provider might be a Redis-backed key-value store, an NATS messaging bus, an HTTP server, or a blob store. The actor calls a capability as if it were a typed interface, and the provider resolves that call against an actual implementation. This separation has several benefits. First, it keeps actors tiny, deterministic, and easy to test. Second, it lets you swap implementations without touching your business logic. Your local tests can use an in-memory key-value provider; production can use Redis; and neither case requires recompiling the actor. Third, it gives you a clear security boundary: the runtime can grant or deny capabilities per actor, and the actor cannot reach around that boundary.</p><p>The second major concept in wasmCloud is the lattice. The lattice is wasmCloud&#8217;s clustering and networking layer, built on NATS. When you run multiple wasmCloud hosts across laptops, VMs, Kubernetes pods, or edge devices, they discover each other through NATS and form a lattice. Actors and providers can be scheduled on different hosts, and they communicate securely over the lattice without you having to configure a service mesh, load balancer, or certificate infrastructure by hand. The lattice handles service discovery, load balancing, failover, and zero-trust identity through signed claims on the Wasm artifacts.</p><p>wasmCloud 1.0 made an important architectural shift by moving from the older &#8220;module + waPC&#8221; model to the standard WebAssembly Component Model. This means actors are now ordinary components, and capabilities are ordinary WIT interfaces. The benefit is interoperability: a component written for wasmCloud can increasingly be reused in other component-aware runtimes, and vice versa. Under the hood, wasmCloud uses Wasmtime as its runtime.</p><p>This architecture fits use cases where you want a distributed platform rather than a simple function-as-a-service runtime. It is well suited to microservices consolidation, where many small services can become actors; to edge-to-cloud deployments, where the same component runs at the edge and in the core; and to polyglot environments, where teams want to let different languages coexist without everyone adopting the same framework.</p><p><em><strong>Spin: functions, triggers, and the art of not hating your toolchain</strong></em></p><p>While wasmCloud asks, &#8220;What if your entire distributed platform was made of composable actors?&#8221;, Spin asks, &#8220;What if writing and deploying a Wasm function felt as easy as writing a Lambda or a Cloud Function, but lighter, faster, and more portable?&#8221; Spin is a developer framework and runtime from Fermyon, and it is arguably the most polished developer experience in the server-side Wasm space right now.</p><p>Spin is built around the idea of triggers and components. You write a component, which is just a Wasm component implementing a handler for a particular trigger type, and Spin takes care of the rest. The most common trigger is HTTP, but Spin also supports Redis pub/sub triggers, NATS messaging triggers, MQTT, cron-like scheduled jobs, and key-value triggers. This event-driven model is immediately familiar to anyone who has written serverless functions.</p><p>The developer workflow looks like this. You run <code>spin new http-rust my-api</code>, which scaffolds a project. You write your handler. You run <code>spin build</code> to compile the component. You run <code>spin up</code> to start a local server. You test it. You run <code>spin deploy</code> or <code>spin registry push</code> to get it into production. There is a <code>spin.toml</code> manifest that describes your application, its components, triggers, build commands, and runtime configuration. The whole experience is intentionally small and fast: Spin starts in milliseconds, and the resulting artifacts are measured in kilobytes or low megabytes rather than hundreds of megabytes of container image.</p><p>Spin runs on Wasmtime and uses the Component Model under the hood. It has good language support, especially for Rust, Go, JavaScript, TypeScript, and, increasingly, Python. For deployment, you can run Spin yourself, deploy to Fermyon Cloud, run Spin on Kubernetes via SpinKube, or embed the Spin runtime in your own platform. The fast cold start and small memory footprint make Spin attractive for high-density serverless, edge functions, plugin systems, and microservices that need to scale to zero.</p><p>Where wasmCloud leans toward distributed platform services, Spin leans toward developer-friendly, trigger-based functions. They are converging on the same Component Model foundation, but their ergonomics and sweet spots differ. If you are a backend team that wants to deploy an HTTP API or a webhook handler without provisioning clusters, Spin is probably the gentler on-ramp. If you are building a polyglot, distributed mesh of capabilities across regions and devices, wasmCloud is the more natural fit.</p><p><em><strong>Why production is starting to care</strong></em></p><p>For a long time, server-side Wasm was the domain of conference demos and hobby edge projects. That is changing. The research signals we are tracking point to a shift from experimental edge and FaaS use cases toward core platform infrastructure. Enterprises are exploring Wasm for several concrete reasons.</p><p>The first is microservices consolidation. Teams that have ended up with hundreds of small services, each wrapped in its own container, are looking for ways to reduce overhead without returning to the monolith. Wasm components can be packed at much higher density than containers because they share a single runtime and start in milliseconds. You can run thousands of components on a single host where you previously ran dozens of containers.</p><p>The second use case is plugin and extension systems. If you run a multi-tenant SaaS platform and you want customers to upload custom business logic, you have historically been stuck choosing between slow sandboxing, heavy containers, or trusting a scripting interpreter. Wasm gives you a sandboxed, near-native execution environment with a well-defined interface. You can let customers upload a <code>.wasm</code> component that implements your plugin interface, and you can run it with tight resource limits and no implicit access to the host.</p><p>The third is multi-tenant isolation. Containers share a kernel. VMs are heavy. Wasm modules run inside a sandboxed VM with no shared kernel surface beyond the runtime, and their capability imports are explicit. That combination is compelling for platforms that need strong isolation boundaries without the cost of a full VM per tenant.</p><p>The fourth is supply-chain verifiability. Wasm binaries are deterministic and self-contained. Tools are emerging to sign them, attest to their provenance, and inspect their imports before running them. In regulated environments, the ability to audit exactly what a workload can do, and to verify that the binary matches a known source, is genuinely useful.</p><p>Across all of these, the consistent value proposition is the same: near-native performance combined with millisecond cold-start isolation. Containers gave us packaging. Serverless gave us scaling to zero. Wasm promises both, with less memory, faster startup, and a smaller attack surface. It will not replace containers everywhere, but for the right workloads it can dramatically improve density and responsiveness.</p><p>In practice, most production adoption today is hybrid. Wasm handles specific workloads, such as event handlers, plugin runtimes, and edge functions, while containers continue to run databases, legacy applications, and heavy stateful services. That is a healthy place to be. You do not need to burn your Kubernetes cluster to benefit from Wasm.</p><p><em><strong>A real-ish Python example: let&#8217;s over-engineer &#8220;Hello, World&#8221;</strong></em></p><p>Enough theory. Let us look at what this actually looks like in code. I promised Python, and I intend to deliver, because there is something deeply satisfying about taking a language famous for its runtime size and stuffing it into a tiny sandboxed <code>.wasm</code> file.</p><p>We are going to build a tiny Spin application in Python. It will respond to an HTTP request with a JSON payload. It is trivial, but it is also a complete component: it has a typed interface, a build pipeline, and a manifest.</p><p>First, install the tools. You will need the Spin CLI, Python, and the <code>componentize-py</code> tool, which compiles Python code and a subset of the Python standard library into a Wasm component. You will also want the <code>spin-sdk</code> package for the Python bindings.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2e0939d6-7f90-4236-a2a1-b39ba3da8a2f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">pip install componentize-py spin-sdk
spin new http-py python-hello --accept-defaults
cd python-hello</code></pre></div><p>Here is the Python handler. Spin handles the HTTP trigger plumbing, so we only need to implement the function that returns a response.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;46311890-218e-4c2d-a7d2-7c6c0aeae64a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># app.py
import json
from spin_http import Response

def handle_request(request):
    """
    A perfectly normal Python function that happens to run inside a Wasm component.
    Spin calls this whenever the HTTP trigger fires.
    """
    payload = {
        "message": "Hello from Python, now living its best life as a .wasm file",
        "method": request.method,
        "path": request.uri,
    }

    return Response(
        status=200,
        headers={"content-type": "application/json"},
        body=json.dumps(payload).encode("utf-8"),
    )</code></pre></div><p>The <code>request</code> object gives you the method, URI, headers, and body. The <code>Response</code> object lets you set the status, headers, and body. That is the whole contract. Under the hood, this maps to the <code>wasi:http/incoming-handler</code> interface through Spin&#8217;s bindings.</p><p>Next, the manifest. The <code>spin.toml</code> file tells Spin what to build and how to route traffic to it.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;46b75034-b42e-4000-8992-337a29d0ea1e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">spin_manifest_version = "2"
name = "python-hello"
version = "0.1.0"
description = "The most over-engineered Hello World in backend history."

[[trigger.http]]
route = "/hello"
component = "hello"

[component.hello]
source = "app.wasm"

[component.hello.build]
command = "componentize-py -w spin-http componentize app -o app.wasm"</code></pre></div><p>The <code>source</code> points at the compiled Wasm component. The <code>build</code> section tells Spin how to create that component from the Python source. The <code>-w spin-http</code> flag selects the Spin HTTP world, and <code>componentize-py</code> packages the Python bytecode and runtime support into a component.</p><p>Now build and run it.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;fde06f46-3b2b-4d09-baca-169f03277d51&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">spin build
spin up --listen 127.0.0.1:3000</code></pre></div><p>In another terminal:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;f72fae89-4dc7-4be3-a3aa-c827f5526b7c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">curl http://127.0.0.1:3000/hello</code></pre></div><p>You should get something like:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;e3dc3224-ef28-4342-b163-30fecc55198a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">{
  "message": "Hello from Python, now living its best life as a .wasm file",
  "method": "GET",
  "path": "/hello"
}</code></pre></div><p>That is it. A Python function, compiled to a Wasm component, running in a tiny runtime, triggered over HTTP, with a typed interface. It starts fast, uses a tiny amount of memory, and cannot open arbitrary network connections unless the manifest explicitly grants that capability.</p><p>I chose Python here because it is the language people least expect to see in a Wasm example, and because it shows how far the tooling has come. Rust is still the smoothest path for components, but Python, Go, JavaScript, and TypeScript are all viable now. The Component Model is doing exactly what it was designed to do: let you pick the language that makes sense for the task without being locked into a runtime-specific ABI.</p><p><em><strong>The parts that still make us sigh</strong></em></p><p>I would not be doing my job if I pretended everything was sunshine and perfect startup times. Server-side Wasm is real, but it is still early enough that you will encounter friction. The research we reviewed identified tooling and standards gaps as the primary friction points, and that matches what I hear from teams actually running this in production.</p><p>Component composition is powerful but not yet effortless. Tools such as <code>wasm-tools compose</code> work well, but composing multiple components, managing versions, and debugging composition failures still feels more like build-engineering surgery than routine development. Expect this to improve rapidly, but today it is a skill, not a button.</p><p>Debugging and observability are still catching up. Traditional containers give you stack traces, core dumps, mature logging agents, and a deep ecosystem of profilers. Wasm components are improving here, with DWARF debugging support and OpenTelemetry integrations appearing in runtimes such as Wasmtime, Spin, and wasmCloud, but the experience is not yet as seamless as debugging a normal process. When something goes wrong inside a component, you may spend more time than you would like staring at linear memory layouts.</p><p>Package registries are another work in progress. The WebAssembly community is developing WARG, the WebAssembly Registry format, and tools such as <code>wasm-pkg-tools</code> are starting to make components feel more like packages. But most teams today use OCI registries, GitHub releases, or custom artifact stores. The registry story will standardize, but it is not standardized yet.</p><p>Language toolchain completeness varies. Rust is the furthest along, with excellent support via <code>cargo-component</code>. Go, Python, and JavaScript are improving quickly through TinyGo, <code>componentize-py</code>, and <code>jco</code>. C and C++ have solid paths. But some languages are still waiting for mature bindings generators, and not every standard library feature is available inside the Wasm sandbox. If your favorite language is not on the list yet, patience is required.</p><p>Finally, operational familiarity is a real barrier. Most platform teams have spent years learning how to operate containers, Kubernetes, service meshes, and observability stacks. Wasm introduces new concepts: components, worlds, WIT, capability providers, lattice networking, and the WASI versioning story. The technology is not harder than Kubernetes was on day one, but it is different, and that difference has a learning tax.</p><p>The good news is that these are exactly the kinds of problems that get solved as adoption grows. We have seen this movie before with containers. The first few years were messy, and then the tooling hardened.</p><p><em><strong>Who&#8217;s building the future</strong></em></p><p>If you want to explore this space, you have a growing list of projects and services to choose from. Here are the ones worth knowing about.</p><ul><li><p><strong>Bytecode Alliance</strong>: The nonprofit home of the WebAssembly Component Model, WASI, Wasmtime, WIT, and the canonical ABI. Start here for standards and reference runtimes.</p></li><li><p><strong>Wasmtime</strong>: The flagship runtime from the Bytecode Alliance. It powers Spin, wasmCloud, and many other server-side Wasm platforms.</p></li><li><p><strong>wasmCloud</strong>: The distributed actor platform we covered. Great for building polyglot, capability-based applications across a lattice.</p></li><li><p><strong>Cosmonic</strong>: A managed platform and tooling layer built on top of wasmCloud, aimed at multi-cloud and edge deployments.</p></li><li><p><strong>Fermyon Spin</strong>: The developer-friendly framework and runtime for event-driven components, with excellent CLI ergonomics.</p></li><li><p><strong>SpinKube</strong>: The Kubernetes operator that lets you deploy Spin applications onto standard Kubernetes clusters.</p></li><li><p><strong>Fermyon Cloud</strong>: The hosted serverless platform for Spin applications, if you want to skip operating your own runtime.</p></li><li><p><strong>WasmEdge</strong>: A lightweight, high-performance Wasm runtime with strong edge and AI inference use cases.</p></li><li><p><strong>wasmer</strong>: Another general-purpose Wasm runtime with a focus on universal binaries and ease of embedding.</p></li><li><p><strong>Fastly Compute</strong>: A production edge compute platform that uses Wasm and Lucet/Wasmtime under the hood to run untrusted customer code at the edge.</p></li><li><p><strong>componentize-py</strong>: The tool that compiles Python into Wasm components, making examples like ours possible.</p></li><li><p><strong>cargo-component</strong>: The Rust toolchain for building Wasm components with WIT interfaces.</p></li><li><p><strong>jco</strong>: The JavaScript and TypeScript toolchain for building and running Wasm components, bridging Node.js and the browser.</p></li><li><p><strong>WARG / wasm-pkg-tools</strong>: Emerging standards and tooling for packaging, distributing, and composing Wasm components.</p></li></ul><p>This is not an exhaustive list, and the landscape changes quickly. If you are evaluating server-side Wasm today, I recommend starting with Spin if you want fast developer feedback, or wasmCloud if you want a distributed platform abstraction. Both will teach you the Component Model by forcing you to use it, which is the only way these concepts actually stick.</p><p><em><strong>Closing stanza</strong></em></p><p>So here is the truth as I see it, after two decades of watching backend paradigms arrive with fanfare and then quietly mature into plumbing: server-side WebAssembly is not going to save the world, replace every container, or make your monolith magically disappear. But it is going to become a standard substrate for the parts of the cloud where portability, isolation, density, and cold-start speed matter. It will sit alongside containers and VMs, not on top of them in triumph, but beside them as a respectable peer.</p><p>The Component Model is the real engine behind this transition. wasmCloud and Spin are the two most compelling production narratives right now, one rooted in distributed actors and the other in developer-friendly functions. The tooling still has scabs and bruises. The learning curve is real. But the trajectory is clear, and the problems being solved are the right problems.</p><p>If you have not written a Wasm component yet, do it this week. Pick Spin, pick Python or Rust, and ship something small. Break it. Debug it. Complain about the error messages. That is how you build the future: one slightly annoyed <code>curl</code> at a time.</p><p>Thanks for reading. Come back tomorrow. We will be here, caffeinated and cynical and genuinely excited about the next weird thing happening behind the API gateway.</p><p>Warmly,</p><p>The Backend Developers</p>]]></content:encoded></item><item><title><![CDATA[Edge AI on Mobile Devices in 2026: On-Device Inference, Battery, and Privacy]]></title><description><![CDATA[Edge AI Is Moving Into Your Pocket, Whether Your Battery Likes It or Not]]></description><link>https://thebackenddevelopers.substack.com/p/edge-ai-on-mobile-devices-in-2026</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/edge-ai-on-mobile-devices-in-2026</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Thu, 23 Jul 2026 23:29:03 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/208269474/94cdfbeb2907171f03f967220e4eb3ae.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>A few years ago, &#8220;AI on mobile&#8221; mostly meant a photo app that could find your cat and a voice assistant that occasionally understood your request after a small spiritual journey. In 2026, that era is over. Mobile devices are no longer passive clients for cloud AI&#8212;they are becoming serious inference engines in their own right.</p><p>That shift matters because the phone in your hand is now expected to do three things at once:</p><ul><li><p>respond instantly,</p></li><li><p>stay cool and battery-efficient,</p></li><li><p>and keep user data as local as possible.</p></li></ul><p>That sounds simple until you remember that phones are tiny computers pretending to be powerhouses. They have limited thermal headroom, shared memory, aggressive power management, and users who become emotionally attached to battery percentages. So the real story of edge AI on mobile in 2026 is not &#8220;can we run models locally?&#8221; It is &#8220;can we do it reliably, cheaply, and respectfully?&#8221;</p><p>The answer is increasingly yes&#8212;but only if we stop treating mobile AI like a mini cloud deployment and start treating it like a hardware-specific, energy-sensitive, privacy-aware system.</p><p><em><strong>Why On-Device Inference Is Becoming the Default</strong></em></p><p>For latency-sensitive tasks, on-device inference is becoming the obvious choice.</p><p>Think about the kinds of workloads users expect to feel instant:</p><ul><li><p>live camera enhancements,</p></li><li><p>transcription,</p></li><li><p>smart replies,</p></li><li><p>object detection,</p></li><li><p>gesture recognition,</p></li><li><p>personalized recommendations,</p></li><li><p>accessibility features,</p></li><li><p>and context-aware assistants.</p></li></ul><p>Sending every one of those requests to a remote server creates friction. Even with a fast network, the trip to the cloud introduces round-trip latency, jitter, connectivity dependency, and recurring backend cost. If the feature is meant to feel embedded in the device experience, cloud detours start to feel clumsy.</p><p>In 2026, the strongest mobile AI deployments are not winning because the model is huge or the paper looked exciting. They are winning because the model fits the device.</p><p>That means:</p><ul><li><p>operators map cleanly to the device&#8217;s NPU or GPU,</p></li><li><p>memory movement is minimized,</p></li><li><p>execution graphs are optimized for the mobile runtime,</p></li><li><p>and the model is shaped around the accelerator rather than the other way around.</p></li></ul><p>This is a subtle but important change. For years, model size reduction was the main obsession: quantize it, prune it, distill it, compress it. Those are still useful techniques, but they are no longer enough by themselves. A smaller model can still perform badly if it causes ugly memory access patterns or falls back to slower execution paths.</p><p>In practical terms, a well-chosen 8 MB model that matches the accelerator may outperform a &#8220;smaller&#8221; 4 MB model that doesn&#8217;t. Mobile AI in 2026 is increasingly about architectural fit, not just compression.</p><p><em><strong>The Hardware Reality: NPUs, GPUs, and the Age of Matching</strong></em></p><p>Modern mobile devices are more capable than people give them credit for. Many flagship phones now include dedicated NPUs, improved GPU pipelines, and runtime support that can accelerate common AI operations dramatically. But these components are not general-purpose magic. They are specialized.</p><p>That means performance depends on whether your model uses operations the device likes.</p><p>For example:</p><ul><li><p>convolution-heavy vision models often map well,</p></li><li><p>transformer-style workloads can be efficient if optimized carefully,</p></li><li><p>but certain dynamic control flows or exotic operators may cause slow fallbacks.</p></li></ul><p>This is why hardware-aware optimization is now central. The best deployment workflows inspect the target device&#8217;s supported operators, fuse layers when possible, reduce memory copies, and keep execution paths stable. In other words: the model should behave like a polite guest in the accelerator&#8217;s house.</p><p>A mobile AI stack in 2026 should answer questions like:</p><ul><li><p>Which layers execute on the NPU?</p></li><li><p>Which operators fall back to CPU?</p></li><li><p>How often are tensors copied between memory regions?</p></li><li><p>Is the runtime using a static graph or rebuilding execution repeatedly?</p></li><li><p>Are we paying hidden costs in preprocessing or postprocessing?</p></li></ul><p>These details are not glamorous, but they define whether a feature feels native or sluggish.</p><p><em><strong>Battery Life: The Real Product Requirement</strong></em></p><p>If latency is the visible constraint, battery is the emotional one.</p><p>Users will forgive a model that takes 150 ms instead of 80 ms. They will not forgive an app that quietly drains 18% of the battery while &#8220;helping.&#8221; That is how uninstallations are born.</p><p>The key insight from 2026 research is that battery impact is not controlled only by model compression. It is controlled just as much by when the model runs, how often it runs, and whether the system avoids wasteful wake-ups.</p><p>That means developers need to think beyond model math and into runtime behavior.</p><p>Here are the main battery levers:</p><ol><li><p><strong>Quantization, pruning, and distillation</strong></p><ul><li><p>These reduce compute and memory traffic.</p></li><li><p>Lower precision often means less power.</p></li><li><p>Fewer parameters usually means less work.</p></li></ul></li><li><p><strong>Opportunistic scheduling</strong></p><ul><li><p>Run heavier inference when the device is already active.</p></li><li><p>Prefer charging, idle, or foreground moments for non-urgent tasks.</p></li><li><p>Avoid waking the CPU and accelerator unnecessarily.</p></li></ul></li><li><p><strong>Batching</strong></p><ul><li><p>Combine multiple low-priority requests when possible.</p></li><li><p>One efficient inference can beat several tiny, repeated ones.</p></li></ul></li><li><p><strong>Adaptive inference</strong></p><ul><li><p>Use smaller models or shorter contexts when battery is low.</p></li><li><p>Scale quality based on thermal state, power state, or user importance.</p></li><li><p>Not every task deserves the flagship treatment.</p></li></ul></li><li><p><strong>Avoiding redundant work</strong></p><ul><li><p>Cache results when appropriate.</p></li><li><p>Don&#8217;t recompute if the input hasn&#8217;t changed.</p></li><li><p>Debounce event streams that trigger inference too often.</p></li></ul></li></ol><p>This is the part many teams overlook: a perfectly optimized model can still be a battery bully if the app keeps asking it to run every second. Sometimes the biggest optimization is simply running less often.</p><p><em><strong>A Practical Python Example: Adaptive Inference Scheduling</strong></em></p><p>Even though mobile deployment usually happens in Swift, Kotlin, Java, or JavaScript bindings, Python is useful for demonstrating the logic behind an energy-aware inference policy.</p><p>Here&#8217;s a simple example of adaptive scheduling for mobile inference jobs:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0a2f4f23-d09f-4b67-bc57-b9092514e510&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import time
from dataclasses import dataclass

@dataclass
class DeviceState:
    battery_level: int      # 0-100
    is_charging: bool
    thermal_status: str     # "normal", "warm", "hot"
    app_in_foreground: bool

def should_run_heavy_model(state: DeviceState) -&gt; bool:
    if state.is_charging:
        return True
    if state.battery_level &lt; 20:
        return False
    if state.thermal_status in ("warm", "hot"):
        return False
    if not state.app_in_foreground:
        return False
    return True

def run_inference(input_data, state: DeviceState):
    if should_run_heavy_model(state):
        model = "large_model"
        print(f"Running {model} on device...")
        # simulate heavy inference
        time.sleep(0.2)
        return {"model": model, "result": "high_accuracy_output"}
    else:
        model = "small_model"
        print(f"Running {model} on device...")
        # simulate lightweight inference
        time.sleep(0.05)
        return {"model": model, "result": "fast_fallback_output"}

# Example usage
state = DeviceState(
    battery_level=18,
    is_charging=False,
    thermal_status="normal",
    app_in_foreground=True
)

result = run_inference({"text": "summarize this"}, state)
print(result)</code></pre></div><p>This example is intentionally simple, but the pattern matters. Production apps can use similar logic to choose between model variants, lower-resolution inputs, shorter context windows, or deferred execution.</p><p>In real mobile systems, this policy may also depend on:</p><ul><li><p>device temperature,</p></li><li><p>user interaction urgency,</p></li><li><p>network availability,</p></li><li><p>and whether the output is time-sensitive.</p></li></ul><p>This is the future of mobile AI: not one model to rule them all, but a set of policies that decide how much intelligence to spend.</p><p><em><strong>Privacy: Local Does Not Automatically Mean Safe</strong></em></p><p>Now for the part that gets people excited in pitch decks: privacy.</p><p>Local inference reduces cloud exposure. That is real. If the user&#8217;s image, voice, location-adjacent context, or typed text never leaves the device, you reduce the amount of personal data in transit and stored on remote servers. That lowers network risk, reduces some compliance burden, and can strengthen user trust.</p><p>But local inference is not the same thing as automatic privacy.</p><p>A mobile AI feature can still leak data through:</p><ul><li><p>cached inputs,</p></li><li><p>model telemetry,</p></li><li><p>analytics events,</p></li><li><p>crash logs,</p></li><li><p>third-party SDKs,</p></li><li><p>local backups,</p></li><li><p>or overly permissive retention policies.</p></li></ul><p>In other words, data can still escape even if the model never makes a network request.</p><p>The strongest privacy posture in 2026 combines several practices:</p><ol><li><p><strong>Offline-first processing</strong></p><ul><li><p>Default to local handling whenever possible.</p></li><li><p>Do not require network access for core functionality.</p></li></ul></li><li><p><strong>Data minimization</strong></p><ul><li><p>Collect only what is necessary.</p></li><li><p>Avoid storing raw inputs unless absolutely needed.</p></li></ul></li><li><p><strong>Explicit retention controls</strong></p><ul><li><p>Define how long temporary data stays on device.</p></li><li><p>Clear caches and buffers predictably.</p></li></ul></li><li><p><strong>Transparent user consent</strong></p><ul><li><p>Tell users what is processed locally.</p></li><li><p>Clarify what, if anything, is sent elsewhere.</p></li></ul></li><li><p><strong>SDK and telemetry audits</strong></p><ul><li><p>Review all third-party integrations carefully.</p></li><li><p>A privacy-preserving model cannot save a chatty analytics library.</p></li></ul></li></ol><p>A lot of mobile AI products want the marketing benefit of &#8220;private by design&#8221; without the engineering discipline required to make that statement true. Users are getting better at noticing the gap.</p><p><em><strong>The Mobile AI Tooling Landscape Is Finally Maturing</strong></em></p><p>One of the most interesting shifts in 2026 is that teams are no longer assuming there is one universal framework that solves mobile AI deployment.</p><p>That is a healthy correction. Different tools now occupy distinct niches:</p><ul><li><p><strong>TensorFlow Lite</strong> remains strong for lightweight, efficient on-device execution.</p></li><li><p><strong>Core ML</strong> is the natural fit for Apple platforms and integrates well with the Apple ecosystem.</p></li><li><p><strong>ONNX Runtime</strong> is attractive for portability and cross-platform workflows.</p></li><li><p><strong>ExecuTorch</strong> is gaining attention for PyTorch-to-edge deployment paths.</p></li><li><p><strong>MediaPipe</strong> is excellent for real-time pipelines, especially vision and multimodal workflows.</p></li></ul><p>The important decision is not &#8220;which framework is best in the abstract?&#8221; It is:</p><ul><li><p>What platforms do we support?</p></li><li><p>What model architecture are we shipping?</p></li><li><p>What accelerator behavior do we need?</p></li><li><p>What does the developer workflow look like?</p></li><li><p>How painful is debugging in production?</p></li></ul><p>That last question matters more than people admit. A technically excellent runtime that is impossible to inspect will create a miserable maintenance burden. The best mobile AI stack is not just fast. It is debuggable.</p><p><em><strong>A JavaScript Example: Async Inference Without Freezing the UI</strong></em></p><p>When mobile AI shows up in client-side JavaScript, the same principle applies: don&#8217;t block the interface while running inference.</p><p>Here&#8217;s a simplified example using asynchronous logic for a client-side app:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;e0630cb9-ece6-498d-8f14-dd1c628fa2e3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">async function runInference(model, input) {
  // Pretend this is an on-device model call
  return new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      resolve({
        label: "person",
        confidence: 0.94
      });
    }, 120);
  });
}

async function handleCameraFrame(frame) {
  try {
    const result = await runInference("quantizedVisionModel", frame);
    console.log("Inference result:", result);
    // Update UI with result
  } catch (error) {
    console.error("Inference failed:", error);
  }
}

// Example usage
handleCameraFrame({ pixels: "..." });</code></pre></div><p>This is obviously a simplified sketch, but the lesson is real: inference should be asynchronous, non-blocking, and respectful of UI responsiveness. A smart mobile app feels immediate even while AI is working in the background.</p><p><em><strong>Engineering Patterns That Make Mobile AI Ship Well</strong></em></p><p>The research points to a practical truth: successful mobile AI is not just about model selection. It is about engineering discipline.</p><p>These are the patterns that separate &#8220;cool demo&#8221; from &#8220;shippable product&#8221;:</p><ol><li><p><strong>Load quantized models</strong></p><ul><li><p>Lower precision can improve performance and reduce memory pressure.</p></li></ul></li><li><p><strong>Warm up the session</strong></p><ul><li><p>Avoid cold-start stalls on the first user interaction.</p></li><li><p>Pre-initialize when the app is idle or launching.</p></li></ul></li><li><p><strong>Use async inference</strong></p><ul><li><p>Keep the UI thread free.</p></li><li><p>Let background work remain background work.</p></li></ul></li><li><p><strong>Constrain thread counts</strong></p><ul><li><p>More threads are not always better.</p></li><li><p>Over-parallelization can increase contention and energy use.</p></li></ul></li><li><p><strong>Watch memory carefully</strong></p><ul><li><p>Mobile devices are far less forgiving than desktops.</p></li><li><p>Memory spikes lead to jank, swaps, or process death.</p></li></ul></li><li><p><strong>Measure thermal and battery effects</strong></p><ul><li><p>Benchmark latency, but also power draw.</p></li><li><p>A fast model that overheats a device is not a win.</p></li></ul></li><li><p><strong>Add fallback modes</strong></p><ul><li><p>Use smaller or cheaper inference paths when needed.</p></li><li><p>Gracefully degrade instead of failing hard.</p></li></ul></li></ol><p>A lot of AI engineering still behaves like the team assumes the device is an infinite server hidden inside a phone case. It is not. The hardware has feelings. It will retaliate.</p><p><em><strong>What &#8220;Good&#8221; Looks Like in 2026</strong></em></p><p>The best mobile edge AI products in 2026 are defined by a few traits:</p><ul><li><p>They feel instant.</p></li><li><p>They respect battery life.</p></li><li><p>They work offline or degrade gracefully.</p></li><li><p>They keep sensitive data local by default.</p></li><li><p>They fit the accelerator instead of fighting it.</p></li><li><p>They are observable, maintainable, and easy to tune.</p></li></ul><p>The strategic shift is clear: mobile AI is no longer a novelty layer on top of app logic. It is becoming infrastructure. The winning teams are treating inference as a constrained resource, not a free utility.</p><p>That requires a mindset change.</p><p>Instead of asking:</p><ul><li><p>&#8220;Can we put this model on the phone?&#8221;</p></li></ul><p>Ask:</p><ul><li><p>&#8220;Can this model run predictably on the phone, at scale, without annoying the user or violating their trust?&#8221;</p></li></ul><p>That question is much harder. It is also the right one.</p><p><em><strong>Example Libraries and Services Worth Watching</strong></em></p><p>If you&#8217;re evaluating the mobile edge AI ecosystem, these are the names that keep showing up:</p><ul><li><p>TensorFlow Lite</p></li><li><p>Core ML</p></li><li><p>ONNX Runtime Mobile</p></li><li><p>ExecuTorch</p></li><li><p>MediaPipe</p></li><li><p>Qualcomm AI Engine / SNPE</p></li><li><p>Apple Neural Engine tooling</p></li><li><p>Google ML Kit</p></li><li><p>NVIDIA TensorRT for edge-adjacent workflows</p></li><li><p>PyTorch Mobile / ExecuTorch migration paths</p></li><li><p>Hugging Face model conversion and deployment workflows</p></li><li><p>Edge Impulse for embedded and mobile-adjacent edge AI</p></li><li><p>OpenVINO for certain edge and cross-device pipelines</p></li></ul><p>Each one has its own sweet spot. The right choice depends on the device target, model shape, and how much pain your team is willing to accept in exchange for performance. A very normal engineering decision, in other words.</p><p><em><strong>Closing Thoughts</strong></em></p><p>Edge AI on mobile in 2026 is not about cramming bigger models into smaller devices. It is about designing intelligence that respects the realities of the device: limited power, specialized hardware, user privacy, and the simple human expectation that an app should not behave like a hungry raccoon in the battery drawer.</p><p>The best teams will combine hardware-aware optimization, energy-aware scheduling, privacy-by-design data handling, and deployment tooling that makes the whole thing manageable. That is where the real advantage lives&#8212;not in hype, but in the quiet competence of systems that run locally, efficiently, and trustworthily.</p><p>Thanks for reading, and if you enjoyed this one, come back tomorrow for more sharp, practical takes from <strong>The Backend Developer</strong>. Stay curious, stay kind to your battery, and keep shipping things that don&#8217;t make users regret tapping &#8220;Allow.&#8221;</p>]]></content:encoded></item><item><title><![CDATA[Passkeys vs Passwords in 2026: UX, Security, and Migration Trade-offs]]></title><description><![CDATA[Passkeys vs Passwords in 2026: the Great Authentication Taste Test]]></description><link>https://thebackenddevelopers.substack.com/p/passkeys-vs-passwords-in-2026-ux</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/passkeys-vs-passwords-in-2026-ux</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Wed, 22 Jul 2026 07:26:54 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/208024388/5610e6925632345cf4b236dafb4dd677.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>Passwords have had a long and frankly overpaid career.</p><p>They&#8217;ve been our digital doormen, our &#8220;forgot password?&#8221; rabbit holes, and the reason every support team on Earth has at least one tired soul saying, &#8220;Have you tried resetting it?&#8221;</p><p>Passkeys arrived with a shiny promise: fewer phishing attacks, less credential stuffing, less password reuse, and fewer moments where users type <code>Summer2026!</code> into yet another login box like it&#8217;s a sacred rite.</p><p>But in 2026, the real story is more interesting than &#8220;new thing good, old thing bad.&#8221; Passkeys are not just a security upgrade. They are a product decision, a UX decision, a migration decision, and&#8212;depending on how you roll them out&#8212;a support ticket generator or a conversion booster.</p><p>Let&#8217;s unpack the trade-offs properly.</p><p><em><strong>Why Passwords Became the Problem We All Pretend Not to See</strong></em></p><p>Passwords fail in predictable ways.</p><p>Users reuse them across sites. Attackers steal them in breaches. Phishing pages harvest them with alarming efficiency. &#8220;Strong&#8221; passwords often end up stored in browser notes, email drafts, or memory locations that are best described as spiritual.</p><p>The fundamental issue is that passwords are shared secrets. If someone learns the secret, they can impersonate the user. Once that secret leaks, the system has very little defense left.</p><p>That&#8217;s why credential stuffing works so well. If a password is exposed on one site, attackers try it everywhere else. The user did not &#8220;get hacked&#8221; in a cinematic way; they simply carried one weak identity key across the internet like a luggage tag with poor judgment.</p><p>Passkeys change the model. Instead of a shared secret, they use public-key cryptography. The private key stays on the user&#8217;s device or synced secure account storage. The website gets only the public key. Even if the server is breached, the attacker doesn&#8217;t get a reusable credential they can replay elsewhere.</p><p>That is a major security shift.</p><p><em><strong>What a Passkey Actually Is</strong></em></p><p>A passkey is a phishing-resistant credential based on WebAuthn and FIDO standards. When a user registers a passkey, their device creates a key pair:</p><ul><li><p>a private key, kept secure on the device or synced credential store</p></li><li><p>a public key, stored by the service</p></li></ul><p>During login, the server sends a challenge. The user approves the login on their trusted device using biometrics, device PIN, or another local user verification method. The device signs the challenge with the private key. The server verifies the signature using the stored public key.</p><p>Important point: the website never sees the private key.</p><p>This matters because phishing becomes much harder. A fake site cannot simply trick the user into typing a secret, because there is no secret to type. The authenticator checks the origin and ensures the signed response is bound to the correct relying party.</p><p>In practical terms: a passkey cannot be &#8220;replayed&#8221; into a lookalike login form the way a password can.</p><p><em><strong>Security: The Strongest Argument for Passkeys</strong></em></p><p>From a security perspective, passkeys are a clear improvement over passwords in the threat categories that matter most:</p><ul><li><p>phishing</p></li><li><p>credential stuffing</p></li><li><p>password reuse</p></li><li><p>password database exposure</p></li><li><p>weak password selection</p></li><li><p>brute-force guessing at scale</p></li></ul><p>They are especially good against modern attacks because the attacker&#8217;s favorite trick&#8212;getting the user to hand over a credential&#8212;mostly stops working.</p><p>That said, the security benefit is not automatic just because a product &#8220;supports passkeys.&#8221;</p><p>The research pattern is consistent: the strongest gains come when passkeys are the primary path, not a decorative extra. If your login page still screams &#8220;enter password first&#8221; and passkeys are buried under a link labeled &#8220;try another way,&#8221; adoption lags and security gains weaken.</p><p>Why?</p><p>Because users choose the path of least resistance. If the password option remains the default mental model, many people will continue using it. The result is a half-modern authentication system with old failure modes still firmly in charge.</p><p>In other words, if passkeys are the guest star and passwords are the lead actor, the plot does not change much.</p><p><em><strong>The UX Win Is Real, But Not Automatic</strong></em></p><p>This is where the conversation gets more interesting.</p><p>A lot of teams assumed passkeys would instantly improve UX because &#8220;fewer passwords = less friction.&#8221; That&#8217;s true only in the narrowest sense.</p><p>The old friction was typing passwords. The new friction is cognitive and cross-platform:</p><ul><li><p>&#8220;Should I save this passkey?&#8221;</p></li><li><p>&#8220;Why is my phone asking me to approve login on my laptop?&#8221;</p></li><li><p>&#8220;Why does this look different on Windows than on my iPhone?&#8221;</p></li><li><p>&#8220;What happens if I lose my device?&#8221;</p></li><li><p>&#8220;Is this passkey synced or device-bound?&#8221;</p></li><li><p>&#8220;Why do I need to use a nearby device I don&#8217;t recognize?&#8221;</p></li><li><p>&#8220;Why did it open iCloud Keychain / Google Password Manager / Windows Hello?&#8221;</p></li></ul><p>These are not small questions. They are the new user experience battlefield.</p><p>Passkeys can be delightful when the product explains them well:</p><ul><li><p>clear naming</p></li><li><p>clear prompts</p></li><li><p>clear recovery</p></li><li><p>clear device trust cues</p></li><li><p>clear enrollment moments</p></li></ul><p>But if the product is vague, users feel like they are being asked to join a secret society with inconsistent membership rules.</p><p>That means UX design is not a cosmetic concern here. It is core infrastructure.</p><p><em><strong>The Mental Model Problem</strong></em></p><p>The biggest UX challenge in 2026 is not entering a password. It&#8217;s understanding what the user is actually approving.</p><p>Users often do not have a clean mental model for:</p><ul><li><p>device-bound vs synced credentials</p></li><li><p>what &#8220;saving a passkey&#8221; means</p></li><li><p>whether their passkey follows them across devices</p></li><li><p>what happens if a device is stolen</p></li><li><p>why a platform asks for biometrics on one device and a PIN on another</p></li></ul><p>If you want passkeys to succeed, you must explain them like a product feature, not like a cryptography paper slipped into onboarding.</p><p>Good UX means:</p><ul><li><p>avoid jargon</p></li><li><p>explain the benefit in user language</p></li><li><p>present the passkey as &#8220;a faster, safer way to sign in&#8221;</p></li><li><p>show exactly when and where it&#8217;s being stored</p></li><li><p>make recovery obvious, not mythical</p></li></ul><p>This is one of those moments where design and security are married whether they like it or not.</p><p><em><strong>Migration: Not a Flip, a Journey</strong></em></p><p>Let&#8217;s be very clear: migrating from passwords to passkeys is not a one-day cutoff event.</p><p>It is a phased rollout.</p><p>Trying to force all users into passkeys overnight is how you create support spikes, confused enterprise admins, abandoned accounts, and a very animated Slack channel at 2:14 a.m.</p><p>The most successful migrations use a staged approach:</p><ol><li><p><strong>Let users sign in with the current method first</strong></p><ul><li><p>establish trust</p></li><li><p>reduce immediate friction</p></li><li><p>avoid blocking edge cases</p></li></ul></li><li><p><strong>Offer passkey enrollment after a successful login</strong></p><ul><li><p>this is the right moment</p></li><li><p>the user is already authenticated</p></li><li><p>the product can explain the value</p></li></ul></li><li><p><strong>Encourage progressively</strong></p><ul><li><p>gentle prompts</p></li><li><p>value-based messaging</p></li><li><p>not a pop-up with the emotional energy of a tax audit</p></li></ul></li><li><p><strong>Preserve recovery and fallback</strong></p><ul><li><p>lost devices happen</p></li><li><p>sync can fail</p></li><li><p>browsers can be unsupported</p></li><li><p>enterprise environments can be locked down</p></li></ul></li><li><p><strong>Instrument everything</strong></p><ul><li><p>enrollment success rate</p></li><li><p>authentication success rate</p></li><li><p>fallback usage</p></li><li><p>recovery completion</p></li><li><p>support contact reasons</p></li></ul></li></ol><p>The key idea: migration is a product and risk-management problem, not just an engineering ticket.</p><p><em><strong>Why Fallbacks Are Necessary, But Dangerous</strong></em></p><p>Fallbacks are a necessary evil during transition, but they must be designed carefully.</p><p>If passwords remain too prominent, people will continue using them indefinitely. That weakens the security upside and makes passkeys feel optional. Optional features are where adoption goes to nap.</p><p>On the other hand, removing fallback too early can create serious pain:</p><ul><li><p>users lose access after device loss</p></li><li><p>enterprise-managed endpoints may not allow sync</p></li><li><p>some environments lack compatible browsers</p></li><li><p>users may not understand recovery procedures</p></li></ul><p>The goal is not &#8220;no fallback ever.&#8221; The goal is:</p><ul><li><p>make passkeys the default</p></li><li><p>make recovery trustworthy</p></li><li><p>make password fallback progressively less central</p></li></ul><p>That&#8217;s the balance.</p><p><em><strong>Implementation: Mature Enough to Be Dangerous</strong></em></p><p>From a backend standpoint, passkeys are now quite implementable. Python teams in particular have solid WebAuthn options and auth libraries that can handle the core registration and assertion flows.</p><p>But the hard part is not &#8220;can I code it?&#8221; The hard part is &#8220;can I run it correctly in production for a million users with weird devices and worse habits?&#8221;</p><p>The important backend concerns include:</p><ul><li><p>challenge generation and one-time use</p></li><li><p>origin validation</p></li><li><p>RP ID validation</p></li><li><p>attestation policy decisions</p></li><li><p>credential storage</p></li><li><p>signature verification</p></li><li><p>session binding after successful assertion</p></li><li><p>error handling for partial or failed flows</p></li><li><p>telemetry for enrollment and login behavior</p></li></ul><p>If any of those are sloppy, the security story gets weaker fast.</p><p>The implementation itself may look simple on a whiteboard. Production reliability is where the dragons live.</p><p><em><strong>Python Example: A Simplified Passkey Registration and Authentication Flow</strong></em></p><p>Below is a simplified conceptual example using Python and a WebAuthn library style flow. Actual implementation details vary by framework and library, but this shows the structure.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;08da1e71-8eae-4516-b7a8-650aeab72b7c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Simplified example: conceptual WebAuthn flow in Python
# Libraries vary, but this illustrates the registration/authentication pattern.

from os import urandom
from base64 import urlsafe_b64encode
from flask import Flask, request, session, jsonify

app = Flask(__name__)
app.secret_key = "replace-with-a-real-secret"

RP_ID = "example.com"
ORIGIN = "https://example.com"

users = {}
credentials = {}

def generate_challenge():
    return urlsafe_b64encode(urandom(32)).decode("utf-8")

@app.route("/webauthn/register/options", methods=["POST"])
def register_options():
    user_id = request.json["user_id"]
    challenge = generate_challenge()
    session["registration_challenge"] = challenge

    return jsonify({
        "rp": {"name": "Example App", "id": RP_ID},
        "user": {"id": user_id, "name": request.json["email"], "displayName": request.json["name"]},
        "challenge": challenge,
        "pubKeyCredParams": [{"type": "public-key", "alg": -7}]  # ES256
    })

@app.route("/webauthn/register/verify", methods=["POST"])
def register_verify():
    client_data = request.json["client_data"]
    attestation = request.json["attestation"]

    expected_challenge = session.get("registration_challenge")
    if not expected_challenge:
        return jsonify({"error": "missing challenge"}), 400

    # In real code:
    # - verify challenge
    # - verify origin == ORIGIN
    # - verify RP ID
    # - verify attestation/response
    # - store credential public key + credential ID
    # - bind to user
    credential_id = attestation["credential_id"]
    public_key = attestation["public_key"]
    user_id = request.json["user_id"]

    credentials[credential_id] = {
        "user_id": user_id,
        "public_key": public_key,
        "sign_count": 0
    }

    return jsonify({"status": "ok"})

@app.route("/webauthn/login/options", methods=["POST"])
def login_options():
    email = request.json["email"]
    challenge = generate_challenge()
    session["login_challenge"] = challenge

    # In real code, fetch user's registered credential IDs
    allowed_credentials = [
        {"type": "public-key", "id": cred_id}
        for cred_id, data in credentials.items()
        if users.get(data["user_id"], {}).get("email") == email
    ]

    return jsonify({
        "challenge": challenge,
        "rpId": RP_ID,
        "allowCredentials": allowed_credentials,
        "userVerification": "required"
    })

@app.route("/webauthn/login/verify", methods=["POST"])
def login_verify():
    expected_challenge = session.get("login_challenge")
    if not expected_challenge:
        return jsonify({"error": "missing challenge"}), 400

    credential_id = request.json["credential_id"]
    assertion = request.json["assertion"]

    stored = credentials.get(credential_id)
    if not stored:
        return jsonify({"error": "unknown credential"}), 400

    # In real code:
    # - verify challenge
    # - verify origin
    # - verify signature using stored public key
    # - verify sign count
    # - establish session
    session["user_id"] = stored["user_id"]
    return jsonify({"status": "authenticated"})</code></pre></div><p>This is intentionally simplified. Real WebAuthn code must perform cryptographic verification and strict origin/RP checks. But the structure is the important part:</p><ul><li><p>generate challenge</p></li><li><p>store challenge temporarily</p></li><li><p>verify response</p></li><li><p>validate origin and relying party</p></li><li><p>store credential securely</p></li><li><p>bind the session after successful auth</p></li></ul><p>Passkeys are not magic. They are carefully validated state transitions with better security properties.</p><p><em><strong>Client-Side UX Example: Clear Passkey Enrollment Prompt in JavaScript</strong></em></p><p>On the client side, the biggest job is making the flow understandable and calm.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;6e99cfb7-3cd6-4d67-9148-2a2f8c1684bc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">async function createPasskey() {
  try {
    // 1. Ask backend for options
    const optionsResponse = await fetch("/webauthn/register/options", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        user_id: "12345",
        email: "user@example.com",
        name: "Ariana Example"
      })
    });

    const options = await optionsResponse.json();

    // 2. Convert base64 challenge to ArrayBuffer in real implementation
    // 3. Call WebAuthn API
    const credential = await navigator.credentials.create({
      publicKey: {
        challenge: Uint8Array.from(atob(options.challenge.replace(/-/g, '+').replace(/_/g, '/')), c =&gt; c.charCodeAt(0)),
        rp: options.rp,
        user: {
          id: Uint8Array.from("12345", c =&gt; c.charCodeAt(0)),
          name: "user@example.com",
          displayName: "Ariana Example"
        },
        pubKeyCredParams: [{ type: "public-key", alg: -7 }],
        authenticatorSelection: {
          userVerification: "required"
        },
        timeout: 60000,
        attestation: "none"
      }
    });

    // 4. Send result to backend for verification
    await fetch("/webauthn/register/verify", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        user_id: "12345",
        client_data: "serialized-client-data",
        attestation: {
          credential_id: "credential-id-from-response",
          public_key: "public-key-from-response"
        }
      })
    });

    alert("Passkey created successfully.");
  } catch (error) {
    console.error(error);
    alert("Passkey setup was cancelled or failed.");
  }
}</code></pre></div><p>In a real product, you would add much better conversion handling, parsing, and error recovery. But the UX principle is what matters: keep the prompt clear, explain why the user is doing this, and provide a way out if they cancel.</p><p><em><strong>Device-Bound vs Synced: The New Confusion Layer</strong></em></p><p>One of the biggest changes in 2026 is that users are no longer just dealing with &#8220;a password.&#8221; They&#8217;re dealing with a credential ecosystem.</p><p>Passkeys may be:</p><ul><li><p>device-bound</p></li><li><p>synced across devices through platform credential managers</p></li><li><p>managed by enterprise tools</p></li><li><p>stored in third-party authenticators</p></li></ul><p>Users usually do not think in these terms. They think:</p><ul><li><p>&#8220;Will this work on my phone?&#8221;</p></li><li><p>&#8220;Will I still have access on my laptop?&#8221;</p></li><li><p>&#8220;What if I switch ecosystems?&#8221;</p></li><li><p>&#8220;Why does this only work on one device?&#8221;</p></li></ul><p>Cross-platform support is better than it used to be, but platform-specific behavior still matters. iCloud Keychain, Google Password Manager, Windows Hello, and third-party authenticators all shape the user experience differently.</p><p>That means your login flow has to account for:</p><ul><li><p>recognition of the user&#8217;s current environment</p></li><li><p>clear recovery options</p></li><li><p>sane prompts for saving credentials</p></li><li><p>graceful fallback when an environment is unsupported</p></li></ul><p>The product challenge is no longer just authentication. It&#8217;s credential choreography.</p><p><em><strong>Enterprise and Unsupported Environments Still Exist, Shockingly</strong></em></p><p>The internet is not just a world of shiny personal phones with updated operating systems and perfect biometric sensors.</p><p>There are managed endpoints. There are outdated browsers. There are locked-down corporate machines. There are users who cannot sync credentials across personal and work devices. There are accessibility requirements that change the flow. There are regions and device ecosystems with different behavior.</p><p>This is why a forced cutover is risky.</p><p>A good passkey strategy has to respect real-world variance:</p><ul><li><p>allow phased adoption</p></li><li><p>recognize incompatible environments</p></li><li><p>support recovery for lost or unavailable devices</p></li><li><p>avoid assuming every user has the same platform capabilities</p></li></ul><p>The teams that win in 2026 are not the ones with the fanciest demo. They&#8217;re the ones with the best edge-case handling.</p><p><em><strong>Choosing a Provider: A UX and Policy Decision, Not Just a Technical One</strong></em></p><p>Hosted identity platforms and auth providers have made passkey adoption much easier.</p><p>Examples include:</p><ul><li><p>Auth0</p></li><li><p>Clerk</p></li><li><p>Firebase Authentication</p></li><li><p>Amazon Cognito</p></li><li><p>Duo</p></li><li><p>Okta</p></li></ul><p>These platforms reduce implementation time and give you a faster path to production.</p><p>But there&#8217;s a catch: provider support varies in important ways:</p><ul><li><p>enrollment UX quality</p></li><li><p>fallback model</p></li><li><p>admin controls</p></li><li><p>device sync assumptions</p></li><li><p>recovery options</p></li><li><p>reporting/telemetry</p></li><li><p>enterprise policy behavior</p></li></ul><p>So picking a provider is not just &#8220;which SDK is easiest?&#8221;</p><p>It&#8217;s also:</p><ul><li><p>how gracefully does it handle passkey enrollment?</p></li><li><p>how much control do we have over fallback?</p></li><li><p>can we support enterprise restrictions?</p></li><li><p>how good are the recovery workflows?</p></li><li><p>can we instrument adoption and failures?</p></li></ul><p>In 2026, your auth provider is part product layer, part policy engine, part user experience.</p><p><em><strong>The Migration Playbook That Actually Works</strong></em></p><p>If I had to summarize the successful migration pattern in one sentence, it would be this:</p><p>Make passkeys the best choice, not the only surprise.</p><p>A sane rollout usually looks like:</p><ul><li><p><strong>Phase 1: Support passkeys</strong></p><ul><li><p>add registration and login flows</p></li><li><p>keep existing login intact</p></li></ul></li><li><p><strong>Phase 2: Encourage enrollment after trust-building moments</strong></p><ul><li><p>post-login prompts</p></li><li><p>onboarding nudges</p></li><li><p>&#8220;make sign-in easier next time&#8221; messaging</p></li></ul></li><li><p><strong>Phase 3: Reduce password prominence</strong></p><ul><li><p>move passkeys to the default path</p></li><li><p>keep passwords as a fallback where needed</p></li></ul></li><li><p><strong>Phase 4: Measure and refine</strong></p><ul><li><p>watch completion rates</p></li><li><p>track cancellations</p></li><li><p>monitor support volume</p></li><li><p>adjust copy and recovery</p></li></ul></li></ul><p>The biggest mistake is treating passkeys as a checkbox feature. &#8220;We added support&#8221; is not the same as &#8220;users adopted it.&#8221;</p><p>The adoption curve depends on:</p><ul><li><p>timing</p></li><li><p>clarity</p></li><li><p>confidence</p></li><li><p>recovery trust</p></li><li><p>platform behavior</p></li><li><p>fallback design</p></li></ul><p>Security tools only help if humans actually use them.</p><p><em><strong>Where Passkeys Beat Passwords Most Clearly</strong></em></p><p>Passkeys are especially strong when your product has one or more of these characteristics:</p><ul><li><p>consumer accounts with frequent phishing exposure</p></li><li><p>high-value accounts</p></li><li><p>support costs from password resets</p></li><li><p>users with repeated credential reuse problems</p></li><li><p>modern browser/device mix</p></li><li><p>a desire to reduce authentication friction over time</p></li></ul><p>They are also compelling if your team wants:</p><ul><li><p>lower account takeover rates</p></li><li><p>better trust posture</p></li><li><p>less password reset traffic</p></li><li><p>cleaner sign-in experiences</p></li></ul><p>Passwords still have one weird strength: universality. Everyone understands them. Everyone has used them. Almost every system can support them.</p><p>Passkeys are better, but they require better product design.</p><p><em><strong>Where Passwords Still Hang Around Like an Old Couch</strong></em></p><p>Despite all the progress, passwords don&#8217;t vanish easily.</p><p>They remain useful when:</p><ul><li><p>a user is on an unsupported device</p></li><li><p>an enterprise environment blocks sync or biometrics</p></li><li><p>recovery must be immediate and low-friction</p></li><li><p>backward compatibility is necessary</p></li><li><p>you&#8217;re in the middle of migration, not the end</p></li></ul><p>So the strategic question is not &#8220;passkeys or passwords forever?&#8221;</p><p>It&#8217;s:</p><ul><li><p>where should passkeys be the default?</p></li><li><p>where should passwords be allowed temporarily?</p></li><li><p>how do we phase out dependency without breaking access?</p></li></ul><p>That&#8217;s the adult version of authentication strategy.</p><p><em><strong>Practical Recommendations for Product and Engineering Teams</strong></em></p><p>If you&#8217;re building in 2026, here&#8217;s the blunt version:</p><ol><li><p><strong>Make passkeys the primary option</strong></p><ul><li><p>don&#8217;t bury them</p></li><li><p>don&#8217;t make them feel experimental</p></li></ul></li><li><p><strong>Keep recovery strong</strong></p><ul><li><p>lost-device flows</p></li><li><p>backup access</p></li><li><p>support-assisted recovery where appropriate</p></li></ul></li><li><p><strong>Design the UI like a translator</strong></p><ul><li><p>explain what&#8217;s happening</p></li><li><p>reduce jargon</p></li><li><p>use stable naming</p></li></ul></li><li><p><strong>Phase migration carefully</strong></p><ul><li><p>enroll after login</p></li><li><p>encourage progressively</p></li><li><p>track adoption</p></li></ul></li><li><p><strong>Instrument everything</strong></p><ul><li><p>success rates</p></li><li><p>failure reasons</p></li><li><p>platform splits</p></li><li><p>fallback usage</p></li></ul></li><li><p><strong>Treat auth as product, not plumbing</strong></p><ul><li><p>because users do</p></li></ul></li></ol><p><em><strong>The Bottom Line</strong></em></p><p>Passkeys are the better authentication model for 2026. They materially reduce phishing, credential reuse, and the chronic absurdity of password management.</p><p>But their success is not guaranteed by cryptography alone.</p><p>The best results come when teams:</p><ul><li><p>make passkeys the default path</p></li><li><p>explain them clearly</p></li><li><p>support recovery thoughtfully</p></li><li><p>migrate in phases</p></li><li><p>avoid forcing users into brittle cutovers</p></li><li><p>choose providers based on UX and policy behavior, not just SDK convenience</p></li></ul><p>So yes, passwords are on borrowed time. But the transition is not a funeral; it&#8217;s a systems redesign.</p><p>And if you do it well, users won&#8217;t think, &#8220;Wow, what a marvelous authentication architecture.&#8221;</p><p>They&#8217;ll think, &#8220;Huh. That was easy.&#8221;</p><p>Which, in product, is basically a standing ovation.</p><p><em><strong>References and Examples Worth Exploring</strong></em></p><p>If you&#8217;re evaluating libraries or services that support passkeys / WebAuthn, take a look at:</p><ul><li><p><strong>Auth0</strong> &#8212; passkey support with hosted identity flows</p></li><li><p><strong>Clerk</strong> &#8212; modern auth UI and passkey-friendly sign-in experiences</p></li><li><p><strong>Firebase Authentication</strong> &#8212; ecosystem-friendly auth integration</p></li><li><p><strong>Amazon Cognito</strong> &#8212; enterprise-oriented identity workflows</p></li><li><p><strong>Duo</strong> &#8212; strong security and authentication options</p></li><li><p><strong>Okta</strong> &#8212; identity platform with enterprise admin controls</p></li><li><p><strong>python-fido2</strong> &#8212; Python library for FIDO2/WebAuthn</p></li><li><p><strong>Yubico WebAuthn / FIDO tooling</strong> &#8212; device and auth ecosystem support</p></li><li><p><strong>SimpleWebAuthn</strong> &#8212; popular WebAuthn tooling for app developers</p></li></ul><p><em><strong>Warm Signoff</strong></em></p><p>That&#8217;s the passkey story for 2026: better security, better UX potential, and a migration path that rewards patience and clarity.</p><p>If you enjoyed this, come back tomorrow for more practical frontend and product-minded engineering wisdom from <strong>The Backend Developer</strong>.<br>Until then, keep shipping, keep learning, and please, for the love of all things digital, retire one password at a time.</p>]]></content:encoded></item><item><title><![CDATA[Adaptive Rate Limiting in Distributed APIs: Fairness, Backpressure, and Burst Control]]></title><description><![CDATA[Why Rate Limiting Is Really a Traffic Cop, a Diplomatically Fair Judge, and Sometimes a Very Tired Thermostat]]></description><link>https://thebackenddevelopers.substack.com/p/adaptive-rate-limiting-in-distributed</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/adaptive-rate-limiting-in-distributed</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Fri, 17 Jul 2026 01:01:49 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/207364678/58738aac059ce8b07e11a5517b9a86c3.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>If you&#8217;ve ever run a distributed API in production, you already know the truth: traffic does not arrive politely in a neat, well-labeled queue wearing a name tag.</p><p>It arrives in waves.</p><p>It arrives from that one customer whose batch job is &#8220;just a little aggressive.&#8221;</p><p>It arrives from retries, timeouts, client bugs, dashboard refreshes, mobile apps on bad networks, and the occasional internet event where everyone suddenly decides your API is now the center of the universe.</p><p>That is why adaptive rate limiting matters. Not as a single defense mechanism, but as a control system. A distributed API is not a static machine; it is a living, noisy ecosystem. The goal is not simply to say &#8220;no&#8221; to excess traffic. The goal is to say &#8220;yes&#8221; fairly, absorb bursts intelligently, and apply backpressure before your system starts wheezing like an overworked espresso machine.</p><div><hr></div><p><em><strong>Rate Limiting Is Not One Problem</strong></em></p><p>A lot of teams treat rate limiting like a simple guardrail: set a number, enforce it, move on.</p><p>In practice, it solves at least four different problems:</p><ol><li><p><strong>Fairness</strong> &#8212; who gets capacity when demand exceeds supply?</p></li><li><p><strong>Backpressure</strong> &#8212; how do you slow traffic before downstream services collapse?</p></li><li><p><strong>Burst control</strong> &#8212; how do you allow normal spikes without punishing healthy usage?</p></li><li><p><strong>Distributed consistency</strong> &#8212; how do you enforce limits across many API instances without turning every request into a coordination ceremony?</p></li></ol><p>Those are different questions, and using one blunt tool for all of them usually creates new pain somewhere else.</p><p>A strict fixed ceiling might protect a database, but it can also punish legitimate bursts. A permissive burst-friendly algorithm might keep users happy, but it can also let one noisy tenant crowd out everyone else. A local in-memory counter is fast, but across multiple instances it can be wildly inconsistent. A globally shared counter is accurate, but if you overdo coordination, your rate limiter becomes its own bottleneck.</p><p>So the real challenge is not &#8220;which algorithm is best?&#8221; It is &#8220;what policy are we trying to enforce, under what constraints, and how do we make it adaptive enough to survive reality?&#8221;</p><div><hr></div><p><em><strong>Fairness Is a Policy Choice, Not Just an Algorithm</strong></em></p><p>This is the part many teams discover the hard way.</p><p>Token bucket and sliding window algorithms are popular because they are simple, understandable, and burst-friendly. They let a client send traffic at a steady average rate while still consuming a temporary burst budget. That is excellent for normal human behavior, because humans are spiky. Users click. Jobs batch. Mobile apps reconnect. Webhooks arrive in clumps.</p><p>But simplicity has a social cost.</p><p>If you let everyone draw from the same pool, the loudest tenant often wins by accident. This is the classic noisy-neighbor problem. One customer with a runaway integration can consume enough burst allowance to make everyone else feel like the system is &#8220;slow,&#8221; even though the system is merely being hogged.</p><p>That is why fairness is not an automatic outcome of an algorithm. Fairness is a policy decision.</p><p>You can express that policy in several ways:</p><ul><li><p><strong>Per-tenant quotas</strong>: each customer gets their own bucket or limit.</p></li><li><p><strong>Per-user or per-client limits</strong>: fairness is enforced closer to the identity of the caller.</p></li><li><p><strong>Priority classes</strong>: paid or critical traffic gets preferential treatment.</p></li><li><p><strong>Weighted scheduling</strong>: one class can receive more capacity than another, but not unlimited dominance.</p></li></ul><p>Weighted fair queuing and related scheduler-style approaches are more sophisticated because they distribute service more intentionally. Instead of merely counting requests, they act more like an operating system scheduler: preserving a balance across traffic classes and helping priority traffic survive contention.</p><p>This matters most in multi-tenant APIs, internal platform APIs, and anything with service tiers. If your product has &#8220;basic,&#8221; &#8220;pro,&#8221; and &#8220;enterprise,&#8221; then fairness is part of the business model, not just the infrastructure.</p><div><hr></div><p><em><strong>Burst Tolerance: The Art of Letting People Be Human</strong></em></p><p>A rate limiter that cannot tolerate bursts is a rate limiter that does not understand reality.</p><p>Most legitimate workloads are bursty. A user opens a dashboard and 14 widgets wake up at once. A nightly sync job starts. A mobile app reconnects after leaving a tunnel. A webhook sender retries because a packet sneezed at the wrong moment. Burstiness is not abuse by default; often it is just life.</p><p>That is why token bucket remains such a beloved primitive.</p><p>Here&#8217;s the intuition:</p><ul><li><p>You refill tokens over time.</p></li><li><p>Each request consumes a token.</p></li><li><p>If you have spare tokens, bursts are allowed.</p></li><li><p>If not, requests wait or get rejected.</p></li></ul><p>This gives you a nice balance between long-term fairness and short-term flexibility.</p><p>The danger is when burst allowance is too generous and there is no smoothing. Then your system may admit a large burst, only to dump too much work into queues downstream. That is where tail latency spikes appear, and tail latency is the kind of thing that makes operators stare at dashboards with a thousand-yard gaze.</p><p>The best burst control systems distinguish between:</p><ul><li><p><strong>Short spikes</strong> that should be absorbed</p></li><li><p><strong>Sustained overload</strong> that should be throttled</p></li></ul><p>That distinction is crucial. A spike is a moment. Overload is a trend.</p><p>Adaptive throttling and dynamic quotas help here. Instead of locking limits forever, the system can adjust based on observed capacity, current health, or tenant behavior. If downstream services are healthy, limits can stay generous. If the system is under stress, limits can tighten gradually instead of collapsing into a hard outage.</p><div><hr></div><p><em><strong>Distributed Enforcement: Local Speed, Global Truth</strong></em></p><p>In a single-process app, rate limiting is almost embarrassingly easy. Put a counter in memory, check it, move on.</p><p>In distributed APIs, that approach has the structural integrity of a paper hat in a thunderstorm.</p><p>If your API is running across multiple instances, each instance only sees a slice of the traffic. A per-process limit can be bypassed simply by routing requests across more pods. That is why distributed rate limiting usually needs layered enforcement.</p><p>The most common pattern is:</p><ol><li><p><strong>Local enforcement at the edge or gateway</strong></p></li><li><p><strong>Shared state for global consistency</strong></p></li><li><p><strong>Policy centralization with distributed execution</strong></p></li></ol><p>This hybrid model is popular for a reason. Local checks are fast and cheap. They protect your app from obvious abuse without making every request pay a round-trip tax to a central coordinator. Shared state&#8212;often Redis, sometimes another distributed store&#8212;lets all instances agree on the broader picture.</p><p>That balance matters.</p><p>If you push too much logic into local memory, you get speed but lose consistency. If you centralize everything, you get consistency but risk creating a bottleneck or single point of pain. The hybrid approach gives you the best chance of staying both fast and honest.</p><p>This is why many production systems place rate limiting at the API gateway, ingress proxy, or service mesh edge. The traffic gets checked early, close to where it enters the system, before it fans out into more expensive work.</p><div><hr></div><p><em><strong>Backpressure: The Missing Conversation Between Systems</strong></em></p><p>Rate limiting is often discussed as if it were purely a rejection mechanism.</p><p>That is a mistake.</p><p>A mature system does not merely say &#8220;429 Too Many Requests&#8221; and wash its hands. It tries to shape demand so the upstream caller learns what the system can handle right now.</p><p>That is backpressure.</p><p>Backpressure is the polite version of &#8220;please slow down before everyone has a bad day.&#8221;</p><p>When rate limiting and backpressure are not coordinated, you can accidentally create retry storms. A client gets throttled, retries aggressively, hits the same limit again, and suddenly your &#8220;protection&#8221; layer has become a congestion amplifier. If the system is also timing out under load, those retries multiply like rabbits with engineering degrees.</p><p>The right design coordinates several mechanisms together:</p><ul><li><p><strong>Rate limits</strong> to cap admission</p></li><li><p><strong>Exponential backoff</strong> to spread retries out</p></li><li><p><strong>Timeouts</strong> to prevent dead hangs</p></li><li><p><strong>Circuit breakers</strong> to stop repeated failing calls</p></li><li><p><strong>Queue-aware admission control</strong> to avoid overfilling downstream queues</p></li></ul><p>This is important because overload is often a feedback problem. If the system starts slowing down, clients may retry more aggressively. Those retries increase load. Increased load slows the system further. And now you have a loop from which everyone learns humility.</p><p>The goal is to break that loop early.</p><p>Good backpressure propagates signals upstream before queues become graveyards of useful latency.</p><div><hr></div><p><em><strong>Adaptive Rate Limiting as Feedback Control</strong></em></p><p>This is where the topic stops being &#8220;an API feature&#8221; and becomes &#8220;a control system.&#8221;</p><p>A static threshold assumes capacity is fixed and traffic patterns are predictable. Neither assumption survives long in production.</p><p>An adaptive rate limiter watches signals such as:</p><ul><li><p>request volume</p></li><li><p>queue depth</p></li><li><p>latency percentiles</p></li><li><p>error rates</p></li><li><p>downstream saturation</p></li><li><p>tenant-specific behavior</p></li></ul><p>Then it adjusts limits dynamically.</p><p>Think of this as closed-loop control:</p><ul><li><p>If latency rises, reduce admission.</p></li><li><p>If downstream health improves, relax limits.</p></li><li><p>If one tenant becomes noisy, clamp their share without punishing the whole platform.</p></li><li><p>If a burst looks legitimate and the system is healthy, allow it.</p></li><li><p>If the burst persists and the queues grow, tighten the screws.</p></li></ul><p>This is much more practical than pretending a single limit will be correct forever.</p><p>Adaptive systems do not eliminate policy. They make policy responsive.</p><p>And yes, this is where some teams get nervous, because &#8220;dynamic&#8221; sounds like &#8220;harder to reason about.&#8221; That concern is valid. But static limits also hide complexity; they just hide it until the wrong day.</p><p>The trick is to keep the control logic understandable, observable, and bounded. Dynamic does not mean chaotic. It means the system has enough situational awareness to avoid being stupid at scale.</p><div><hr></div><p><em><strong>A Python Example: Token Bucket with Redis and Atomic Updates</strong></em></p><p>Here&#8217;s a practical Python example of a simple distributed token bucket using Redis. This is not a full production gateway, but it demonstrates the core idea: shared atomic state, burst tolerance, and a per-client policy.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;547868fa-eaec-462a-b011-8893d755fd36&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import time
import redis

r = redis.Redis(host="localhost", port=6379, db=0)

LUA_SCRIPT = """
local key = KEYS[1]
local now = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local capacity = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])

local data = redis.call("HMGET", key, "tokens", "last_refill")
local tokens = tonumber(data[1])
local last_refill = tonumber(data[2])

if tokens == nil then
    tokens = capacity
    last_refill = now
end

local elapsed = math.max(0, now - last_refill)
local new_tokens = math.min(capacity, tokens + elapsed * refill_rate)

if new_tokens &lt; cost then
    redis.call("HMSET", key, "tokens", new_tokens, "last_refill", now)
    redis.call("EXPIRE", key, math.ceil(capacity / refill_rate * 2))
    return 0
end

new_tokens = new_tokens - cost
redis.call("HMSET", key, "tokens", new_tokens, "last_refill", now)
redis.call("EXPIRE", key, math.ceil(capacity / refill_rate * 2))
return 1
"""

token_bucket = r.register_script(LUA_SCRIPT)

def allow_request(client_id: str, refill_rate: float = 1.0, capacity: int = 10, cost: int = 1) -&gt; bool:
    now = time.time()
    key = f"rate_limit:{client_id}"
    allowed = token_bucket(
        keys=[key],
        args=[now, refill_rate, capacity, cost]
    )
    return bool(allowed)

# Example usage
client = "tenant_123"
for i in range(15):
    if allow_request(client):
        print(f"{i}: allowed")
    else:
        print(f"{i}: throttled")</code></pre></div><p>A few important points about this example:</p><ul><li><p><strong>Redis provides shared state</strong> across distributed API instances.</p></li><li><p><strong>Lua makes the update atomic</strong>, so concurrent requests do not race each other.</p></li><li><p><strong>Per-client keys improve fairness</strong>, because one tenant does not consume everyone else&#8217;s burst budget.</p></li><li><p><strong>Capacity allows bursts</strong>, while refill rate controls sustained throughput.</p></li></ul><p>This is the kind of implementation that works well when you want something operationally simple and reasonably robust.</p><p>Notice that the code is not trying to solve every policy question. It just enforces a basic contract. In production, you might add tenant tiers, different capacities, dynamic adjustment, or downstream health-based tuning.</p><div><hr></div><p><em><strong>Why Atomicity Matters More Than Elegance</strong></em></p><p>Distributed rate limiting fails in wonderfully creative ways when state updates are not atomic.</p><p>Imagine two instances reading the same token count at the same time. Both think the request should be allowed. Both decrement. Suddenly you have allowed more traffic than your budget intended. Congratulations: your limiter now performs distributed optimism.</p><p>That is why simple primitives often win.</p><p>A Redis counter with atomic increment/decrement is easier to reason about than an over-engineered custom consensus system. A Lua script is easier to debug than a chain of half-synchronized service calls. In distributed systems, correctness under concurrency usually matters more than algorithmic beauty.</p><p>There are times for sophistication. But there are also times when the best engineering move is to choose the boring thing that works.</p><div><hr></div><p><em><strong>How Backpressure Changes Client Behavior</strong></em></p><p>Backpressure is only useful if clients can respond to it.</p><p>If every limit violation looks identical, clients may treat throttling as random failure and retry in harmful ways. Better systems provide signals that help callers adapt:</p><ul><li><p>clear 429 responses</p></li><li><p><code>Retry-After</code> headers</p></li><li><p>well-documented retry guidance</p></li><li><p>tenant-specific quota dashboards</p></li><li><p>predictable burst allowances</p></li></ul><p>This turns rate limiting from a punishment into a negotiation.</p><p>Well-behaved clients can then back off intelligently. Batch jobs can slow down. SDKs can spread requests apart. Integration platforms can reduce concurrency. The system becomes less like a bouncer and more like traffic signage that helps everyone avoid an unnecessary pileup.</p><p>That said, not all clients are well-behaved. Some are badly written, some are legacy, and some appear to have been assembled in a midnight outage with a strongly caffeinated sense of optimism. For those cases, server-side enforcement must still be firm.</p><p>The point is not trust. The point is coordination.</p><div><hr></div><p><em><strong>Practical Policy Patterns That Actually Work</strong></em></p><p>In production, the strongest systems usually blend several policies:</p><ul><li><p><strong>Global per-tenant limits</strong></p></li><li><p><strong>Local instance-level smoothing</strong></p></li><li><p><strong>Priority tiers for critical traffic</strong></p></li><li><p><strong>Burst credits for short spikes</strong></p></li><li><p><strong>Adaptive tightening under load</strong></p></li><li><p><strong>Backpressure signals to clients</strong></p></li><li><p><strong>Queue-aware protection for downstreams</strong></p></li></ul><p>This layered approach is much closer to how real traffic behaves than a single universal limit.</p><p>For example:</p><ul><li><p>A free-tier tenant might get a smaller burst bucket and lower sustained throughput.</p></li><li><p>An enterprise tenant might get a larger burst allowance plus priority scheduling.</p></li><li><p>Internal control-plane traffic might bypass some public limits but still receive protective backpressure.</p></li><li><p>A service under stress might temporarily reduce accepted traffic even if quotas are not fully consumed.</p></li></ul><p>That is what makes the system adaptive rather than merely restrictive.</p><div><hr></div><p><em><strong>Libraries and Services Worth Looking At</strong></em></p><p>If you want to see how this space is handled in the wild, these are good places to study:</p><ul><li><p><strong>Envoy</strong> &#8212; rate limiting filters and proxy-level enforcement</p></li><li><p><strong>NGINX</strong> &#8212; request limiting and traffic shaping at the edge</p></li><li><p><strong>Kong</strong> &#8212; policy-driven API gateway rate limiting</p></li><li><p><strong>AWS API Gateway</strong> &#8212; throttling, quotas, and usage plans</p></li><li><p><strong>Google Cloud API Gateway / Apigee</strong> &#8212; managed API policies and quotas</p></li><li><p><strong>Redis</strong> &#8212; commonly used for distributed counters and token accounting</p></li><li><p><strong>FastAPI middleware examples</strong> &#8212; lightweight Python implementations</p></li><li><p><strong>Django middleware + Redis</strong> &#8212; common server-side enforcement pattern</p></li><li><p><strong>rlan, limits, pyrate-limiter</strong> &#8212; Python libraries exploring rate limiting patterns</p></li></ul><p>If you are comparing approaches, pay special attention to whether the tool supports:</p><ul><li><p>distributed enforcement</p></li><li><p>per-tenant policies</p></li><li><p>burst configuration</p></li><li><p>retry signaling</p></li><li><p>atomic state updates</p></li><li><p>observability and metrics</p></li></ul><p>Those are the features that separate a demo from a production-ready control loop.</p><div><hr></div><p><em><strong>The Final Takeaway</strong></em></p><p>Adaptive rate limiting in distributed APIs is not just about protecting servers from too much traffic.</p><p>It is about making intelligent tradeoffs under contention.</p><p>Fairness decides who gets served when demand is high. Burst control decides how much temporary excitement is acceptable. Backpressure decides whether the system merely rejects traffic or actually helps shape it. Distributed enforcement decides whether the policy holds across the whole fleet or only in the imagination of one instance.</p><p>The best systems do not choose one mechanism and hope for the best. They combine local enforcement, shared global state, tenant-aware fairness, and feedback-driven adaptation. They treat limits as living policy rather than frozen constants.</p><p>That is the real art: keeping the API generous enough for legitimate bursts, strict enough to prevent abuse, and smart enough to adapt when the world gets messy.</p><p>And the world, as you know, is always getting a little messier.</p><div><hr></div><p><em><strong>Warm Signoff</strong></em></p><p>If this was useful, come back tomorrow for more practical backend wisdom with a side of operational sanity.<br>Until then, keep your queues short, your retries polite, and your Redis scripts atomic.</p><p>&#8212; The Backend Developers</p>]]></content:encoded></item><item><title><![CDATA[Transactional Inbox Pattern in Backend Systems: Idempotency, Ordering, and Delivery Guarantees]]></title><description><![CDATA[When &#8220;At-Least-Once&#8221; Means &#8220;At-Least-Twice&#8221;: Why the Transactional Inbox Pattern Exists]]></description><link>https://thebackenddevelopers.substack.com/p/transactional-inbox-pattern-in-backend</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/transactional-inbox-pattern-in-backend</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Tue, 14 Jul 2026 02:05:11 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/206952269/96663c730f212d91877d30691d0825e1.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>If you&#8217;ve spent enough time building backend systems, you eventually meet the same uninvited guest over and over again: duplicate messages.</p><p>They arrive after retries, network blips, consumer restarts, broker redeliveries, and the occasional &#8220;everything is fine&#8221; lie told by an infrastructure dashboard. Your service processes the same event twice, and suddenly one order becomes two shipments, one payment becomes two ledger entries, or one &#8220;welcome email&#8221; becomes a very committed spam campaign.</p><p>That, in a nutshell, is why the transactional inbox pattern exists.</p><p>It&#8217;s not glamorous. It doesn&#8217;t sparkle like event-driven architecture diagrams on a conference slide. But it is one of those deeply practical patterns that keeps production systems from slowly turning into a haunted house.</p><p><em><strong>What the Transactional Inbox Pattern Actually Is</strong></em></p><p>The transactional inbox pattern is a consumer-side reliability mechanism. It records incoming message identity and processing state in a durable store&#8212;usually a database table&#8212;before or alongside business logic. The goal is simple: if the same message is delivered more than once, your backend can detect that it has already seen it and avoid applying the side effect again.</p><p>In other words, the inbox pattern helps transform <strong>at-least-once delivery</strong> into <strong>effectively-once processing</strong>.</p><p>That wording matters.</p><ul><li><p><strong>At-least-once delivery</strong> means the broker will make a best effort to deliver messages, but duplicates are possible.</p></li><li><p><strong>Exactly-once delivery</strong> is a stronger promise, but in distributed systems it is often limited, expensive, or conditional.</p></li><li><p><strong>Effectively-once processing</strong> is the practical target: your application behaves as though each message was handled once, even if the broker delivered it multiple times.</p></li></ul><p>The inbox pattern is the consumer-side cousin of the <strong>outbox pattern</strong>. Outbox solves the producer problem: &#8220;How do I reliably publish events when my database transaction succeeds?&#8221; Inbox solves the receiver problem: &#8220;How do I safely consume events when the broker or network might retry?&#8221;</p><p>Together, they form a robust message flow strategy.</p><p><em><strong>Why Duplicate Messages Are Not a Bug, But a Fact of Life</strong></em></p><p>A lot of systems fail because teams design as if duplicates are a rare edge case. They are not.</p><p>In distributed messaging, duplicates happen because:</p><ul><li><p>the consumer processed the message but crashed before acknowledging it,</p></li><li><p>the acknowledgment was lost,</p></li><li><p>the broker retried after timeout,</p></li><li><p>the consumer restarted mid-flight,</p></li><li><p>partitions were reassigned,</p></li><li><p>a network hiccup made everyone act dramatic,</p></li><li><p>or the same event was legitimately published more than once.</p></li></ul><p>The important shift is this: <strong>you do not build a safe system by trying to guarantee duplicates never happen</strong>. You build a safe system by assuming they will happen and designing your consumer so repeating work is harmless.</p><p>That&#8217;s the heart of idempotency.</p><p><em><strong>Idempotency: The Backbone of the Inbox Pattern</strong></em></p><p>Idempotency means that applying the same operation multiple times has the same effect as applying it once.</p><p>Examples:</p><ul><li><p>Setting a user&#8217;s status to &#8220;active&#8221; is idempotent.</p></li><li><p>Incrementing a counter is not idempotent unless you protect it.</p></li><li><p>Marking an order as &#8220;paid&#8221; is usually idempotent if the transition is guarded carefully.</p></li><li><p>Charging a credit card twice is, historically, not a feature customers appreciate.</p></li></ul><p>The inbox pattern uses idempotency by keeping a durable record of message identity. A message typically carries a unique key such as:</p><ul><li><p>event ID</p></li><li><p>message ID</p></li><li><p>transaction ID</p></li><li><p>aggregate ID + sequence number</p></li><li><p>broker message UUID</p></li></ul><p>When a message arrives, the consumer:</p><ol><li><p>Checks whether that message ID already exists in the inbox store.</p></li><li><p>If it does, the system skips processing or treats it as already complete.</p></li><li><p>If it does not, the system records it and proceeds with the business action in the same transaction.</p></li></ol><p>This prevents the &#8220;processed but not acknowledged&#8221; and &#8220;acknowledged but not processed&#8221; failure windows from corrupting business state.</p><p><em><strong>The Core Transaction Flow</strong></em></p><p>A common inbox implementation follows this shape:</p><ol><li><p>Receive message from broker.</p></li><li><p>Start a database transaction.</p></li><li><p>Insert the message ID into an inbox table with a unique constraint.</p></li><li><p>If insert succeeds, continue processing.</p></li><li><p>Apply domain changes.</p></li><li><p>Commit transaction.</p></li><li><p>Acknowledge message to broker.</p></li></ol><p>If the same message comes again, the unique constraint blocks the duplicate insert, and the handler knows this message was already handled.</p><p>That unique constraint is doing a lot of heavy lifting.</p><p>It is the difference between &#8220;we hope this wasn&#8217;t already processed&#8221; and &#8220;the database will physically stop us from messing this up.&#8221;</p><p><em><strong>A Practical Python Example with PostgreSQL</strong></em></p><p>Below is a simplified example using Python and PostgreSQL. It demonstrates an inbox table and a consumer that processes each message only once.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;38664637-6000-47b6-904a-4e36f97f42f4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import psycopg2
from psycopg2.extras import Json

DSN = "dbname=app user=app password=secret host=localhost"

def handle_order_paid(message):
    """
    message example:
    {
        "message_id": "evt_123",
        "order_id": "ord_456",
        "amount": 4999
    }
    """
    conn = psycopg2.connect(DSN)
    try:
        with conn:
            with conn.cursor() as cur:
                # 1) Try to record the message in the inbox
                cur.execute("""
                    INSERT INTO inbox_messages (message_id, processed_at)
                    VALUES (%s, NOW())
                    ON CONFLICT (message_id) DO NOTHING
                    RETURNING message_id
                """, (message["message_id"],))

                inserted = cur.fetchone()

                # If not inserted, we've seen it before
                if not inserted:
                    print(f"Duplicate message skipped: {message['message_id']}")
                    return

                # 2) Apply business logic
                cur.execute("""
                    UPDATE orders
                    SET status = 'PAID',
                        paid_amount = %s,
                        paid_at = NOW()
                    WHERE id = %s
                """, (message["amount"], message["order_id"]))

                # 3) Optionally write audit/event records as part of same transaction
                cur.execute("""
                    INSERT INTO payment_audit (order_id, message_id, payload)
                    VALUES (%s, %s, %s)
                """, (message["order_id"], message["message_id"], Json(message)))

        print(f"Processed message: {message['message_id']}")
    finally:
        conn.close()</code></pre></div><p>And the table definition:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;a45f3099-af48-4d67-b9e1-f2cc11092496&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE inbox_messages (
    message_id TEXT PRIMARY KEY,
    processed_at TIMESTAMP NOT NULL
);</code></pre></div><p>A few important notes:</p><ul><li><p>The unique key on <code>message_id</code> is what gives you deduplication.</p></li><li><p>The business update and inbox insert should be in the same transaction.</p></li><li><p>If the transaction fails, nothing is committed.</p></li><li><p>If the broker redelivers the message, the duplicate insert is rejected safely.</p></li></ul><p>This is a simplified version. In production, you often store more metadata: source service, consumer name, partition, offset, retries, status, and error details.</p><p><em><strong>Ordering: The Part Everyone Wants to Ignore Until It Bites Them</strong></em></p><p>The transactional inbox pattern helps with duplicates, but it does <strong>not magically solve ordering</strong>.</p><p>That distinction is critical.</p><p>A message can be:</p><ul><li><p>unique but arrive late,</p></li><li><p>duplicated and out of order,</p></li><li><p>delayed by retries,</p></li><li><p>processed in parallel by multiple consumers,</p></li><li><p>or partitioned in a way that breaks global ordering.</p></li></ul><p>The inbox pattern protects consistency, but order is a separate concern.</p><p>If your domain requires strict ordering, you need additional design decisions such as:</p><ul><li><p><strong>Partition keys</strong>: keep related events in the same broker partition.</p></li><li><p><strong>Sequence numbers</strong>: reject or delay messages that arrive with an unexpected sequence.</p></li><li><p><strong>Per-aggregate processing</strong>: only one worker processes a given entity&#8217;s stream at a time.</p></li><li><p><strong>Locking or fencing</strong>: ensure stale workers do not override newer state.</p></li><li><p><strong>Reordering buffers</strong>: temporarily hold messages until missing ones arrive.</p></li></ul><p>For example, if you process <code>OrderCreated</code>, <code>OrderPaid</code>, and <code>OrderCancelled</code>, the inbox can ensure each event is handled once, but it cannot by itself guarantee that <code>OrderPaid</code> won&#8217;t arrive before <code>OrderCreated</code> if your producer or broker allows that scenario.</p><p>And if your logic assumes order, your data model must reflect that assumption explicitly.</p><p><em><strong>A Better Example: Inbox Plus Sequencing</strong></em></p><p>Here&#8217;s a pattern for handling ordered events safely using a sequence number.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;753bb2de-8227-4309-b449-c71d2ac9dd18&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import psycopg2

DSN = "dbname=app user=app password=secret host=localhost"

def process_order_event(event):
    """
    event example:
    {
        "event_id": "evt_101",
        "order_id": "ord_456",
        "sequence": 12,
        "type": "ORDER_PAID"
    }
    """
    conn = psycopg2.connect(DSN)

    try:
        with conn:
            with conn.cursor() as cur:
                # Deduplicate by event_id
                cur.execute("""
                    INSERT INTO inbox_messages (message_id, processed_at)
                    VALUES (%s, NOW())
                    ON CONFLICT (message_id) DO NOTHING
                    RETURNING message_id
                """, (event["event_id"],))

                if cur.fetchone() is None:
                    print("Duplicate event ignored")
                    return

                # Ensure sequence is newer than last processed
                cur.execute("""
                    SELECT last_sequence
                    FROM order_stream_state
                    WHERE order_id = %s
                    FOR UPDATE
                """, (event["order_id"],))

                row = cur.fetchone()
                last_sequence = row[0] if row else 0

                if event["sequence"] &lt;= last_sequence:
                    print("Out-of-order or stale event ignored")
                    return

                # Apply update
                cur.execute("""
                    UPDATE orders
                    SET status = %s
                    WHERE id = %s
                """, (event["type"], event["order_id"]))

                # Update stream state
                cur.execute("""
                    INSERT INTO order_stream_state (order_id, last_sequence)
                    VALUES (%s, %s)
                    ON CONFLICT (order_id)
                    DO UPDATE SET last_sequence = EXCLUDED.last_sequence
                """, (event["order_id"], event["sequence"]))

        print("Event processed")
    finally:
        conn.close()</code></pre></div><p>This adds a second safeguard: not just &#8220;Have I seen this message?&#8221; but also &#8220;Is this message newer than the state I&#8217;ve already accepted?&#8221;</p><p>That&#8217;s how you begin to tame ordering problems without pretending they don&#8217;t exist.</p><p><em><strong>What the Inbox Pattern Does Well</strong></em></p><p>The transactional inbox pattern shines in these areas:</p><p><strong>1. It makes duplicate delivery safe</strong><br>You can process redelivered messages without corrupting data.</p><p><strong>2. It reduces uncertainty under failure</strong><br>If the consumer crashes halfway through, the transaction boundaries help keep the system recoverable.</p><p><strong>3. It provides a durable record of what was seen</strong><br>That&#8217;s useful for auditability, troubleshooting, and replay analysis.</p><p><strong>4. It supports exactly the kind of operational paranoia backend systems need</strong><br>And by paranoia, I mean &#8220;healthy respect for reality.&#8221;</p><p><strong>5. It plays nicely with transactionally consistent databases</strong><br>Especially PostgreSQL, MySQL, SQL Server, and other systems with strong transactional semantics.</p><p><em><strong>What It Does Not Solve</strong></em></p><p>The inbox pattern is powerful, but it is not a miracle.</p><p>It does not automatically solve:</p><ul><li><p>global ordering across all consumers,</p></li><li><p>poisoned messages that always fail,</p></li><li><p>business logic bugs,</p></li><li><p>lost messages before they reach the consumer,</p></li><li><p>producer-side reliability,</p></li><li><p>schema evolution mistakes,</p></li><li><p>or poor observability.</p></li></ul><p>If a message always fails because the payload is malformed or the downstream dependency is down, the inbox table will not save you from operational pain. It may simply preserve the evidence.</p><p>That&#8217;s why the research consistently points to operational controls as part of the pattern, not optional extras.</p><p><em><strong>Operational Controls You Should Pair with an Inbox</strong></em></p><p>A production inbox implementation should typically include:</p><ul><li><p><strong>Retries with backoff</strong><br>Avoid hammering the system during transient failures.</p></li><li><p><strong>Dead-letter queues (DLQs)</strong><br>Move poison messages aside after repeated failure.</p></li><li><p><strong>Observability</strong><br>Track duplicate rates, lag, processing time, and error counts.</p></li><li><p><strong>Alerting</strong><br>If duplicates spike or inbox lag grows, someone should know before customers do.</p></li><li><p><strong>Replay tooling</strong><br>Let operators safely reprocess messages when needed.</p></li><li><p><strong>Retention policies</strong><br>Decide how long processed inbox records should remain.</p></li></ul><p>The inbox table is not just a table. It is part of your system&#8217;s memory of what happened and when.</p><p><em><strong>Broker Features Help, But They Do Not Replace the Pattern</strong></em></p><p>Modern messaging platforms often provide useful features:</p><ul><li><p><strong>Kafka</strong>: partitions, consumer groups, offsets, and strong ordering within a partition</p></li><li><p><strong>RabbitMQ</strong>: acknowledgments, retries, dead-letter exchanges</p></li><li><p><strong>Amazon SQS</strong>: visibility timeout, FIFO queues, deduplication window</p></li><li><p><strong>Azure Service Bus</strong>: sessions, duplicate detection, dead-lettering</p></li></ul><p>These features are great. Use them.</p><p>But they reduce risk; they do not eliminate the need for application-level safety.</p><p>Why?</p><p>Because broker guarantees are bounded by their own rules. Your application still has to deal with:</p><ul><li><p>side effects in your database,</p></li><li><p>exactly which operation happened before crash,</p></li><li><p>whether an update was partial,</p></li><li><p>and whether downstream systems are idempotent too.</p></li></ul><p>In the real world, a strong backend usually uses both:</p><ul><li><p>broker capabilities for transport-level reliability,</p></li><li><p>inbox logic for application-level correctness.</p></li></ul><p><em><strong>A Simple Mental Model</strong></em></p><p>Here&#8217;s the mental model I recommend:</p><ul><li><p><strong>Broker</strong>: &#8220;I will try to deliver this message.&#8221;</p></li><li><p><strong>Inbox</strong>: &#8220;I will remember whether I already handled it.&#8221;</p></li><li><p><strong>Business logic</strong>: &#8220;I will only mutate state when it is safe.&#8221;</p></li><li><p><strong>Observability</strong>: &#8220;I will tell you when things are getting weird.&#8221;</p></li></ul><p>That last one is crucial. Systems rarely fail with a tidy explanation. They fail with symptoms.</p><p>Duplicate counts rising. Lag increasing. One consumer handling too much traffic. A DLQ that nobody watches. A sequence mismatch that only appears on Thursdays. The usual circus.</p><p><em><strong>Schema Design for an Inbox Table</strong></em></p><p>A practical inbox schema often includes:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;fd88b832-5607-4c82-97b2-ff2dcc591cc0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE inbox_messages (
    message_id TEXT PRIMARY KEY,
    consumer_name TEXT NOT NULL,
    source_topic TEXT NOT NULL,
    partition_id INT NULL,
    offset_value BIGINT NULL,
    status TEXT NOT NULL DEFAULT 'processed',
    received_at TIMESTAMP NOT NULL DEFAULT NOW(),
    processed_at TIMESTAMP NULL,
    error_text TEXT NULL
);</code></pre></div><p>You might also add:</p><ul><li><p><code>attempt_count</code></p></li><li><p><code>correlation_id</code></p></li><li><p><code>aggregate_id</code></p></li><li><p><code>sequence_number</code></p></li><li><p><code>payload_hash</code></p></li></ul><p>This depends on your debugging and replay needs.</p><p>A few design tips:</p><ul><li><p>Keep the dedup key <strong>unique and immutable</strong>.</p></li><li><p>Choose whether dedup is global or scoped per consumer.</p></li><li><p>Store enough metadata to explain failures later.</p></li><li><p>Avoid indefinite growth unless your storage budget enjoys adventure.</p></li></ul><p><em><strong>When to Use an Inbox Pattern</strong></em></p><p>Use the transactional inbox pattern when:</p><ul><li><p>duplicate delivery is possible,</p></li><li><p>side effects must be protected,</p></li><li><p>your system updates persistent state,</p></li><li><p>retries are expected,</p></li><li><p>or you need resilience under partial failure.</p></li></ul><p>It is especially valuable in:</p><ul><li><p>payment processing,</p></li><li><p>order handling,</p></li><li><p>inventory systems,</p></li><li><p>account state updates,</p></li><li><p>notifications,</p></li><li><p>workflow engines,</p></li><li><p>and event-driven microservices.</p></li></ul><p>If the consequence of duplicate processing is &#8220;meh, no harm done,&#8221; you may not need the full pattern. But if one duplicate can move money, change inventory, or trigger another service chain, the inbox becomes a very good investment.</p><p><em><strong>Inbox and Outbox: A Very Functional Couple</strong></em></p><p>The outbox pattern ensures that when your service changes its database state, it also records an event to publish later.</p><p>The inbox pattern ensures that when your service receives an event, it can safely process it once.</p><p>Together they solve the classic two-sided messaging problem:</p><ul><li><p>Outbox: don&#8217;t lose messages when publishing</p></li><li><p>Inbox: don&#8217;t double-apply messages when consuming</p></li></ul><p>If you want reliable event-driven architecture, this pair is often the backbone.</p><p>Without them, you are basically asking your distributed system to &#8220;just be chill,&#8221; which is not a serious architecture strategy.</p><p><em><strong>Example Libraries and Services That Support Similar Goals</strong></em></p><p>Here are some technologies that support or complement inbox-style processing:</p><ul><li><p><strong>Kafka</strong> &#8211; consumer groups, offsets, partition ordering</p></li><li><p><strong>RabbitMQ</strong> &#8211; acknowledgments, dead-letter exchanges, retry patterns</p></li><li><p><strong>Amazon SQS FIFO</strong> &#8211; deduplication and ordered delivery within constraints</p></li><li><p><strong>Azure Service Bus</strong> &#8211; duplicate detection, sessions, dead-letter queues</p></li><li><p><strong>PostgreSQL</strong> &#8211; unique constraints, transactional inserts, advisory locks</p></li><li><p><strong>Django / SQLAlchemy / psycopg2</strong> &#8211; useful for building transactional consumer logic in Python</p></li><li><p><strong>Temporal</strong> &#8211; workflow durability and retries that reduce custom inbox complexity</p></li><li><p><strong>Debezium</strong> &#8211; often paired with outbox patterns for reliable event publishing</p></li></ul><p>These tools can help, but the architectural principle remains the same: <strong>store identity, check state, and make duplicate work harmless</strong>.</p><p><em><strong>A Final Word on Discipline</strong></em></p><p>The transactional inbox pattern is one of those quiet architectural decisions that separates &#8220;works in dev&#8221; from &#8220;survives in production.&#8221;</p><p>It does not eliminate failure. It makes failure survivable.</p><p>It does not promise perfect order. It makes disorder manageable.</p><p>It does not prevent duplicates from happening. It makes duplicates safe.</p><p>That is a much more realistic promise, and in backend systems, realism is usually the highest form of elegance.</p><p><em><strong>Closing Stanza</strong></em></p><p>If this was useful, come back tomorrow for more backend tales, hard-won patterns, and the occasional friendly warning from the trenches.<br>Follow <strong>The Backend Developers</strong> and stay close&#8212;there&#8217;s always another distributed system gremlin waiting behind the next queue.</p>]]></content:encoded></item><item><title><![CDATA[Event Sourcing in Backend Systems: Auditability, Replays, and Operational Trade-offs]]></title><description><![CDATA[Why Event Sourcing Still Makes Backend Engineers Both Grin and Sweat]]></description><link>https://thebackenddevelopers.substack.com/p/event-sourcing-in-backend-systems-3e7</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/event-sourcing-in-backend-systems-3e7</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Fri, 10 Jul 2026 02:30:45 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/206387854/342b22922e62cea2b32152742d829912.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>There are two kinds of backend systems in this world:</p><ol><li><p>The ones that tell you what the current state is.</p></li><li><p>The ones that tell you what happened, when it happened, and occasionally make you question your life choices.</p></li></ol><p>Event sourcing belongs firmly in the second category.</p><p>If you&#8217;ve ever built a system where someone asked, &#8220;How did we end up here?&#8221; and your answer was a vague shrug followed by a SQL query that still didn&#8217;t explain anything, event sourcing starts sounding very attractive. Instead of overwriting state, you append immutable events to a log. Those events become the source of truth. The current state is just a reconstruction of history.</p><p>That sounds elegant, and in many cases it absolutely is. But like all elegant backend ideas, it arrives wearing a tuxedo and carrying a toolbox full of trade-offs.</p><p>In this post, we&#8217;ll walk through what event sourcing is, why teams adopt it, how replays work, where snapshots and projections fit in, and why the operational burden can feel like adopting a very smart, very stubborn pet.</p><div><hr></div><p><em><strong>What Event Sourcing Actually Means</strong></em></p><p>In a traditional CRUD-style backend, you usually store the latest version of an entity. A customer changes their address? Update the row. An order is shipped? Flip the status field. A payment fails? Update a flag and move on with your day.</p><p>Event sourcing takes a different approach.</p><p>Instead of storing the final state directly, you store the sequence of events that led there:</p><ul><li><p><code>CustomerCreated</code></p></li><li><p><code>AddressUpdated</code></p></li><li><p><code>OrderPlaced</code></p></li><li><p><code>PaymentAuthorized</code></p></li><li><p><code>OrderShipped</code></p></li></ul><p>Each event is immutable. You do not edit history. You add to it.</p><p>The current state is derived by replaying those events in order. If you want to know the state of an order, you rebuild it from the event stream. If you want to know what happened last Tuesday at 3:17 PM, the log can tell you. If you want to reconstruct the system as it looked before a bug was introduced, you can often replay up to a point in time and inspect the result.</p><p>This is the core value proposition: <strong>auditability and reconstructability</strong>.</p><p>And that is why event sourcing shows up so often in domains like finance, logistics, healthcare, compliance-heavy applications, dispute resolution, and anywhere the history matters as much as the present.</p><div><hr></div><p><em><strong>Why Teams Adopt Event Sourcing</strong></em></p><p>Let&#8217;s be honest: nobody wakes up excited to add complexity to their backend for fun. Well, not nobody, but those people should be monitored.</p><p>Teams adopt event sourcing for very specific reasons:</p><h3><strong>Auditability</strong></h3><p>You can inspect the full history of a domain object or aggregate. This is useful when the business needs a complete record of who did what and when.</p><h3><strong>Temporal reconstruction</strong></h3><p>You can rebuild the system as it existed at a point in time. That&#8217;s incredibly valuable for debugging, compliance, and forensic analysis.</p><h3><strong>Replayability</strong></h3><p>You can reprocess historical events to build new read models, recover from bugs, or support new reporting needs without changing the original write path.</p><h3><strong>Business meaning in history</strong></h3><p>Sometimes the sequence itself is part of the domain. In a trading system, a supply chain, or a billing workflow, the order and nature of changes matter deeply.</p><h3><strong>Better traceability across distributed systems</strong></h3><p>Events can become the shared language of the system. This makes it easier to reason about long-running processes and eventual consistency.</p><p>So yes, event sourcing is powerful. But power in software is never free. The bill arrives later, often in the form of operational complexity and debugging headaches.</p><div><hr></div><p><em><strong>The Event Stream as Source of Truth</strong></em></p><p>The mental model is simple: the event log is the truth, and everything else is a projection of that truth.</p><p>That means your write model does not say, &#8220;the order is shipped.&#8221; It says, &#8220;an <code>OrderShipped</code> event occurred.&#8221;</p><p>That distinction matters because the event does not merely describe state; it describes a fact. Facts do not get overwritten.</p><p>This has several benefits:</p><ul><li><p>You keep full historical context.</p></li><li><p>You can answer &#8220;how did we get here?&#8221;</p></li><li><p>You can rebuild derived state if your projection logic changes.</p></li><li><p>You can support multiple views of the same data.</p></li></ul><p>But this also means your system is no longer just &#8220;storing data.&#8221; It is now preserving a history that must remain meaningful over time.</p><p>And that is where the fun begins.</p><div><hr></div><p><em><strong>Replays: The Superpower and the Footgun</strong></em></p><p>Replays are what make event sourcing truly useful.</p><p>A replay means taking the stored events and applying them again, usually to:</p><ul><li><p>rebuild projections after a bug fix,</p></li><li><p>populate a new read model,</p></li><li><p>recover system state after a failure,</p></li><li><p>test event handler behavior,</p></li><li><p>or run analytics against the historical stream.</p></li></ul><p>This is fantastic, because it gives you a way to reconstruct the system at will.</p><p>But replays also define an operational contract. Once you rely on replay, your handlers must be deterministic. Given the same sequence of events, they should always produce the same result.</p><p>That means:</p><ul><li><p>no hidden randomness,</p></li><li><p>no dependence on changing external state,</p></li><li><p>no logic that behaves differently depending on the day of the week unless that&#8217;s explicit domain behavior,</p></li><li><p>and careful handling of idempotency.</p></li></ul><p>If replaying the same events twice changes the result, your architecture has started lying to you.</p><h3><strong>Why determinism matters</strong></h3><p>Imagine a projection that counts orders and accidentally increments twice when the same event is replayed. Now your &#8220;truth&#8221; is inflated. That&#8217;s not a small bug. That&#8217;s a history problem.</p><h3><strong>Why idempotency matters</strong></h3><p>Events may be delivered more than once. Systems fail. Consumers retry. Queues do queue-like things. Your event handlers need to be resilient to duplicate processing.</p><h3><strong>Why stable semantics matter</strong></h3><p>If you change the meaning of an event without versioning it properly, replays can silently become incorrect. That is one of event sourcing&#8217;s cruelest jokes: the system works until you try to understand the past.</p><p>In plain terms: replay is not &#8220;just re-reading the log.&#8221; It is a carefully maintained pipeline that must be testable, versioned, and disciplined.</p><div><hr></div><p><em><strong>The Big Trade-Off: Simpler Writes, Harder Everything Else</strong></em></p><p>Event sourcing often simplifies write-time persistence. Instead of updating multiple tables or chasing denormalized state, you append one event. That is clean and elegant.</p><p>But the cost moves elsewhere.</p><h3><strong>Debugging becomes more layered</strong></h3><p>When a user says, &#8220;The current view is wrong,&#8221; the root cause may be in:</p><ul><li><p>the event itself,</p></li><li><p>the projection logic,</p></li><li><p>the replay process,</p></li><li><p>schema evolution,</p></li><li><p>a handler bug,</p></li><li><p>or a missed edge case in versioning.</p></li></ul><p>With a traditional mutable row, the problem is often localized. With event sourcing, the issue may be in any one of several stages.</p><h3><strong>Schema evolution is harder</strong></h3><p>Events are not just data. They are historical contracts. Once emitted, they can live for years.</p><p>So if your domain changes, you must decide how old events continue to make sense.</p><p>Typical strategies include:</p><ul><li><p>versioned event types,</p></li><li><p>upcasters,</p></li><li><p>backward-compatible payload evolution,</p></li><li><p>and migration layers in replay pipelines.</p></li></ul><h3><strong>Mental model complexity increases</strong></h3><p>Developers must think in terms of:</p><ul><li><p>commands,</p></li><li><p>domain events,</p></li><li><p>projections,</p></li><li><p>snapshots,</p></li><li><p>subscriptions,</p></li><li><p>eventual consistency,</p></li><li><p>and reprocessing semantics.</p></li></ul><p>That&#8217;s a lot more than &#8220;update row, commit transaction, go home and pretend distributed systems are someone else&#8217;s problem.&#8221;</p><h3><strong>Operational complexity rises</strong></h3><p>You need monitoring for consumers, replay jobs, lag, projection correctness, storage growth, and version compatibility.</p><p>So the major trade-off is this:<br><strong>event sourcing can make writing data elegant, but it makes the overall system architecture more demanding.</strong></p><div><hr></div><p><em><strong>Python Example: A Tiny Event-Sourced Aggregate</strong></em></p><p>Here&#8217;s a simple example in Python to show the pattern.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6d259e52-b2c9-41df-9674-2ec1c3cbaadb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from dataclasses import dataclass, asdict
from typing import List, Union

# Domain Events
@dataclass(frozen=True)
class AccountOpened:
    account_id: str
    initial_balance: int

@dataclass(frozen=True)
class MoneyDeposited:
    account_id: str
    amount: int

@dataclass(frozen=True)
class MoneyWithdrawn:
    account_id: str
    amount: int

Event = Union[AccountOpened, MoneyDeposited, MoneyWithdrawn]


class BankAccount:
    def __init__(self):
        self.account_id = None
        self.balance = 0
        self._uncommitted_events: List[Event] = []

    def apply(self, event: Event):
        if isinstance(event, AccountOpened):
            self.account_id = event.account_id
            self.balance = event.initial_balance
        elif isinstance(event, MoneyDeposited):
            self.balance += event.amount
        elif isinstance(event, MoneyWithdrawn):
            self.balance -= event.amount

    def open_account(self, account_id: str, initial_balance: int):
        event = AccountOpened(account_id, initial_balance)
        self.apply(event)
        self._uncommitted_events.append(event)

    def deposit(self, amount: int):
        event = MoneyDeposited(self.account_id, amount)
        self.apply(event)
        self._uncommitted_events.append(event)

    def withdraw(self, amount: int):
        if self.balance &lt; amount:
            raise ValueError("Insufficient funds")
        event = MoneyWithdrawn(self.account_id, amount)
        self.apply(event)
        self._uncommitted_events.append(event)

    @classmethod
    def rehydrate(cls, events: List[Event]):
        account = cls()
        for event in events:
            account.apply(event)
        return account


# Example usage
history = [
    AccountOpened("acct-123", 100),
    MoneyDeposited("acct-123", 50),
    MoneyWithdrawn("acct-123", 20),
]

account = BankAccount.rehydrate(history)
print(account.balance)  # 130</code></pre></div><p>This example captures the essence of event sourcing:</p><ul><li><p>events are immutable facts,</p></li><li><p>state is reconstructed by replay,</p></li><li><p>and the aggregate behaves as a pure function of its history.</p></li></ul><p>Of course, real systems also need persistence, concurrency control, versioning, and projections. Because if backend engineering were allowed to stay this simple, we&#8217;d all be sleeping more.</p><div><hr></div><p><em><strong>Projections: How You Make Reads Fast</strong></em></p><p>If the event log is your truth, projections are your convenience.</p><p>A projection is a read-optimized view built from events. For example:</p><ul><li><p>a customer summary table,</p></li><li><p>an order status dashboard,</p></li><li><p>a search index,</p></li><li><p>a reporting warehouse,</p></li><li><p>or a materialized view for querying by status.</p></li></ul><p>Why projections matter:</p><ul><li><p>Replaying a huge event stream for every user request would be painfully slow.</p></li><li><p>Most systems need fast reads.</p></li><li><p>Different consumers need different shapes of data.</p></li></ul><p>So instead of querying the event log directly every time, you build one or more projections.</p><p>This is one of the most important practical lessons in event sourcing:<br><strong>the raw event log alone is not enough.</strong></p><p>You need read models.</p><p>And once you have read models, you&#8217;ve introduced eventual consistency. The write side changes first, and the projection catches up later. That&#8217;s acceptable in many domains, but it is a trade-off you must consciously embrace.</p><div><hr></div><p><em><strong>Snapshots: Reducing the Cost of Rebuilding History</strong></em></p><p>Replaying millions of events to rebuild a state object can become expensive.</p><p>That is where snapshots come in.</p><p>A snapshot is a saved state at a particular point in time. Instead of replaying from the beginning of history, you start from the snapshot and apply only the events that happened after it.</p><p>This helps with:</p><ul><li><p>faster rehydration,</p></li><li><p>shorter recovery times,</p></li><li><p>more efficient replays,</p></li><li><p>and better performance in large aggregates.</p></li></ul><p>Important note: snapshots are an optimization, not the source of truth. The event log still is.</p><h3><strong>When to use snapshots</strong></h3><p>Snapshots are useful when:</p><ul><li><p>aggregate histories are long,</p></li><li><p>rehydration is expensive,</p></li><li><p>or replaying from zero becomes too slow.</p></li></ul><h3><strong>What snapshots do not solve</strong></h3><p>Snapshots do not eliminate the need for versioning, deterministic handlers, or replay-safe logic. They just reduce the amount of history you need to process in one go.</p><p>Think of them as bookmarks in a very long and highly opinionated novel.</p><div><hr></div><p><em><strong>Event Versioning and Schema Evolution</strong></em></p><p>This is where many event sourcing systems grow a few gray hairs.</p><p>Your domain will evolve. It always does. Product managers will discover new ideas. Compliance will demand a new field. Marketing will ask for &#8220;just one more attribute.&#8221; Someone, somewhere, will use the phrase &#8220;should be quick.&#8221;</p><p>But old events still exist.</p><p>So you need a strategy for evolution:</p><h3><strong>Additive changes</strong></h3><p>The safest route is often adding new optional fields while keeping old ones intact.</p><h3><strong>New event versions</strong></h3><p>You may define <code>OrderPlacedV2</code> instead of changing <code>OrderPlaced</code> in place.</p><h3><strong>Upcasting</strong></h3><p>You transform older event payloads into newer structures during replay.</p><h3><strong>Translation layers</strong></h3><p>You convert old events into new domain shapes as they are read.</p><p>The important idea is this: <strong>events are long-lived contracts</strong>.<br>You are not just designing a current data model. You are designing historical semantics.</p><p>That&#8217;s why event sourcing rewards teams that treat event definitions like APIs, not like temporary implementation details.</p><div><hr></div><p><em><strong>When Event Sourcing Works Best</strong></em></p><p>Event sourcing is not a universal default. It shines in specific kinds of systems.</p><p>It tends to work well when:</p><ul><li><p>the domain history is valuable,</p></li><li><p>auditability is required,</p></li><li><p>disputes or investigations are common,</p></li><li><p>projections can be eventually consistent,</p></li><li><p>and the bounded context is clear.</p></li></ul><p>Examples:</p><ul><li><p>financial ledgers,</p></li><li><p>order management,</p></li><li><p>inventory movement,</p></li><li><p>booking systems,</p></li><li><p>approval workflows,</p></li><li><p>compliance systems,</p></li><li><p>and collaborative systems where history matters.</p></li></ul><p>It is less attractive when:</p><ul><li><p>the data is simple CRUD,</p></li><li><p>historical reconstruction is not valuable,</p></li><li><p>team maturity is low,</p></li><li><p>or operational overhead needs to stay minimal.</p></li></ul><p>A lot of backend architectures fail because people adopt them for ideology instead of fit. Event sourcing is a specialized tool. A very sharp one. Not a hammer for every nail.</p><div><hr></div><p><em><strong>Real-World Tooling Makes All the Difference</strong></em></p><p>In production, event sourcing is rarely built from scratch end-to-end. Most teams lean on established ecosystem support.</p><p>Examples include:</p><ul><li><p><strong>EventStoreDB</strong> for event storage and subscriptions,</p></li><li><p><strong>Axon</strong> for CQRS and event-driven architecture support,</p></li><li><p><strong>Marten</strong> for event storage on PostgreSQL,</p></li><li><p><strong>NEventStore</strong> in .NET ecosystems,</p></li><li><p><strong>Kafka-based patterns</strong> for distributed event streams,</p></li><li><p>and Python libraries like <code>eventsourcing</code>.</p></li></ul><p>These tools help with:</p><ul><li><p>storage,</p></li><li><p>subscriptions,</p></li><li><p>replay,</p></li><li><p>projections,</p></li><li><p>and operational primitives.</p></li></ul><p>This matters because event sourcing is not just a pattern. It is a system of responsibilities. Good tooling reduces the surface area enough that the approach becomes viable in real production environments.</p><p>Without tooling, you&#8217;re basically building a very expensive log-based philosophy project.</p><div><hr></div><p><em><strong>Practical Guidelines if You Want to Use It</strong></em></p><p>If you&#8217;re considering event sourcing, here are the rules I&#8217;d put on the wall in bold marker:</p><ol><li><p><strong>Start with the domain, not the pattern.</strong><br>If history matters, event sourcing may fit. If not, don&#8217;t force it.</p></li><li><p><strong>Treat events as immutable contracts.</strong><br>Version them carefully and keep semantics stable.</p></li><li><p><strong>Design projections as first-class citizens.</strong><br>Fast reads don&#8217;t happen by magic.</p></li><li><p><strong>Make replay a tested path.</strong><br>Don&#8217;t assume reprocessing will &#8220;just work.&#8221;</p></li><li><p><strong>Use snapshots where replay cost becomes painful.</strong></p></li><li><p><strong>Build idempotency into consumers.</strong><br>Duplicate processing will happen eventually.</p></li><li><p><strong>Monitor lag and projection correctness.</strong><br>A healthy event store with broken projections is still a broken system.</p></li><li><p><strong>Expect operational maturity to matter.</strong><br>This pattern rewards teams that test, version, and observe carefully.</p></li></ol><div><hr></div><p><em><strong>A Tiny Mental Model to Remember</strong></em></p><p>If traditional persistence asks:</p><blockquote><p>&#8220;What is the state right now?&#8221;</p></blockquote><p>Event sourcing asks:</p><blockquote><p>&#8220;What sequence of facts created this state?&#8221;</p></blockquote><p>That shift is beautiful when you need it.</p><p>It is also the reason event sourcing can feel like explaining a joke to a compiler: conceptually elegant, operationally exacting, and not forgiving when you cut corners.</p><div><hr></div><p><em><strong>References to Libraries and Services Worth Exploring</strong></em></p><p>If you want to go deeper, these are commonly used in real systems:</p><ul><li><p><strong>EventStoreDB</strong> &#8212; purpose-built event storage and streaming</p></li><li><p><strong>Axon Framework</strong> &#8212; popular in Java for CQRS and event sourcing</p></li><li><p><strong>Marten</strong> &#8212; PostgreSQL-backed document and event store for .NET</p></li><li><p><strong>NEventStore</strong> &#8212; event store abstraction in .NET ecosystems</p></li><li><p><strong>Apache Kafka</strong> &#8212; often used for event streaming and replay-centric architectures</p></li><li><p><code>eventsourcing</code> &#8212; a Python library for event-sourced applications</p></li></ul><p>Each of these brings different trade-offs, especially around storage model, replay ergonomics, projection handling, and operational complexity.</p><div><hr></div><p><em><strong>Closing Thoughts</strong></em></p><p>Event sourcing is one of those ideas that makes engineers feel both brilliant and slightly under-caffeinated.</p><p>Used well, it gives you auditable history, time-travel debugging, reconstructable state, and a powerful foundation for replayable systems. Used carelessly, it gives you a beautiful log and a confusing pile of projections that all disagree with each other.</p><p>The research is clear: the architecture pays off when the domain truly needs history, traceability, and recovery. It also demands discipline in replay handling, versioning, snapshots, and operational design. That&#8217;s the deal.</p><p>So if your backend needs to answer not just &#8220;what is true now?&#8221; but &#8220;how did we get here, and can we prove it?&#8221;, event sourcing is worth serious consideration.</p><p>If you enjoyed this breakdown, come back tomorrow with your coffee, your curiosity, and your healthiest skepticism.<br>Until then, keep your events immutable and your projections honest.</p><p>Warmly,<br><strong>The Backend Developers</strong></p>]]></content:encoded></item><item><title><![CDATA[Cold Starts in Serverless Architectures: Latency, Caching, and Cost Trade-offs]]></title><description><![CDATA[When &#8220;It Just Scales&#8221; Meets Reality: What Cold Starts Actually Are]]></description><link>https://thebackenddevelopers.substack.com/p/cold-starts-in-serverless-architectures</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/cold-starts-in-serverless-architectures</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Wed, 08 Jul 2026 01:01:32 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/205979304/a9b5fde3cc33247a0b68c5192abf2209.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>Serverless is one of those ideas that sounds suspiciously like magic until it has to wake up before coffee.</p><p>The promise is seductive: no servers to manage, automatic scaling, and pay only for what you use. For the backend engineer, this is the equivalent of ordering a pizza and discovering the delivery person also cleaned your kitchen. But then reality arrives in the form of latency spikes, and suddenly your function is taking the scenic route to hello-world.</p><p>Cold starts are the price of that magic. They are not just &#8220;serverless being slow.&#8221; They are the accumulated cost of making something exist on demand: allocating compute, booting a runtime, loading your code, resolving dependencies, and initializing connections. Depending on your provider and runtime, different parts of that chain dominate the delay.</p><p>The important mental model is this: cold-start latency is not one problem, but a stack of problems.</p><p><em><strong>What Actually Happens During a Cold Start</strong></em></p><p>A cold start usually includes several phases:</p><ol><li><p><strong>Platform allocation</strong><br>The cloud provider creates or assigns a container, sandbox, or microVM. This is the &#8220;please wait while we find a chair&#8221; stage.</p></li><li><p><strong>Runtime bootstrap</strong><br>The language runtime starts up. A lightweight runtime like Python or Node.js often gets going faster than heavier environments such as Java.</p></li><li><p><strong>Dependency loading</strong><br>Your package, libraries, layers, and framework code are loaded. If your deployment bundle is chonky, startup gets chonkier.</p></li><li><p><strong>Application initialization</strong><br>This is where your own code does startup work: reading configuration, warming caches, creating database clients, fetching secrets, opening network connections, and doing anything else that politely says, &#8220;I&#8217;ll be ready in a sec.&#8221;</p></li><li><p><strong>First-request work</strong><br>Sometimes the first request also pays for lazy initialization that didn&#8217;t happen earlier. That means the &#8220;cold start&#8221; is not always fully over when the platform says it is.</p></li></ol><p>The reason people misdiagnose this is that they talk about latency as if it were one blob. It is not. Separating platform initialization from application initialization is critical. Otherwise, teams spend weeks tuning the wrong layer and wonder why the app still feels like it&#8217;s waking up from hibernation.</p><p><em><strong>Why Runtime Choice Matters More Than People Admit</strong></em></p><p>If your app lives in Java, you probably already know the ritual: startup begins, memory is allocated, classes are loaded, the JVM stretches its arms, and then asks for a few more minutes. Java can absolutely run serverless workloads well, but its cold-start profile is typically heavier.</p><p>Node.js and Python usually have a lower startup cost because the runtime bootstraps faster. That means they often show better tail latency on the first invocation. Tail latency matters because users do not care about your average; they care about the one request that took long enough to make them refresh the page three times and then blame DNS.</p><p>That said, a fast language does not magically erase startup variability. A tiny Python function with a giant dependency graph can still be slow. A Node.js service that opens five network connections on import day can still be awkwardly late to the party. Package size, dependency resolution, and network-bound initialization can dominate even when the runtime is light.</p><p>So yes, runtime matters. But runtime is only the opening act.</p><p><em><strong>The Real Trade-Off: Latency, Cost, and Comfort</strong></em></p><p>There are three ways to reduce the pain of cold starts:</p><ul><li><p><strong>Provisioned concurrency</strong></p></li><li><p><strong>Always-on instances</strong></p></li><li><p><strong>Pre-warming and caching</strong></p></li></ul><p>Each one improves latency by paying a different bill.</p><p>Provisioned concurrency is the most predictable option. You pay to keep instances ready so requests don&#8217;t have to wait for startup. The benefit is excellent consistency. The cost is that some of your variable serverless spend becomes fixed spend. It is the cloud equivalent of reserving a table &#8220;just in case&#8221; and paying for the seat whether or not your friend group shows up.</p><p>Always-on instances can be operationally simpler in some teams, especially when traffic is fairly stable. But you give up the elasticity that makes serverless attractive in the first place. If you&#8217;re always paying for readiness, the economic story starts to look less like &#8220;serverless&#8221; and more like &#8220;serverless-flavored hosting.&#8221;</p><p>Pre-warming and caching are more surgical. They aim to reduce how much work must happen during startup, or reduce how often that work repeats. These techniques are often cheaper and more flexible than capacity-based solutions, but they are not guarantees. They work best when your traffic has repeatable patterns.</p><p>The hard truth: there is no universal &#8220;fix&#8221; for cold starts. There is only an acceptable balance between latency, complexity, and cost.</p><p><em><strong>Caching: Your Best Friend, Until It Isn&#8217;t</strong></em></p><p>Caching is one of the most effective cold-start mitigation techniques, but only if you cache the right things.</p><p>Good candidates include:</p><ul><li><p>Database connections</p></li><li><p>Auth clients or SDK instances</p></li><li><p>Parsed configuration</p></li><li><p>Frequently used code paths</p></li><li><p>Static lookup data</p></li><li><p>In-memory results that are safe to reuse</p></li></ul><p>The idea is simple: if a piece of initialization work is repeated often, keep it around. Reuse it when the function stays warm. This can dramatically improve perceived performance.</p><p>But caching in serverless has an important limitation: warm state is opportunistic, not guaranteed. A cache in memory might survive multiple invocations, or it might vanish whenever the platform decides to recycle the instance. That means caching reduces the frequency or severity of cold starts, but it does not eliminate the underlying scaling behavior.</p><p>This is why caching works best when you understand traffic shape:</p><ul><li><p><strong>Bursty traffic</strong> may benefit a lot if invocations arrive close together.</p></li><li><p><strong>Infrequent traffic</strong> may see little benefit because instances go cold between requests.</p></li><li><p><strong>Localized state</strong> is more reusable than broad, request-specific state.</p></li></ul><p>If your workload looks like &#8220;one request every 20 minutes,&#8221; your in-memory cache is basically a very expensive diary.</p><p><em><strong>A Practical Python Example: Reusing Initialization Work</strong></em></p><p>Here&#8217;s a small Python example showing how to reduce repeated startup cost in a serverless function by initializing expensive objects outside the handler.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9fbca9d9-a4e9-450a-adea-a1cd74644ac7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import json
import time
import os

# Simulate expensive setup
start_init = time.time()

# Imagine this is a database client, SDK, or secrets manager client
class FakeDBClient:
    def __init__(self):
        time.sleep(1.2)  # expensive initialization
        self.connected = True

    def query(self):
        return {"message": "data from warm client"}

db_client = FakeDBClient()

INIT_TIME = time.time() - start_init
print(f"Initialization took: {INIT_TIME:.2f}s")


def lambda_handler(event, context):
    start = time.time()

    # Reuse the pre-initialized client
    result = db_client.query()

    duration = time.time() - start
    return {
        "statusCode": 200,
        "body": json.dumps({
            "result": result,
            "handler_time_ms": round(duration * 1000, 2),
            "init_time_ms": round(INIT_TIME * 1000, 2)
        })
    }</code></pre></div><p>What this does well:</p><ul><li><p>Moves costly setup outside the request handler</p></li><li><p>Allows warm invocations to reuse the same client</p></li><li><p>Reduces per-request latency after the first start</p></li></ul><p>What it does not do:</p><ul><li><p>Remove the first cold start</p></li><li><p>Guarantee the instance stays warm</p></li><li><p>Fix slow dependency loading or giant deployment packages</p></li></ul><p>In a real AWS Lambda, Azure Function, or Google Cloud Function, this pattern is common. The trick is to initialize only what benefits from reuse, and keep the startup path lean.</p><p><em><strong>Observability: The Difference Between Guessing and Knowing</strong></em></p><p>Cold starts are notorious for being overestimated, underestimated, and misattributed. Sometimes the slow request is truly a cold start. Sometimes it is a database connection timeout. Sometimes it is a downstream API taking a nap. Sometimes it is your own logging layer trying to write a novel.</p><p>This is why observability is not optional.</p><p>You need:</p><ul><li><p><strong>Structured logs</strong> to identify whether a request was a cold start</p></li><li><p><strong>Tracing</strong> to see where time is spent across startup phases</p></li><li><p><strong>Metrics</strong> to track p50, p95, and p99 latency separately</p></li><li><p><strong>Benchmarks</strong> to compare runtimes, memory allocations, and deployment sizes</p></li></ul><p>With these tools, teams can answer important questions:</p><ul><li><p>Is the delay in platform startup or application initialization?</p></li><li><p>Does increasing memory reduce cold-start time?</p></li><li><p>Are cold starts happening only after idle periods?</p></li><li><p>Is a specific dependency causing the delay?</p></li><li><p>Would provisioned concurrency actually pay off?</p></li></ul><p>Without measurement, teams often throw money at the problem in the form of blanket provisioned capacity. With measurement, they can apply the right fix to the right workload.</p><p>That is a much nicer use of budget than &#8220;we&#8217;re not sure why this is slow, so let&#8217;s pay more.&#8221;</p><p><em><strong>How Traffic Patterns Decide Everything</strong></em></p><p>The best cold-start strategy depends heavily on traffic shape.</p><p><em><strong>High-traffic, latency-sensitive workloads</strong></em></p><p>If you have an API serving users around the clock, especially if latency SLOs matter, cold starts can be painful enough to justify provisioned concurrency or always-on capacity. Predictability becomes more valuable than raw elasticity.</p><p>Examples:</p><ul><li><p>Login endpoints</p></li><li><p>Checkout flows</p></li><li><p>Real-time APIs</p></li><li><p>Event-driven systems with strict deadlines</p></li></ul><p><em><strong>Bursty workloads</strong></em></p><p>If traffic arrives in spikes and then goes quiet, cold starts may happen, but they may be acceptable if the total cost stays lower. In these cases, caching and pre-warming can help reduce pain without locking you into fixed spend.</p><p>Examples:</p><ul><li><p>Scheduled jobs</p></li><li><p>Marketing campaigns</p></li><li><p>Upload processing</p></li><li><p>Notification bursts</p></li></ul><p><em><strong>Cost-sensitive workloads</strong></em></p><p>If the workload is background-oriented or user-visible latency is not critical, occasional cold starts may be the correct trade-off. The beauty of serverless is that sometimes you can let the system be a little lazy and still win economically.</p><p>Examples:</p><ul><li><p>Internal tools</p></li><li><p>Batch tasks</p></li><li><p>Low-frequency automation</p></li><li><p>Administrative functions</p></li></ul><p>The broader question is not whether cold starts exist. They do. The real question is whether the latency variance they introduce is acceptable for the amount of money and operational simplicity you save.</p><p><em><strong>Provisioned Concurrency vs Always-On vs Optimized Warm Paths</strong></em></p><p>Let&#8217;s make the trade-offs explicit.</p><p><em><strong>Provisioned concurrency</strong></em></p><ul><li><p>Best for predictable performance</p></li><li><p>Reduces latency variance</p></li><li><p>Converts variable cost into fixed cost</p></li><li><p>Good for critical paths</p></li></ul><p><em><strong>Always-on instances</strong></em></p><ul><li><p>Simple conceptually</p></li><li><p>Useful in stable workloads</p></li><li><p>Often undermines serverless cost advantages</p></li><li><p>Can be easier to reason about operationally</p></li></ul><p><em><strong>Caching and warm-start optimization</strong></em></p><ul><li><p>Lowers repeated setup cost</p></li><li><p>Often the cheapest first move</p></li><li><p>Works best with repeat invocations on warm instances</p></li><li><p>Does not guarantee cold-start elimination</p></li></ul><p>A sensible architecture often combines them. For example:</p><ul><li><p>Use a fast runtime</p></li><li><p>Keep dependencies slim</p></li><li><p>Reuse connections</p></li><li><p>Add targeted provisioned concurrency only on the hottest endpoints</p></li><li><p>Measure continuously</p></li></ul><p>That hybrid model is usually more practical than trying to exorcise cold starts from the entire platform like a cloud priest with a very expensive wand.</p><p><em><strong>A Simple Pattern for Reusing Connections in Python</strong></em></p><p>Here&#8217;s a more realistic example showing how to keep a database connection or client alive between invocations:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;76a2a3d9-7434-445a-a40a-f429bbd1e091&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import os
import psycopg2

connection = None

def get_connection():
    global connection

    if connection is None or connection.closed != 0:
        connection = psycopg2.connect(
            host=os.environ["DB_HOST"],
            dbname=os.environ["DB_NAME"],
            user=os.environ["DB_USER"],
            password=os.environ["DB_PASSWORD"],
            connect_timeout=3
        )

    return connection


def handler(event, context):
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT now();")
    row = cursor.fetchone()
    cursor.close()

    return {
        "statusCode": 200,
        "body": f"Current time: {row[0]}"
    }</code></pre></div><p>This pattern helps because:</p><ul><li><p>The connection is created once during a warm lifecycle</p></li><li><p>Subsequent requests reuse the same connection</p></li><li><p>The handler avoids connection setup on every invocation</p></li></ul><p>Caution:</p><ul><li><p>Serverless environments can freeze or recycle instances</p></li><li><p>You still need reconnect logic</p></li><li><p>Connection pooling often needs special attention in serverless contexts</p></li></ul><p>If your database has feelings, this is the part where it appreciates being treated gently.</p><p><em><strong>Libraries and Services Worth Knowing</strong></em></p><p>If you want to explore this space further, these are useful references and tools:</p><ul><li><p><strong>AWS Lambda Provisioned Concurrency</strong></p></li><li><p><strong>AWS Lambda SnapStart</strong> for Java workloads</p></li><li><p><strong>Azure Functions Premium Plan</strong></p></li><li><p><strong>Google Cloud Functions / Cloud Run</strong> with warm instance tuning</p></li><li><p><strong>Datadog</strong> for traces and latency metrics</p></li><li><p><strong>New Relic</strong> for observability</p></li><li><p><strong>OpenTelemetry</strong> for tracing and metrics instrumentation</p></li><li><p><strong>AWS X-Ray</strong> for request tracing</p></li><li><p><strong>psycopg2</strong> or <strong>SQLAlchemy</strong> for Python database connectivity</p></li><li><p><strong>Boto3</strong>, <strong>google-cloud-*</strong>, and <strong>Azure SDKs</strong> for cloud service client reuse</p></li><li><p><strong>Serverless Framework</strong> and <strong>AWS SAM</strong> for deployment and experimentation</p></li><li><p><strong>Knative</strong> and <strong>Cloud Run</strong> for container-based serverless patterns</p></li></ul><p><em><strong>The Bottom Line</strong></em></p><p>Cold starts are not a myth, and they are not a reason to abandon serverless. They are a design constraint. Once you accept that they come from initialization overhead&#8212;platform startup, runtime bootstrapping, dependency loading, and first-request work&#8212;you can manage them intelligently.</p><p>The best solution is rarely &#8220;eliminate them completely.&#8221; It is usually:</p><ul><li><p>choose a faster runtime,</p></li><li><p>keep dependencies lean,</p></li><li><p>reuse expensive objects,</p></li><li><p>cache where reuse is realistic,</p></li><li><p>instrument everything,</p></li><li><p>and selectively pay for readiness only where it matters.</p></li></ul><p>That is the real backend developer move: not chasing perfection, but choosing the right compromise for the workload in front of you.</p><p><em><strong>Warm signoff</strong></em></p><p>That&#8217;s it for today&#8217;s dispatch from <strong>The Backend Developers</strong>. If this helped you reason more clearly about serverless latency, caching, and cost, come back tomorrow for another practical deep dive&#8212;with fewer buzzwords and more useful engineering. Follow along, stay curious, and may your cold starts be rare, your p99s be kind, and your dashboards unusually calm.</p>]]></content:encoded></item><item><title><![CDATA[Kubernetes Cost Optimization in 2026: Rightsizing, Autoscaling, and Multi-Cloud Trade-offs]]></title><description><![CDATA[Kubernetes Cost Optimization in 2026: The Art of Not Paying for Idle Containers]]></description><link>https://thebackenddevelopers.substack.com/p/kubernetes-cost-optimization-in-2026</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/kubernetes-cost-optimization-in-2026</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Tue, 30 Jun 2026 20:18:06 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/204335492/931994311d74e68563f6d2c965e732ff.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>If Kubernetes were a person, it would be that extremely capable friend who helps you move apartments, optimize your taxes, and build a home theater&#8212;then casually bills you for three extra trucks, six full days, and a &#8220;premium coordination fee.&#8221; It is brilliant. It is powerful. And left unattended, it will happily turn your cloud bill into a small national debt.</p><p>In 2026, Kubernetes cost optimization is no longer about one heroic cleanup sprint where somebody finds a few oversized deployments and declares victory with a spreadsheet. The real savings come from treating cost as a living system: measured continuously, tuned conservatively, and aligned with workload behavior, autoscaling policy, and cloud placement strategy.</p><p>That is the big shift. Cost optimization has matured from &#8220;trim the fat&#8221; into &#8220;design the body correctly.&#8221;</p><p><em><strong>Why Kubernetes Costs Still Surprise Teams in 2026</strong></em></p><p>The core reason Kubernetes bills keep surprising teams is simple: Kubernetes does not bill you for how busy your containers are. It bills you for what you reserve, what you provision, and what you move around.</p><p>That means several things can inflate spend even when your app looks &#8220;fine&#8221;:</p><ul><li><p>CPU and memory requests are too high</p></li><li><p>nodes are larger than needed</p></li><li><p>pods do not scale down when traffic drops</p></li><li><p>workloads run on on-demand compute when they could tolerate cheaper options</p></li><li><p>traffic crosses cloud boundaries and quietly accrues egress costs</p></li><li><p>teams run the same observability and control-plane stack in multiple clouds because &#8220;portability&#8221;</p></li></ul><p>The last one is especially expensive. Multi-cloud sounds wonderfully strategic in board slides, but in real life it often means duplicated tooling, more operational work, more fragmented visibility, and more bills with line items that feel personally offended by your existence.</p><p>The 2026 lesson is not &#8220;never use multi-cloud.&#8221; The lesson is: use it for resilience, regulatory needs, bargaining power, or specific workloads&#8212;not as a default cost-saving architecture. Multi-cloud can absolutely be justified. It just rarely saves money by itself.</p><p><em><strong>Rightsizing: Still the Highest-Confidence Win</strong></em></p><p>Rightsizing remains the most reliable Kubernetes cost lever in 2026.</p><p>Why? Because requests drive scheduling, node packing, and infrastructure allocation. If your workloads ask for far more CPU or memory than they actually need, you create artificial scarcity. The cluster then spins up more capacity than necessary, and you pay for that idle headroom.</p><p>But the best teams do not treat rightsizing as a one-time cleanup project. They treat it as an ongoing measurement discipline.</p><p>Here is the practical pattern that works:</p><ol><li><p>Measure actual utilization over time</p></li><li><p>Compare it against requested CPU and memory</p></li><li><p>Identify chronic overprovisioning at the workload and namespace level</p></li><li><p>Reduce requests gradually</p></li><li><p>Watch for latency, throttling, and OOM regressions</p></li><li><p>Repeat</p></li></ol><p>There is an important distinction between CPU and memory:</p><ul><li><p><strong>CPU</strong> is often easier to tune aggressively</p></li><li><p><strong>Memory</strong> is usually the harder constraint and the bigger reliability risk</p></li></ul><p>That is because CPU throttling might slow a service down, but memory pressure can kill it outright. A service that survives slower response times is annoying. A service that gets OOM-killed in production is a meeting.</p><p>A healthy rightsizing approach is conservative. You do not want to slash requests until your pod becomes a stress experiment.</p><p><em><strong>Using Data Instead of Vibes</strong></em></p><p>The biggest improvement in mature optimization programs is visibility.</p><p>Teams that combine allocation data, labels, namespaces, and workload-level spend views can connect engineering decisions to financial outcomes. That is the difference between:</p><ul><li><p>&#8220;We think this namespace is expensive&#8221;</p></li><li><p>and</p></li><li><p>&#8220;This deployment is responsible for 17% of monthly spend, and its average utilization suggests we can safely reduce requests by 30%.&#8221;</p></li></ul><p>This is where OpenCost and Kubecost-style reporting shine. They create the bridge between infrastructure telemetry and FinOps accountability. Once cost is attributed at the workload, namespace, and label level, you can finally have meaningful conversations about responsibility, trade-offs, and savings.</p><p>Without visibility, cost optimization is a guessing game. With visibility, it becomes engineering.</p><p>If your teams already use labels consistently, you are ahead of the pack. If not, you should fix that before you try to optimize anything else. Labels are not just metadata; they are the breadcrumbs that let you trace spend back to a team, product, environment, or customer.</p><p><em><strong>Example: Checking Requests Against Real Usage in Python</strong></em></p><p>Below is a simplified Python example that compares average usage against Kubernetes requests and suggests conservative reductions. This is not production-grade recommendation logic, but it shows the basic idea.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c42128b3-1340-414e-8ffa-eec1104c321b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from dataclasses import dataclass

@dataclass
class Workload:
    name: str
    cpu_request_millicores: int
    cpu_avg_usage_millicores: int
    memory_request_mib: int
    memory_avg_usage_mib: int

def recommend_rightsizing(workload: Workload, cpu_buffer=1.5, memory_buffer=1.3):
    """
    Recommend new requests based on observed averages plus a safety buffer.
    CPU can usually be tuned more aggressively than memory.
    """
    recommended_cpu = int(workload.cpu_avg_usage_millicores * cpu_buffer)
    recommended_memory = int(workload.memory_avg_usage_mib * memory_buffer)

    cpu_reduction = 100 * (1 - recommended_cpu / workload.cpu_request_millicores)
    mem_reduction = 100 * (1 - recommended_memory / workload.memory_request_mib)

    return {
        "name": workload.name,
        "current_cpu_request": workload.cpu_request_millicores,
        "recommended_cpu_request": recommended_cpu,
        "cpu_reduction_percent": round(cpu_reduction, 1),
        "current_memory_request": workload.memory_request_mib,
        "recommended_memory_request": recommended_memory,
        "memory_reduction_percent": round(mem_reduction, 1),
    }

services = [
    Workload("payments-api", 1000, 280, 2048, 1200),
    Workload("orders-worker", 500, 180, 1024, 720),
    Workload("search-api", 1500, 900, 3072, 2400),
]

for svc in services:
    rec = recommend_rightsizing(svc)
    print(rec)</code></pre></div><p>A few things to notice here:</p><ul><li><p>We use a safety buffer rather than matching usage exactly</p></li><li><p>We recommend CPU and memory separately</p></li><li><p>Memory gets a more cautious buffer</p></li><li><p>The output can be reviewed before applying anything</p></li></ul><p>That last point matters. Cost optimization should be reviewed like any production change, because it is one.</p><p><em><strong>Autoscaling: Not a Feature, a Control System</strong></em></p><p>Autoscaling in 2026 is best understood as a multi-layer control system.</p><p>There are at least four layers involved:</p><ul><li><p><strong>HPA (Horizontal Pod Autoscaler)</strong>: scales pods based on demand signals</p></li><li><p><strong>VPA (Vertical Pod Autoscaler)</strong>: suggests or applies resource changes to pod requests</p></li><li><p><strong>KEDA</strong>: scales workloads based on events, queues, streams, and custom signals</p></li><li><p><strong>Cluster autoscaling or Karpenter-like provisioning</strong>: adds or removes nodes to match demand</p></li></ul><p>The savings happen when these layers work together.</p><p>The trap is assuming autoscaling is a single knob. It is not. It is a set of interacting controllers, and if you apply them carelessly, they can fight each other.</p><p>For example:</p><ul><li><p>HPA and VPA can conflict if VPA keeps changing requests that HPA uses as a baseline</p></li><li><p>HPA alone may scale pods efficiently but leave too many underutilized nodes</p></li><li><p>Cluster autoscaling alone cannot fix bloated pod requests</p></li><li><p>KEDA is excellent for event-driven workloads but irrelevant for always-on APIs</p></li></ul><p>The modern pattern is:</p><ol><li><p>Right-size workloads</p></li><li><p>Use HPA for real traffic elasticity</p></li><li><p>Use VPA carefully, often in recommendation mode first</p></li><li><p>Use KEDA where the workload is queue- or event-driven</p></li><li><p>Use cluster autoscaling or Karpenter to shrink the infrastructure underneath</p></li></ol><p>That last step is where the real infrastructure savings appear. If your workloads scale down but your nodes do not, you have only moved the waste around.</p><p><em><strong>Example: A Simple HPA Mental Model in JavaScript</strong></em></p><p>This example is intentionally simplified. It shows the logic behind autoscaling decisions based on CPU utilization.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;7c2b474e-bc01-42d0-8bd4-b4e36897b65b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">function desiredReplicas(currentReplicas, currentCpuUtilization, targetCpuUtilization) {
  const ratio = currentCpuUtilization / targetCpuUtilization;
  const scaled = Math.ceil(currentReplicas * ratio);
  return Math.max(1, scaled);
}

const currentReplicas = 4;
const currentCpuUtilization = 78; // percent
const targetCpuUtilization = 60;  // percent

console.log(
  "Recommended replicas:",
  desiredReplicas(currentReplicas, currentCpuUtilization, targetCpuUtilization)
);</code></pre></div><p>This is the basic idea behind HPA: keep utilization near a target by scaling replicas up or down. In real Kubernetes setups, the signal sources, stabilization windows, and policies matter a lot, because without guardrails you can get scaling flaps that resemble panic rather than optimization.</p><p><em><strong>Why Visibility and Autoscaling Must Be Paired</strong></em></p><p>Autoscaling without visibility is like putting a turbocharger on a car and forgetting to check whether it has wheels.</p><p>You need workload-level spend data to answer questions like:</p><ul><li><p>Which team is driving this cost?</p></li><li><p>Is the expensive service actually over-requested?</p></li><li><p>Are we scaling because of real demand or because our requests are too high?</p></li><li><p>Are we paying for node headroom that never gets used?</p></li><li><p>Which namespaces are consistently wasteful?</p></li></ul><p>This is why mature teams connect OpenCost/Kubecost reporting with FinOps workflows. Once cost is tied to a workload, a namespace, or a label, you can set budgets, create ownership, and make reductions actionable.</p><p>Showback and chargeback are not just accounting theater. Done well, they create accountability without turning engineering into a blame festival.</p><p><em><strong>The Tooling Stack That Actually Works</strong></em></p><p>The strongest optimization stacks in 2026 combine policy, visibility, and provisioning tactics.</p><p>Common pieces include:</p><ul><li><p><strong>Goldilocks</strong> for right-sizing recommendations</p></li><li><p><strong>OpenCost</strong> or <strong>Kubecost</strong> for allocation and attribution</p></li><li><p><strong>Karpenter</strong> or cluster autoscalers for node-level elasticity</p></li><li><p><strong>HPA / VPA / KEDA</strong> for workload scaling</p></li><li><p><strong>Spot instances</strong> for interruptible or tolerant workloads</p></li><li><p>Admission policies and workload classes to keep teams from wandering into chaos</p></li></ul><p>The key insight is that no single tool solves Kubernetes cost. Each tool solves one layer of the stack.</p><p>But every added tool also adds:</p><ul><li><p>configuration complexity</p></li><li><p>operational overhead</p></li><li><p>failure modes</p></li><li><p>more things to monitor at 2:13 AM when someone says &#8220;it was working yesterday&#8221;</p></li></ul><p>So the best teams standardize first:</p><ul><li><p>define workload classes</p></li><li><p>establish guardrails</p></li><li><p>agree on criticality tiers</p></li><li><p>then automate savings inside those boundaries</p></li></ul><p>That is how you avoid creating an optimization program that costs more to run than it saves.</p><p><em><strong>Spot Instances: Cheap, Effective, and Slightly Dramatic</strong></em></p><p>Spot instances remain one of the most powerful ways to cut compute cost in 2026, especially for stateless, interruptible, or queue-driven workloads.</p><p>They can slash costs materially, but they demand architecture discipline.</p><p>Good candidates for spot:</p><ul><li><p>batch jobs</p></li><li><p>CI runners</p></li><li><p>workers that can resume after interruption</p></li><li><p>horizontally scaled stateless services with enough replicas</p></li></ul><p>Poor candidates for spot:</p><ul><li><p>single-instance databases</p></li><li><p>latency-sensitive critical paths</p></li><li><p>anything without graceful shutdown and rescheduling logic</p></li></ul><p>The winning pattern is to use spot selectively, not everywhere. The goal is not &#8220;save money by making production gamble with destiny.&#8221; The goal is to move the right workloads onto cheaper capacity while keeping reliability intact.</p><p><em><strong>Multi-Cloud: Portability Has a Price Tag</strong></em></p><p>Multi-cloud is one of the most misunderstood cost topics in Kubernetes.</p><p>On paper, it promises:</p><ul><li><p>resilience</p></li><li><p>provider leverage</p></li><li><p>flexibility</p></li><li><p>geographic options</p></li></ul><p>In practice, it often introduces:</p><ul><li><p>separate managed Kubernetes fees</p></li><li><p>duplicated observability stacks</p></li><li><p>different pricing models</p></li><li><p>higher storage and networking overhead</p></li><li><p>significant egress charges</p></li><li><p>more engineering complexity</p></li></ul><p>Egress deserves special attention. Data transfer across clouds can become a silent budget assassin. You think you are saving money by being &#8220;portable,&#8221; and then your bill arrives wearing brass knuckles.</p><p>The cost question is not &#8220;Can Kubernetes run in multiple clouds?&#8221; Of course it can.</p><p>The real question is: <strong>Does the business value of multi-cloud outweigh the operational and financial overhead?</strong></p><p>Sometimes yes. Often no. Almost never for cost alone.</p><p>The most sensible use of multi-cloud in 2026 is selective:</p><ul><li><p>regulatory requirements</p></li><li><p>customer or market presence</p></li><li><p>redundancy strategy</p></li><li><p>vendor negotiation leverage</p></li></ul><p>If your only reason is &#8220;we don&#8217;t want lock-in,&#8221; that is not a cost strategy. That is a philosophy. And philosophy does not pay cloud invoices.</p><p><em><strong>The 2026 Mindset: Optimize the System, Not the Knob</strong></em></p><p>The most important research-backed insight is that cost optimization is now a system design problem.</p><p>You do not get durable savings by tuning one parameter in isolation. You get them by aligning:</p><ul><li><p>requests and limits</p></li><li><p>HPA/VPA/KEDA behavior</p></li><li><p>cluster provisioning</p></li><li><p>node pool strategy</p></li><li><p>spot/on-demand mix</p></li><li><p>cloud placement</p></li><li><p>workload criticality</p></li></ul><p>That means engineering, platform, and FinOps need to collaborate.</p><p>This is where teams often go wrong. One group optimizes for reliability, another for cost, and a third for portability. Everyone is technically right, and the bill remains emotionally unavailable.</p><p>The winning organizations treat cost and reliability as paired goals:</p><ul><li><p>Critical workloads get safer headroom</p></li><li><p>Elastic workloads get aggressive autoscaling</p></li><li><p>Interruptible jobs go on cheaper compute</p></li><li><p>Cross-cloud traffic is minimized</p></li><li><p>Every request is justified by evidence</p></li></ul><p>That is the difference between spending money on compute and accidentally sponsoring it.</p><p><em><strong>A Practical Optimization Workflow for 2026</strong></em></p><p>If I had to boil this down into an operational sequence, it would look like this:</p><ol><li><p><strong>Establish visibility</strong></p><ul><li><p>Use OpenCost, Kubecost, or similar tooling</p></li><li><p>Ensure labels, namespaces, and ownership are consistent</p></li></ul></li><li><p><strong>Rank by spend</strong></p><ul><li><p>Find the top workloads and namespaces by cost</p></li><li><p>Don&#8217;t optimize randomly; optimize where the money is</p></li></ul></li><li><p><strong>Right-size carefully</strong></p><ul><li><p>Compare usage to requests</p></li><li><p>Reduce CPU first when safe</p></li><li><p>Be conservative with memory</p></li></ul></li><li><p><strong>Tune autoscaling</strong></p><ul><li><p>Use HPA for demand-driven services</p></li><li><p>Use VPA carefully, often in recommend-only mode first</p></li><li><p>Use KEDA for event-driven systems</p></li></ul></li><li><p><strong>Reduce node waste</strong></p><ul><li><p>Add cluster autoscaling or Karpenter</p></li><li><p>Review bin packing efficiency</p></li><li><p>Shrink node pools where possible</p></li></ul></li><li><p><strong>Introduce cheap capacity selectively</strong></p><ul><li><p>Use Spot for tolerant workloads</p></li><li><p>Keep fallback capacity for critical services</p></li></ul></li><li><p><strong>Re-evaluate cloud placement</strong></p><ul><li><p>Check egress and storage costs</p></li><li><p>Avoid multi-cloud unless the business case is real</p></li></ul></li><li><p><strong>Repeat continuously</strong></p><ul><li><p>Rightsizing is not a project</p></li><li><p>It is a practice</p></li></ul></li></ol><p><em><strong>Example Libraries and Services Worth Knowing</strong></em></p><p>A few popular tools and services that fit into this space:</p><ul><li><p><strong>OpenCost</strong> &#8212; open-source Kubernetes cost monitoring and allocation</p></li><li><p><strong>Kubecost</strong> &#8212; commercial cost monitoring and FinOps workflows for Kubernetes</p></li><li><p><strong>Goldilocks</strong> &#8212; resource request recommendations based on Vertical Pod Autoscaler data</p></li><li><p><strong>Karpenter</strong> &#8212; node provisioning and cluster cost efficiency for AWS</p></li><li><p><strong>Cluster Autoscaler</strong> &#8212; scales node groups based on scheduling needs</p></li><li><p><strong>KEDA</strong> &#8212; event-driven autoscaling for Kubernetes</p></li><li><p><strong>Prometheus</strong> &#8212; metrics backbone for usage and autoscaling signals</p></li><li><p><strong>Grafana</strong> &#8212; dashboards for visibility and analysis</p></li><li><p><strong>Vertical Pod Autoscaler (VPA)</strong> &#8212; recommends or applies pod request changes</p></li></ul><p><em><strong>Closing Thoughts</strong></em></p><p>Kubernetes cost optimization in 2026 is not about chasing the cheapest possible bill at any cost. It is about building a system where spend reflects actual business value, where elasticity matches workload behavior, and where cloud architecture supports both reliability and financial discipline.</p><p>Rightsizing gives you the clearest direct savings. Autoscaling keeps those savings alive. Visibility makes the savings real. And cloud placement decides whether your &#8220;portable&#8221; architecture is elegant or just expensive with better branding.</p><p>If you take only one idea from this post, let it be this:<br><strong>optimize Kubernetes as a coupled system, not as a collection of independent tweaks.</strong></p><p>That is where durable savings live.</p><p>Warmly,<br>See you tomorrow in <strong>The Backend Developers</strong>&#8212;bring your clusters, your metrics, and perhaps a mild suspicion that some of your pods are living much larger lives than they need to.</p>]]></content:encoded></item><item><title><![CDATA[Browser-Based MCP for Frontend Apps: Context, Security, and Tool Reliability]]></title><description><![CDATA[Browser-Based MCP for Frontend Apps: Why the Browser Shouldn&#8217;t Become a Tiny Chaos Server]]></description><link>https://thebackenddevelopers.substack.com/p/browser-based-mcp-for-frontend-apps</link><guid isPermaLink="false">https://thebackenddevelopers.substack.com/p/browser-based-mcp-for-frontend-apps</guid><dc:creator><![CDATA[Ankur Yadav]]></dc:creator><pubDate>Wed, 24 Jun 2026 20:01:23 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/202064955/3af331d5e173cfa560f06f54f05a1036.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>If you&#8217;ve spent any time building frontend apps lately, you&#8217;ve probably noticed a very modern pattern creeping in from the edges: the browser is no longer just rendering buttons, forms, and lovingly overworked spinners. It&#8217;s also becoming a place where models observe, suggest, and sometimes act.</p><p>That&#8217;s where browser-based MCP enters the scene.</p><p>And like most things in software, the idea sounds elegant right up until it meets reality, security reviews, flaky DOMs, and a user who clicked three things before the model finished &#8220;thinking.&#8221;</p><p>Browser-based MCP for frontend apps is not about turning the browser into a magical all-powerful robot shell. The strongest pattern is much more humble: use the browser as a thin orchestration layer. Let it capture visible context, user intent, and local state. Then hand off sensitive, privileged, or high-risk actions to a server boundary or tightly scoped service.</p><p>That distinction matters a lot.</p><p>Because once you treat the browser like a place to expose raw power, you&#8217;re basically inviting every security problem on the internet to a little party in your app. And unlike normal parties, this one includes prompt injection, credential leakage, cross-origin weirdness, and the kind of unpredictable behavior that makes engineers stare at logs like they&#8217;re reading tea leaves.</p><p><em><strong>What Browser-Based MCP Is Actually Good At</strong></em></p><p>At a practical level, browser-based MCP is best thought of as an orchestration pattern that connects three worlds:</p><ol><li><p>the user&#8217;s visible UI state,</p></li><li><p>the model&#8217;s reasoning,</p></li><li><p>external tools or services that can do useful work.</p></li></ol><p>The browser is especially good at the first part. It knows what the user is seeing, what they clicked, what form they filled, and what route they&#8217;re on. That is valuable context. But context is not the same thing as privilege.</p><p>A browser can safely observe. It should not automatically be trusted to execute everything.</p><p>The strongest architecture keeps the model&#8217;s &#8220;reach&#8221; intentionally limited. The frontend collects context, normalizes it, and emits structured tool calls. Those calls are then routed through a well-defined adapter layer to backend services or scoped browser APIs. This keeps protocol complexity out of your UI code and avoids the dreaded &#8220;random script spaghetti with AI seasoning.&#8221;</p><p>In other words: successful MCP-style browser integrations should feel like proper client libraries, not like someone duct-taped an LLM onto <code>document.body</code>.</p><p><em><strong>The Thin Orchestration Layer Pattern</strong></em></p><p>The central lesson from the research is clear: browser-based MCP works best when it is thin.</p><p>That means:</p><ul><li><p>the browser captures context,</p></li><li><p>the app translates UI events into structured actions,</p></li><li><p>the model proposes or selects actions,</p></li><li><p>the real risky work happens behind stronger boundaries.</p></li></ul><p>Why is this so effective?</p><p>Because browsers are noisy. They are full of untrusted content, third-party scripts, extensions, cross-origin frames, user-generated HTML, and state that changes without warning. If you let the model directly manipulate raw browser internals, every page becomes a potential attack surface.</p><p>A thin orchestration layer gives you:</p><ul><li><p>clearer data flow,</p></li><li><p>less protocol leakage into UI code,</p></li><li><p>reduced attack surface,</p></li><li><p>better debuggability,</p></li><li><p>easier policy enforcement.</p></li></ul><p>This is one of those rare architecture choices where &#8220;less ambitious&#8221; is actually &#8220;more shippable.&#8221;</p><p><em><strong>Context Is Valuable, But It Is Not Free</strong></em></p><p>The browser is a fantastic source of context because it sees what the user sees. That sounds obvious, but it&#8217;s the whole ballgame for assistant-style experiences.</p><p>Useful browser context can include:</p><ul><li><p>visible text on the page,</p></li><li><p>selected elements,</p></li><li><p>current route or tab state,</p></li><li><p>form values the user is actively entering,</p></li><li><p>application-specific metadata,</p></li><li><p>user actions and event history.</p></li></ul><p>This context improves model quality dramatically. The assistant can answer in a way that reflects the actual UI state instead of hallucinating from a blank slate.</p><p>But there&#8217;s a tradeoff: every extra bit of context increases risk.</p><p>For example:</p><ul><li><p>reading more DOM can expose sensitive data,</p></li><li><p>collecting cross-origin data can violate boundaries,</p></li><li><p>persisting credentials can create exfiltration risk,</p></li><li><p>shipping too much state to the model can leak private information,</p></li><li><p>exposing too much application internals makes prompt injection more dangerous.</p></li></ul><p>The best systems minimize what they expose, sanitize aggressively, and scope every piece of context. In practice, that means you should not send the model the entire page if a summary of the visible component tree will do.</p><p>The browser should not become an indiscriminate data vacuum.</p><p><em><strong>Security: The Main Character in This Story</strong></em></p><p>If browser-based MCP had a boss fight, this would be it.</p><p>Security is the dominant constraint in browser-exposed MCP designs. Not performance. Not elegance. Not even developer ergonomics, though those all matter. Security is what decides whether the architecture survives contact with production.</p><p>The recurring security principles are straightforward, but they need discipline:</p><ul><li><p>least privilege,</p></li><li><p>explicit authorization,</p></li><li><p>sandboxing,</p></li><li><p>strict validation of inputs and outputs,</p></li><li><p>careful handling of untrusted content,</p></li><li><p>user-visible confirmations for sensitive actions.</p></li></ul><p>That last one is important. If a model is about to do something meaningful on behalf of a user, the user should know what is happening. Maybe not every tiny operation needs a modal dialog from the depths of despair, but meaningful actions should be inspectable or approvable.</p><p>The browser is inherently an untrusted environment. The page itself may be adversarial. So may embedded content. So may extension interactions. And yes, model outputs can also be untrustworthy if you treat them as commands instead of suggestions.</p><p>One of the biggest threats here is prompt injection. In browser contexts, a malicious page or piece of content can try to manipulate the model by embedding instructions inside what looks like normal text. That means the system should never assume visible content is benign. You need policy boundaries between what the model can read and what it can command.</p><p>A good rule: the model may observe content, but it should not be allowed to blindly obey content.</p><p><em><strong>Reliability: Because &#8220;Sometimes It Works&#8221; Is Not a Product Strategy</strong></em></p><p>Let&#8217;s talk about the second boss fight: tool reliability.</p><p>Browser-exposed tools tend to fail in ways that are wonderfully annoying:</p><ul><li><p>latency spikes,</p></li><li><p>network timeouts,</p></li><li><p>flaky state dependencies,</p></li><li><p>DOM changes,</p></li><li><p>partial completion,</p></li><li><p>nondeterministic element selection,</p></li><li><p>transient auth errors.</p></li></ul><p>This is why browser-based MCP needs serious operational discipline.</p><p>Reliable implementations usually include:</p><ul><li><p>timeouts,</p></li><li><p>retries with exponential backoff,</p></li><li><p>idempotent tool design,</p></li><li><p>clear error surfaces,</p></li><li><p>observability hooks,</p></li><li><p>recovery flows when an action partially succeeds.</p></li></ul><p>Idempotency deserves a special mention. If a tool is going to be retried, it should not accidentally double-charge a user, duplicate a record, or submit the same form twice because the network sneezed.</p><p>Clear error surfaces matter just as much. If the tool fails, the frontend should know whether it failed because of authentication, stale UI state, blocked permissions, or something else entirely. &#8220;Unknown error&#8221; is not a diagnosis; it&#8217;s a cry for help.</p><p>And observability is not optional. If you can&#8217;t trace how a tool call moved through the frontend, adapter, model, and backend, then debugging becomes a folklore-based profession.</p><p><em><strong>What Good Frontend Integrations Look Like</strong></em></p><p>The most practical browser-based MCP implementations tend to use familiar frontend patterns:</p><ul><li><p>SDK wrappers,</p></li><li><p>event-driven tool calls,</p></li><li><p>adapter layers,</p></li><li><p>isolated service clients,</p></li><li><p>normalized responses.</p></li></ul><p>This is important because frontend teams already know how to build maintainable client code. The trick is not to invent a new style of app architecture just because an LLM is in the room.</p><p>Instead, translate UI actions into structured calls.</p><p>For example:</p><ul><li><p>user clicks &#8220;summarize this page,&#8221;</p></li><li><p>app captures visible content,</p></li><li><p>adapter sends a structured request to the MCP tool,</p></li><li><p>model returns a response,</p></li><li><p>UI renders the result.</p></li></ul><p>That&#8217;s clean. That&#8217;s debuggable. That&#8217;s a client library with taste.</p><p>What you want to avoid is embedding protocol logic all over your component tree. Once that happens, your app becomes a haunted house of side effects.</p><p><em><strong>A Simple Frontend Pattern in JavaScript</strong></em></p><p>Below is a minimal example of how a browser app might capture context and call an MCP-style tool through an adapter layer.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;c3bbe31d-0c1b-4944-9022-9afd359ed2cc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// A small adapter around an MCP-style tool invocation.
// In real apps, this would likely talk to your backend or extension service.

class BrowserMCPClient {
  constructor({ endpoint, token }) {
    this.endpoint = endpoint;
    this.token = token;
  }

  async callTool(toolName, input, options = {}) {
    const controller = new AbortController();
    const timeoutMs = options.timeoutMs ?? 8000;

    const timeout = setTimeout(() =&gt; controller.abort(), timeoutMs);

    try {
      const response = await fetch(`${this.endpoint}/tools/${toolName}`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Authorization": `Bearer ${this.token}`
        },
        body: JSON.stringify(input),
        signal: controller.signal
      });

      if (!response.ok) {
        const text = await response.text();
        throw new Error(`Tool call failed: ${response.status} ${text}`);
      }

      return await response.json();
    } finally {
      clearTimeout(timeout);
    }
  }
}

// Example: capture visible context from the browser UI.
function getVisibleContext() {
  const selection = window.getSelection()?.toString() || "";
  const title = document.title;
  const url = location.href;

  // In real apps, you&#8217;d want more careful sanitization and filtering.
  const bodyText = document.body.innerText.slice(0, 4000);

  return {
    title,
    url,
    selection,
    visibleText: bodyText
  };
}

async function summarizeCurrentPage() {
  const client = new BrowserMCPClient({
    endpoint: "https://api.example.com/mcp",
    token: "user-access-token"
  });

  const context = getVisibleContext();

  try {
    const result = await client.callTool("summarize_page", {
      context,
      tone: "concise"
    }, {
      timeoutMs: 10000
    });

    console.log("Summary:", result.summary);
    return result.summary;
  } catch (error) {
    console.error("Failed to summarize page:", error);
    return null;
  }
}

// Example UI hookup
document.getElementById("summarize-btn")?.addEventListener("click", summarizeCurrentPage);</code></pre></div><p>This example is deliberately boring in the best possible way.</p><p>Why? Because boring code is usually secure enough to review and predictable enough to maintain. It also shows the right boundary: the browser captures context and triggers a structured call, but the privileged work stays behind a service boundary.</p><p><em><strong>What This Should Look Like on the Backend</strong></em></p><p>The server side should enforce the real trust boundaries.</p><p>A backend MCP tool can:</p><ul><li><p>validate the request,</p></li><li><p>sanitize incoming context,</p></li><li><p>check permissions,</p></li><li><p>apply policy,</p></li><li><p>execute the risky action,</p></li><li><p>return a normalized response.</p></li></ul><p>That means the frontend can remain lightweight while the backend becomes the source of truth for authorization and execution.</p><p>Here&#8217;s a simple Python example of a backend tool handler:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3343a8fc-8451-4b1c-849c-4fcdf73f820b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from flask import Flask, request, jsonify

app = Flask(__name__)

def sanitize_context(context: dict) -&gt; dict:
    # Example: keep only the fields we expect
    allowed_keys = {"title", "url", "selection", "visibleText"}
    return {k: context.get(k, "") for k in allowed_keys}

def user_can_access(user_id: str, url: str) -&gt; bool:
    # Replace with your real policy engine / ACL / auth logic
    return url.startswith("https://example.com")

@app.route("/tools/summarize_page", methods=["POST"])
def summarize_page():
    auth_header = request.headers.get("Authorization", "")
    if not auth_header.startswith("Bearer "):
        return jsonify({"error": "unauthorized"}), 401

    payload = request.get_json(force=True)
    context = sanitize_context(payload.get("context", {}))
    user_id = "current-user"  # derive from auth in real code

    if not user_can_access(user_id, context["url"]):
        return jsonify({"error": "forbidden"}), 403

    visible_text = context["visibleText"][:4000]

    # In reality, this could call an LLM or another service.
    summary = f"Page titled '{context['title']}' appears to contain {len(visible_text.split())} words."

    return jsonify({
      "summary": summary,
      "source": {
          "title": context["title"],
          "url": context["url"]
      }
    })

if __name__ == "__main__":
    app.run(debug=True)</code></pre></div><p>Again, the important part is not the exact framework. It&#8217;s the shape of the design:</p><ul><li><p>the frontend requests,</p></li><li><p>the backend validates,</p></li><li><p>the policy layer decides,</p></li><li><p>the tool executes,</p></li><li><p>the response is normalized.</p></li></ul><p>That is how you keep browser-based MCP from becoming a security incident with nice typography.</p><p><em><strong>Where Browser-Based MCP Is Most Useful</strong></em></p><p>The strongest use cases are the ones where the model augments an existing workflow instead of trying to replace the browser.</p><p>Common examples include:</p><ul><li><p>assistant panels inside web apps,</p></li><li><p>browser extensions that help summarize or act on visible content,</p></li><li><p>productivity tools that operate on the current page,</p></li><li><p>workflow assistants in CRMs, dashboards, or knowledge bases,</p></li><li><p>&#8220;help me do this faster&#8221; experiences where the model needs UI state.</p></li></ul><p>These use cases work because the model is close to the user&#8217;s work, but not in charge of everything.</p><p>That balance matters. Users want assistance, not a browser that thinks it owns the keyboard.</p><p>Browser-based MCP shines when it can inspect visible state, suggest next steps, or trigger narrow actions with clear boundaries. It is less compelling when it tries to access everything everywhere all at once. That&#8217;s when the architecture becomes brittle, and brittle systems are expensive hobbies.</p><p><em><strong>A Useful Mental Model: Context In, Authority Out</strong></em></p><p>Here&#8217;s a simple way to think about the architecture.</p><ul><li><p>The browser gets context.</p></li><li><p>The backend gets authority.</p></li><li><p>The MCP layer gets orchestration.</p></li><li><p>The user keeps control.</p></li></ul><p>That mental model helps prevent a lot of design mistakes.</p><p>If a feature requires authority, don&#8217;t leave it in the browser just because it&#8217;s convenient. If a feature only needs visible context, don&#8217;t drag it into your backend like it&#8217;s a fragile suitcase full of state.</p><p>The best systems keep these roles separate.</p><p>That separation also makes auditing easier. You can answer questions like:</p><ul><li><p>What did the browser observe?</p></li><li><p>What did the model infer?</p></li><li><p>What action was proposed?</p></li><li><p>What was actually executed?</p></li><li><p>Who authorized it?</p></li><li><p>What happened when it failed?</p></li></ul><p>If you can&#8217;t answer those questions, your architecture is still in its &#8220;we&#8217;ll figure it out later&#8221; era.</p><p><em><strong>Design Guidelines Worth Keeping Close</strong></em></p><p>If you&#8217;re building browser-based MCP for a frontend app, here are the rules I&#8217;d pin above the desk:</p><ol><li><p>Keep the browser thin.</p></li><li><p>Treat browser content as untrusted.</p></li><li><p>Minimize context exposure.</p></li><li><p>Put privileged actions behind a boundary.</p></li><li><p>Require explicit authorization for sensitive actions.</p></li><li><p>Make tools idempotent whenever possible.</p></li><li><p>Add timeouts, retries, and observability.</p></li><li><p>Normalize responses for the UI.</p></li><li><p>Keep protocol details out of app components.</p></li><li><p>Assume the page can be adversarial.</p></li></ol><p>If that sounds strict, good. Security and reliability are the part of software where optimism goes to get audited.</p><p><em><strong>Example Libraries, SDKs, and Services to Explore</strong></em></p><p>If you want to look at existing building blocks and adjacent ecosystems, here are some useful references:</p><ul><li><p><strong>Model Context Protocol (MCP) SDKs</strong> for JavaScript and Python</p></li><li><p><strong>Anthropic MCP ecosystem</strong> and related tooling</p></li><li><p><strong>OpenAI-style tool calling patterns</strong> for structured function invocation</p></li><li><p><strong>Browser extension frameworks</strong> such as Chrome Extensions MV3</p></li><li><p><strong>Playwright</strong> for browser automation patterns</p></li><li><p><strong>Puppeteer</strong> for headless browser control</p></li><li><p><strong>LangChain</strong> and <strong>LlamaIndex</strong> for orchestration patterns adjacent to tool use</p></li><li><p><strong>Zapier</strong> and <strong>n8n</strong> for structured workflow orchestration</p></li><li><p><strong>Auth0</strong>, <strong>Clerk</strong>, or <strong>Firebase Auth</strong> for identity and authorization layers</p></li><li><p><strong>Datadog</strong>, <strong>Sentry</strong>, or <strong>OpenTelemetry</strong> for observability and error tracing</p></li></ul><p>These aren&#8217;t all MCP-specific, but they each solve a piece of the puzzle: auth, orchestration, browser control, or observability.</p><p><em><strong>Closing Thoughts: The Browser Is a Great Observer, Not a Great Emperor</strong></em></p><p>Browser-based MCP is genuinely promising, especially for assistant-style frontend experiences where the model needs to understand what the user is looking at and help them act faster. But the winning architecture is not &#8220;give the browser everything.&#8221; It&#8217;s &#8220;give the browser just enough.&#8221;</p><p>That means:</p><ul><li><p>richer context, but not reckless context,</p></li><li><p>useful actions, but not unbounded power,</p></li><li><p>fast experiences, but not brittle ones,</p></li><li><p>smart assistance, but still user control.</p></li></ul><p>If you build it that way, browser-based MCP becomes a practical, elegant bridge between model reasoning and everyday frontend workflows.</p><p>And if you don&#8217;t, well&#8230; congratulations in advance on your new favorite incident channel.</p><p>Come back tomorrow for more frontend-backend survival notes, architecture stories, and the occasional polite rant from <strong>The Backend Developers</strong>. Stay sharp, ship safely, and keep your browser thin.</p>]]></content:encoded></item></channel></rss>