<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Samar Prakash</title>
    <description>The latest articles on DEV Community by Samar Prakash (@samarprakash22).</description>
    <link>https://dev.to/samarprakash22</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3959751%2F73ecdf68-c7d6-42cd-98ef-3579550645bf.png</url>
      <title>DEV Community: Samar Prakash</title>
      <link>https://dev.to/samarprakash22</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/samarprakash22"/>
    <language>en</language>
    <item>
      <title>When Two Containers on the Same Host Are Shouting Through a Load Balancer</title>
      <dc:creator>Samar Prakash</dc:creator>
      <pubDate>Sat, 30 May 2026 09:55:10 +0000</pubDate>
      <link>https://dev.to/samarprakash22/when-two-containers-on-the-same-host-are-shouting-through-a-load-balancer-20mh</link>
      <guid>https://dev.to/samarprakash22/when-two-containers-on-the-same-host-are-shouting-through-a-load-balancer-20mh</guid>
      <description>&lt;h3&gt;
  
  
  Building a Unix-Domain-Socket IPC server for ECS-on-EC2 services that need to talk fast, cheap, and reliably
&lt;/h3&gt;




&lt;p&gt;A while back I was looking at a flamegraph of a service that, on paper, should not have been having any performance problems. The producer and the consumer were the same Docker image's worth of trouble — colocated on the same EC2 host, in the same ECS cluster, sharing the same instance type, the same kernel, the same RAM. By every reasonable measure they were neighbours.&lt;/p&gt;

&lt;p&gt;And yet every event was making a round trip that looked roughly like this: producer → kernel TCP stack → ENI on the producer task → AWS VPC → internal load balancer → ENI on the consumer task → kernel TCP stack → consumer. TLS handshake. HTTP framing. JSON over the wire. Connection pool. Retry policy. The whole circus.&lt;/p&gt;

&lt;p&gt;I wasn't doing anything wrong. This is what the platform funnels you toward. ECS with &lt;code&gt;awsvpc&lt;/code&gt; networking gives every task its own ENI. The default story for "service A talks to service B" is "give B a DNS name, put a load balancer in front of it, configure a security group, point A at the LB." Even if A and B are physically on the same box, the bytes are still leaving the kernel, traversing the VPC, and coming back.&lt;/p&gt;

&lt;p&gt;There's a fix for this. It's been a fix for fifty-something years. It just hasn't been the &lt;em&gt;default&lt;/em&gt; fix, because cloud-native architecture grew up assuming services would be scattered across hosts and the network was the abstraction that mattered.&lt;/p&gt;

&lt;p&gt;This article is about building a proper IPC server using Unix Domain Sockets, deployed as a sidecar pattern on ECS-on-EC2, with a wire protocol robust enough to ship in production. We're going to design it from scratch — the transport choice, the wire format, the backpressure model, the failure modes, the deployment topology. I'll show you real pseudo-code from the implementation and call out the small number of places where, if you get it wrong, you'll spend a weekend debugging it.&lt;/p&gt;

&lt;p&gt;The intended outcome is something you could lift the pattern from. The article is long because the problem isn't actually that simple once you get past the "just use a socket file" stage. But none of it is mystical. If you've written Netty code or a binary protocol parser before, you'll be fine. If you haven't, the early sections will land regardless.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffk69h03hg2lzyv8x7td1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffk69h03hg2lzyv8x7td1.png" alt="Topology: producers and the IPC server sharing a UDS socket file on one EC2 host" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with "it's all just localhost"
&lt;/h2&gt;

&lt;p&gt;The first thing every engineer reaches for when they realise two services live on the same host is &lt;code&gt;127.0.0.1&lt;/code&gt;. That worked beautifully in 1999. It works less well on modern container platforms, and the reason is worth understanding properly.&lt;/p&gt;

&lt;p&gt;When you run an ECS task in &lt;code&gt;awsvpc&lt;/code&gt; networking mode — and you probably are, because &lt;code&gt;bridge&lt;/code&gt; mode has its own pile of caveats — every task gets its own ENI. AWS attaches that ENI to a dedicated network namespace inside the EC2 host's kernel. Task A's loopback interface is &lt;em&gt;not&lt;/em&gt; the same loopback interface as task B's loopback interface. They both see &lt;code&gt;127.0.0.1&lt;/code&gt;, but those two &lt;code&gt;127.0.0.1&lt;/code&gt;s are different things. A connection to &lt;code&gt;127.0.0.1:8080&lt;/code&gt; from inside task A will never reach a listener inside task B, even if they're on the same physical EC2 instance.&lt;/p&gt;

&lt;p&gt;You can work around this with &lt;code&gt;host&lt;/code&gt; networking mode, but then you've given up port isolation and you have to coordinate every port number across every container on the host. That trade ages badly.&lt;/p&gt;

&lt;p&gt;The second thing engineers reach for is "well, fine, put a load balancer in front of it." This works. It also costs you:&lt;/p&gt;

&lt;p&gt;A real ENI on each side (and ENIs are a rationed resource per instance type). A trip through the AWS VPC data plane. Potentially TLS termination and re-encryption. JSON serialization on the way out and deserialization on the way in. The overhead of HTTP itself: headers, status codes, content negotiation, the eternal question of whether to use keep-alive. And in some setups, an actual hop through an external NLB or ALB, which means the bytes leave the host entirely just to come back.&lt;/p&gt;

&lt;p&gt;For request rates in the dozens or hundreds per second, none of this matters. For thousands per second per host, it starts mattering. For tens of thousands, it dominates your cost and your tail latency.&lt;/p&gt;

&lt;p&gt;We need a transport that says: "I know we're on the same kernel. Just give me a pipe."&lt;/p&gt;

&lt;p&gt;That transport is the Unix Domain Socket.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Unix Domain Socket actually is
&lt;/h2&gt;

&lt;p&gt;If you already know, skip this section. If you've been writing distributed systems for years but never had a reason to use one, this is the five-minute version.&lt;/p&gt;

&lt;p&gt;A UDS is a socket that uses a file path as its address instead of an IP and port. You bind to &lt;code&gt;/var/run/something.sock&lt;/code&gt;, the kernel creates a special file at that path, and clients &lt;code&gt;connect()&lt;/code&gt; to the same path. Once connected, both sides have a file descriptor that behaves almost exactly like a TCP socket: &lt;code&gt;read()&lt;/code&gt;, &lt;code&gt;write()&lt;/code&gt;, &lt;code&gt;close()&lt;/code&gt;, the works.&lt;/p&gt;

&lt;p&gt;The interesting differences:&lt;/p&gt;

&lt;p&gt;There's no TCP/IP stack in the path. No headers, no checksums, no congestion control, no retransmits. The kernel just shuffles bytes from one process's send buffer to the other's receive buffer. Two copies, both inside the kernel: user-space buffer to kernel skb, kernel skb back to user-space buffer. That's it.&lt;/p&gt;

&lt;p&gt;There's no network. The bytes never touch a NIC, never go through &lt;code&gt;iptables&lt;/code&gt;, never see your VPC routing tables. If you can't reach the other side, it's because the file doesn't exist or you don't have permission to open it, not because some far-away switch is having a bad day.&lt;/p&gt;

&lt;p&gt;There's no port collision. The address is a filesystem path, so two services on the same host can each have their own socket at their own path with zero coordination.&lt;/p&gt;

&lt;p&gt;And the performance is genuinely excellent. A single UDS channel on a modern Linux server will sustain something on the order of 50–55 Gbit/s of throughput before you start seeing CPU saturation in the syscall layer. That's "I cannot saturate this with anything I am about to throw at it" territory for almost every application.&lt;/p&gt;

&lt;p&gt;The one piece of cloud reality you have to manage is that UDS sockets live on the filesystem. If your producer container and your server container are different containers, they don't share a filesystem by default. You have to give them a shared volume — a host bind-mount works perfectly — so both can see the same socket file. We'll come back to this when we talk about deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  A quick bake-off against the alternatives
&lt;/h2&gt;

&lt;p&gt;Before we commit, it's worth being honest about the options. I considered five transports for this problem; here's where I landed on each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TCP loopback.&lt;/strong&gt; Works fine when both endpoints are in the same network namespace, which on &lt;code&gt;awsvpc&lt;/code&gt; ECS they aren't. You can switch the task to &lt;code&gt;host&lt;/code&gt; networking and make it work, but you've coupled every container on the host into one port namespace forever. Hard pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP/gRPC over loopback or over the LB.&lt;/strong&gt; Easy to write, easy to operate, miserable per-message overhead. gRPC has its place when the consumer might one day be in a different region; it's wrong when the consumer is forty inches of copper trace away from the producer. Also, gRPC over a real socket on the same host still pays the cost of HTTP/2 framing, flow control, header compression — none of which buys you anything when the round-trip is microseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared memory.&lt;/strong&gt; The classic answer for "ridiculously fast IPC" — map a region into both processes, treat it as a ring buffer, get sub-microsecond latency. The cost is operational: you need a discipline around term buffers, you need to size memory carefully, you need to deal with the case where one side crashes mid-write. Tools like Aeron do this very well and are the right call when you genuinely need 250-nanosecond publish latency or hundreds of millions of messages per second. For "I need to push a few gigabytes per second between two containers with single-digit-millisecond budgets", shared memory is a Ferrari for a school run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Named pipes / FIFOs.&lt;/strong&gt; Half-duplex, no &lt;code&gt;accept()&lt;/code&gt; model, no easy way to fan multiple clients into one server. Fine for shell pipelines, awkward for service-to-service IPC. Not a serious contender.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unix Domain Sockets.&lt;/strong&gt; Full-duplex byte stream. Connection-oriented (&lt;code&gt;SOCK_STREAM&lt;/code&gt;), so we get FIFO ordering and per-channel isolation. Every language has battle-tested support. The Linux &lt;code&gt;epoll&lt;/code&gt; event loop treats UDS exactly like TCP, so frameworks like Netty just work. Per-channel throughput well above what we need. No new infrastructure to operate.&lt;/p&gt;

&lt;p&gt;The decision more or less makes itself once you write it out. UDS is unsexy, well-understood, and exactly the right tool. Aeron is a reasonable Phase 2 escape hatch if measurements ever say we need it, but the design rule is "do not add infrastructure complexity until measurements demand it."&lt;/p&gt;




&lt;h2&gt;
  
  
  Designing the wire protocol
&lt;/h2&gt;

&lt;p&gt;A socket is a byte stream. The application has to invent the concept of "a message." This is the first place where careless design will haunt you, so let's be careful.&lt;/p&gt;

&lt;p&gt;The rule I'll defend without hesitation: &lt;strong&gt;length-prefixed binary frames with a fixed-size, parseable-first-thing prelude.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the layout I ended up with:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmijja0gncjjuycsxfb0h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmijja0gncjjuycsxfb0h.png" alt="Wire frame layout: 16-byte fixed prelude, Protobuf header, opaque payload" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The frame is three regions stitched end-to-end on the byte stream:&lt;/p&gt;

&lt;p&gt;A 16-byte fixed prelude that &lt;em&gt;every implementation reads first&lt;/em&gt;. It contains the total frame length (&lt;code&gt;uint32&lt;/code&gt;), the header length (&lt;code&gt;uint16&lt;/code&gt;), a flags byte, a reserved byte, and a CRC32C over the header bytes (&lt;code&gt;fixed32&lt;/code&gt;). All big-endian, all at known offsets.&lt;/p&gt;

&lt;p&gt;A variable-length header, defined as a Protobuf message. It carries the frame type (HELLO, DATA, ACCEPTED, FAILED, CREDIT_UPDATE, DRAIN, PING, PONG), the protocol version, producer identity, sequence numbers, and any per-frame metadata. Because it's Protobuf, we can add new fields over time without breaking older clients.&lt;/p&gt;

&lt;p&gt;A payload, which is opaque bytes. The server does not parse the payload. It does not know or care what's in it. From its perspective the payload is &lt;code&gt;byte[]&lt;/code&gt;, end of story. The application protocol defines what those bytes mean.&lt;/p&gt;

&lt;p&gt;Three quick questions that always come up when I explain this design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Why not just one Protobuf message with the payload as a &lt;code&gt;bytes&lt;/code&gt; field?"&lt;/strong&gt; Because then your parser is in a chicken-and-egg situation. To know where the message ends, you have to parse the message; to parse the message, you have to have read all of it. Worse, if the first few bytes get corrupted, the Protobuf parser has no anchor — it can't tell you "the length field is wrong" because it doesn't know what the length field even is. The fixed prelude gives every reader a known landmark. Read 16 bytes, validate them, &lt;em&gt;then&lt;/em&gt; trust the variable-length parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Why is there a CRC if TCP / UDS is already reliable?"&lt;/strong&gt; UDS is reliable across the wire. It is not reliable against your own bugs. A CRC on the header bytes will catch a serializer that wrote the wrong length, a buffer that got truncated on a partial write, a decoder that misaligned itself after an earlier malformed frame. The CRC is in there to protect you from yourself. (The flags byte reserves a second CRC slot for the payload, which we don't compute in v1 because the payload is opaque and the application owns its own integrity checks. The bit is reserved so we can turn it on later without a version bump.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Why CRC32C specifically?"&lt;/strong&gt; It has hardware acceleration on modern x86-64 and ARM CPUs (&lt;code&gt;CRC32C&lt;/code&gt; is in SSE 4.2 and the ARMv8 CRC extensions). Computing it costs effectively nothing per frame. The standard &lt;code&gt;CRC32&lt;/code&gt; would also work but is meaningfully slower in software.&lt;/p&gt;

&lt;p&gt;Here's the encoder in pseudo-code, lightly cleaned up from what's actually running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Frame&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ByteBuf&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CRC32C&lt;/span&gt; &lt;span class="n"&gt;scratch&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;headerBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toByteArray&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;payloadLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;frameLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headerBytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;payloadLength&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frameLength&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;MAX_FRAME_BYTES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="o"&gt;(...);&lt;/span&gt;

    &lt;span class="n"&gt;scratch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reset&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;scratch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headerBytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headerBytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;headerCrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scratch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeInt&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;frameLength&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// 4&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeShort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headerBytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 2&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeByte&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;                    &lt;span class="c1"&gt;// flags  (1)&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeByte&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;                    &lt;span class="c1"&gt;// rsv    (1)&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeInt&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;headerCrc&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// 4   -- total prelude = 16 bytes&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headerBytes&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadLength&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the decoder, which is the more interesting half because it has to defend against partial reads and malicious peers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;DecodeResult&lt;/span&gt; &lt;span class="nf"&gt;tryDecode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ByteBuf&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxFrameBytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CRC32C&lt;/span&gt; &lt;span class="n"&gt;scratch&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readableBytes&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;PRELUDE_BYTES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;INSUFFICIENT_BYTES&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;frameLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUnsignedInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readerIndex&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frameLength&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxFrameBytes&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MalformedFrameException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frame too large"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// &amp;lt;-- before any alloc&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readableBytes&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;PRELUDE_BYTES&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;frameLength&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;INSUFFICIENT_BYTES&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;headerLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUnsignedShort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readerIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUnsignedByte&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readerIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUnsignedByte&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readerIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt;  &lt;span class="n"&gt;headerCrc&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUnsignedInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readerIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;validatePreludeInvariants&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headerLength&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frameLength&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;scratch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reset&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;scratch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asReadOnlySlice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headerStart&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headerLength&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scratch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;headerCrc&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MalformedFrameException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"header CRC mismatch"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;FrameHeader&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FrameHeader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parseFrom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headerStart&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headerLength&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;copyPayload&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headerStart&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;headerLength&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frameLength&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;headerLength&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;skipBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PRELUDE_BYTES&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;frameLength&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Decoded&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The discipline that makes this safe: &lt;strong&gt;reject before you allocate.&lt;/strong&gt; The &lt;code&gt;frameLength &amp;gt; maxFrameBytes&lt;/code&gt; check happens before we allocate anything for the payload. If a hostile or buggy peer sends a frame claiming to be 2 GB, we throw immediately and close the connection. We never give them the satisfaction of making us reserve 2 GB of direct memory.&lt;/p&gt;

&lt;p&gt;On the Netty side, this lives in a &lt;code&gt;ByteToMessageDecoder&lt;/code&gt; subclass. One small detail that matters at scale: we use &lt;code&gt;COMPOSITE_CUMULATOR&lt;/code&gt; instead of the default &lt;code&gt;MERGE_CUMULATOR&lt;/code&gt;. The default copies every fragmented inbound read into a new contiguous buffer. The composite cumulator keeps the fragments as a chain and doesn't copy until something actually needs a contiguous view. At sustained high throughput, this is the difference between hot loops that allocate and hot loops that don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frame types and the handshake
&lt;/h2&gt;

&lt;p&gt;There are nine frame types in this protocol. They split into three groups.&lt;/p&gt;

&lt;p&gt;The handshake pair: &lt;code&gt;HELLO&lt;/code&gt; and &lt;code&gt;HELLO_ACK&lt;/code&gt;. The client sends &lt;code&gt;HELLO&lt;/code&gt; immediately on &lt;code&gt;channelActive&lt;/code&gt;, advertising the protocol version range it can speak and its identity (producer ID, producer epoch). The server picks the highest mutually-supported version and replies with &lt;code&gt;HELLO_ACK&lt;/code&gt; containing the negotiated version and the initial credit budget it's willing to grant. If there is no overlap, the server replies with &lt;code&gt;accepted=false&lt;/code&gt; and a rejection reason, then closes the connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;OptionalInt&lt;/span&gt; &lt;span class="nf"&gt;negotiateVersion&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HelloFrame&lt;/span&gt; &lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;producerMin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMinProtocolVersion&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;producerMax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMaxProtocolVersion&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producerMax&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;serverConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;minProtocolVersion&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OptionalInt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producerMin&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;serverConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxProtocolVersion&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OptionalInt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OptionalInt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producerMax&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serverConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxProtocolVersion&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the entire version-negotiation algorithm. It's six lines because it should be six lines. The temptation to invent something fancier here is real — feature flags per version, optional capabilities, etc. — but every layer of cleverness you add to the handshake is a layer your operations team will be debugging at 3 AM. Two integers, pick the overlap, ship it.&lt;/p&gt;

&lt;p&gt;The data path: &lt;code&gt;DATA&lt;/code&gt; and &lt;code&gt;DATA_BATCH&lt;/code&gt;. A single chunk goes in &lt;code&gt;DATA&lt;/code&gt;. Multiple chunks coalesced into one wire frame go in &lt;code&gt;DATA_BATCH&lt;/code&gt;, with the header carrying an array of per-chunk slice descriptors (offset, length, event count, sequence). One write syscall ships a hundred chunks. The kernel never sees us spamming.&lt;/p&gt;

&lt;p&gt;The control path: &lt;code&gt;ACCEPTED&lt;/code&gt;, &lt;code&gt;FAILED&lt;/code&gt;, &lt;code&gt;CREDIT_UPDATE&lt;/code&gt;, &lt;code&gt;DRAIN&lt;/code&gt;, &lt;code&gt;PING&lt;/code&gt;, &lt;code&gt;PONG&lt;/code&gt;. These are how the server tells the client what happened to its data and how the two sides keep the channel alive.&lt;/p&gt;

&lt;p&gt;The full handshake-to-publish-to-drain story looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsna22cirz6g2upyrticu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsna22cirz6g2upyrticu.png" alt="Sequence: connect, handshake, publish bursts, drain" width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The producer connects to the socket file (retrying with exponential backoff and jitter if the server isn't up yet — more on that in the lifecycle section). It immediately writes &lt;code&gt;HELLO&lt;/code&gt;. The server reads it, negotiates a version, allocates per-connection state, and writes &lt;code&gt;HELLO_ACK&lt;/code&gt;. From that moment, the connection is in the "data plane" — both sides are free to send and receive frames in any order.&lt;/p&gt;

&lt;p&gt;The producer then writes &lt;code&gt;DATA&lt;/code&gt; frames as fast as its credit budget allows. Each frame has a &lt;code&gt;producer_sequence&lt;/code&gt; field that's monotonically increasing per producer. The server reads them, runs them through its decoder, gates them against its server-side credit window, and hands them to a pluggable &lt;code&gt;ChunkSink&lt;/code&gt;. The sink returns either &lt;code&gt;Accepted&lt;/code&gt; or &lt;code&gt;Failed(reason, retryable)&lt;/code&gt;. On &lt;code&gt;Accepted&lt;/code&gt;, the server appends the chunk identity to a per-channel "pending ACCEPTED" list. On &lt;code&gt;Failed&lt;/code&gt;, it flushes the pending list and emits a &lt;code&gt;FAILED&lt;/code&gt; frame.&lt;/p&gt;

&lt;p&gt;Here's the thing I want to highlight. Netty hands you frames in batches — it reads as many bytes as the socket has and then walks them through the pipeline. When the batch is done, it calls &lt;code&gt;channelReadComplete&lt;/code&gt;. We hook into that hook to flush the pending ACCEPTED list as a single frame containing every chunk identity that landed in this batch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;channelReadComplete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChannelHandlerContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;flushPendingAcceptedAcks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// one ack frame for N data frames&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fireChannelReadComplete&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At sustained billions of frames per day, this is the single most important throughput optimization in the protocol. Without it, every &lt;code&gt;DATA&lt;/code&gt; round-trips against its own &lt;code&gt;ACCEPTED&lt;/code&gt;. With it, a burst of N data frames generates approximately one ack frame. The ack channel stays under 1% of the forward traffic.&lt;/p&gt;

&lt;p&gt;The reverse trick: when something fails, the &lt;code&gt;FAILED&lt;/code&gt; frame carries the &lt;em&gt;restored credit footprint&lt;/em&gt;. The producer reserved credit when it sent the chunk; the failed ack tells it "you can take that credit back, plus this amount of additional credit I freed on your behalf." A single round-trip closes the negative path. There's no ambiguity about who owns the credit footprint of a rejected chunk and no separate &lt;code&gt;CREDIT_UPDATE&lt;/code&gt; frame needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three-dimensional backpressure (this is the part most designs get wrong)
&lt;/h2&gt;

&lt;p&gt;Anyone who's built a queue-like thing has shipped a one-dimensional credit system: "you can have N in-flight requests." It works fine until somebody sends a single request that contains 50 megabytes of payload while another somebody sends ten thousand requests that are 12 bytes each. Both saturate the system in completely different ways, and a one-dimensional limit can't see either one coming.&lt;/p&gt;

&lt;p&gt;So we do it in three dimensions. A producer is admitted to send a chunk if and only if:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;There is at least one chunk-slot available (caps the count of in-flight chunks).&lt;/li&gt;
&lt;li&gt;There are enough raw-event slots available to cover the chunk's event count (caps the aggregate cardinality).&lt;/li&gt;
&lt;li&gt;There are enough payload-byte slots available to cover the chunk's payload size (caps the aggregate bytes in flight).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The acquire is all-or-nothing. If any dimension is short, the chunk is rejected with &lt;code&gt;REJECTED_NO_CREDIT&lt;/code&gt; and the producer is expected to retry later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;synchronized&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;tryAcquire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;payloadBytes&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;availableChunkSlots&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;
            &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;availableRawEvents&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
            &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;availablePayloadBytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;payloadBytes&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;availableChunkSlots&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;availableRawEvents&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;availablePayloadBytes&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;payloadBytes&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The producer enforces this &lt;em&gt;and&lt;/em&gt; the server enforces an identical copy of it on its side. If the producer is buggy or malicious and tries to send beyond its credit, the server's credit gate catches it and the offending chunk gets &lt;code&gt;FAILED(retryable=true, reason="credit exhausted")&lt;/code&gt;. This is defence in depth: the client controls its own behaviour, but the server is the source of truth for what's actually safe.&lt;/p&gt;

&lt;p&gt;Credit is restored on three events: ACCEPTED ack received, FAILED ack received (with the restored amount carried in the frame), or connection drop (every outstanding chunk's footprint is freed so the producer is never permanently blocked).&lt;/p&gt;

&lt;p&gt;Layered on top of this is Netty's channel-level backpressure. The pipeline is configured with a &lt;code&gt;WriteBufferWaterMark&lt;/code&gt;. When the outbound buffer crosses the high mark, &lt;code&gt;channel.isWritable()&lt;/code&gt; flips to false; when it drains below the low mark, it flips back. A well-behaved producer checks &lt;code&gt;isWritable()&lt;/code&gt; before submitting, which means even within its credit budget the producer naturally pauses when the socket's send buffer is full. Below all of that, the kernel's UDS send buffer applies its own pressure: if it's full, &lt;code&gt;write()&lt;/code&gt; blocks (or &lt;code&gt;writev()&lt;/code&gt; returns a short write). The application credit window, Netty's watermark, and the kernel buffer form three concentric rings, each catching a different misbehaviour.&lt;/p&gt;

&lt;p&gt;The "retry-after" hint deserves a mention. When the server sends a credit update with no available room, it can attach a &lt;code&gt;retry_after_millis&lt;/code&gt; field. The producer treats that as actionable — sleep for that duration before retrying, and if the producer is itself fronted by an HTTP API, convert it to a 429 with a &lt;code&gt;Retry-After&lt;/code&gt; header. Backpressure should always be observable to the original caller. Silent drops are how you lose a weekend chasing data loss that isn't actually data loss.&lt;/p&gt;




&lt;h2&gt;
  
  
  Concurrency without sadness
&lt;/h2&gt;

&lt;p&gt;Netty's threading model is famously easy to get wrong. The single cardinal rule, the one I've now seen broken in production at three different companies: &lt;strong&gt;do not block on a Netty event loop thread.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The event loop owns the channel. It reads bytes off the socket, runs them through the decoder, dispatches to the protocol handler, runs the encoder, writes bytes back. One thread, many channels, no contention per channel. If you block that thread — on a synchronous database call, on a blocking ring-buffer publish, on a thread &lt;code&gt;sleep&lt;/code&gt;, on a lock that's held by an offline service — you don't just stall one channel. You stall every channel that thread owns. Under sustained load that's the entire fleet of producers connected to that server.&lt;/p&gt;

&lt;p&gt;The way out of this is discipline about where work happens. The server pipeline does exactly this much work on the event loop:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F90ibdwmbgi1apv80ymgt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F90ibdwmbgi1apv80ymgt.png" alt="Server pipeline and the cardinal threading rule" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The IO event loop runs &lt;code&gt;FlushConsolidationHandler&lt;/code&gt; → &lt;code&gt;IdleStateHandler&lt;/code&gt; → &lt;code&gt;FrameDecoder&lt;/code&gt; → &lt;code&gt;FrameEncoder&lt;/code&gt; → protocol handler. All of those are CPU-only operations. The protocol handler does Protobuf parsing, CRC checking, version negotiation, credit accounting, ack accumulation. None of it talks to disk or to the network or to a database.&lt;/p&gt;

&lt;p&gt;When a chunk needs to actually go somewhere — be aggregated, compressed, written to durable storage, fanned out to a notification stream — the protocol handler hands it to a &lt;code&gt;ChunkSink&lt;/code&gt; interface. The sink is pluggable. In the simplest case (the v1 implementation), it might just enqueue the chunk into a lock-free ring buffer and return immediately. The actual heavy lifting happens on separate worker pools that the event loop never touches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ChunkSink&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SinkVerdict&lt;/span&gt; &lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FrameHeader&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SinkVerdict&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;Accepted&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;SinkVerdict&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;Failed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;retryable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;SinkVerdict&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract: &lt;code&gt;accept&lt;/code&gt; returns synchronously, never blocks, never throws on transient backpressure. Backpressure is communicated by returning &lt;code&gt;Failed(retryable=true)&lt;/code&gt;. The sink may publish into a queue or a ring buffer, but it must publish non-blockingly. If the queue is full, the sink rejects the chunk. The credit system upstream means this should be a rare event; if it isn't, your queue is sized wrong or your downstream pipeline can't keep up and you should be observing it on a dashboard, not absorbing it as backpressure on the IO loop.&lt;/p&gt;

&lt;p&gt;The pluggability matters operationally too. When you're testing the transport in isolation, you wire in a sink that just buffers everything in memory. When you're benchmarking the protocol, you wire in a sink that drops the chunks on the floor. When you're running in production, you wire in the real ingestion pipeline. The transport never changes.&lt;/p&gt;

&lt;p&gt;A quick note on thread counts. The boss event loop accepts new connections; for UDS this is single-threaded by nature, so one thread is plenty. The worker event loops handle the actual channels; 2–4 threads on a moderately-sized host will saturate UDS throughput well before they saturate the CPU. There's no value in throwing more threads at this. More threads means more context switching, not more throughput. The expensive work happens elsewhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  The socket file problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;The socket is a file. Files persist across process restarts. This sounds like a footnote until it bites you.&lt;/p&gt;

&lt;p&gt;Two scenarios that have to be handled:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario one: the server died ungracefully and left a stale socket file behind.&lt;/strong&gt; When the new server starts up, &lt;code&gt;bind()&lt;/code&gt; on that path will fail with "address already in use." You have to clean up the stale file. But: how do you know it's stale? What if another instance of the server is actually running on that path because of a bug in your orchestration?&lt;/p&gt;

&lt;p&gt;The wrong fix is to blindly &lt;code&gt;unlink()&lt;/code&gt; the file before binding. That wins you a five-second outage when you accidentally remove the live server's socket and silently strand every producer trying to talk to it.&lt;/p&gt;

&lt;p&gt;The right fix is to probe before deleting. Try to &lt;code&gt;connect()&lt;/code&gt; to the socket as a client. If the connect succeeds, somebody is actually listening — refuse to boot and let the operator figure out what went wrong. If the connect fails with the expected "no listener" error, the file is stale and safe to delete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;prepareForBind&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socketPath&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probeForLiveListener&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IOException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Refusing to bind: another process is listening on "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;socketPath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socketPath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;probeForLiveListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SocketChannel&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SocketChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StandardProtocolFamily&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UNIX&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;connect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UnixDomainSocketAddress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socketPath&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="n"&gt;refused&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// expected on stale file&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Scenario two: a rolling deployment.&lt;/strong&gt; During a deploy, the new server might bind a new socket at the same path while the old server's shutdown cleanup is still in flight. If the old server then does &lt;code&gt;Files.delete(socketPath)&lt;/code&gt;, it just removed the new server's socket. Now every connected client gets a silent disconnect and no new connections can be established.&lt;/p&gt;

&lt;p&gt;The fix is to record the inode (file key) of the socket file at bind time and only delete on shutdown if the current inode still matches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;recordBoundInode&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;boundFileKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readFileKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socketPath&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;removeAfterShutdown&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;boundFileKey&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;currentKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readFileKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socketPath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;boundFileKey&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Socket file no longer matches our inode; skipping delete"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteIfExists&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socketPath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of these are about ten lines of code. Neither of them is in any tutorial I've ever read. Both will eventually save you an outage.&lt;/p&gt;

&lt;p&gt;There's also the question of &lt;em&gt;where&lt;/em&gt; to put the socket file. The answer is "on a path that's a host bind-mount visible to every container that needs it, and is on a real local filesystem." &lt;code&gt;/var/run/&amp;lt;service&amp;gt;/&amp;lt;service&amp;gt;.sock&lt;/code&gt; is the conventional choice. Do not put it on a network filesystem. Do not put it on &lt;code&gt;tmpfs&lt;/code&gt; shared between containers without thinking about it. Do not put it in a Docker volume that uses &lt;code&gt;overlay2&lt;/code&gt; semantics. A boring local-filesystem path under &lt;code&gt;/var/run&lt;/code&gt; will not surprise you. Anything else might.&lt;/p&gt;

&lt;p&gt;Permissions: directory mode &lt;code&gt;0660&lt;/code&gt; or &lt;code&gt;0770&lt;/code&gt;, owned by a UID/GID shared between the producer and server containers. The kernel enforces filesystem permissions on the socket file — if the producer's user can't open the file, the connection fails the same way as if the directory didn't exist. That's your access control, and it's stronger than any token-based scheme because it's enforced by the kernel before your code runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Draining, reconnecting, and not losing data when the server goes away
&lt;/h2&gt;

&lt;p&gt;Failure handling deserves its own section because the easy story is wrong.&lt;/p&gt;

&lt;p&gt;The easy story: "the server crashed and came back; the client reconnects and keeps going." That's true, but it elides the question of what happens to the chunks the client had in flight when the server died.&lt;/p&gt;

&lt;p&gt;The model we commit to is &lt;strong&gt;at-least-once delivery with idempotent retry&lt;/strong&gt;. The contract has three pieces.&lt;/p&gt;

&lt;p&gt;First, every chunk has a stable identity. The header carries &lt;code&gt;(producer_id, producer_epoch, producer_sequence)&lt;/code&gt;. &lt;code&gt;producer_id&lt;/code&gt; is the client's identity. &lt;code&gt;producer_epoch&lt;/code&gt; is a monotonically-increasing counter that the client bumps every time it restarts. &lt;code&gt;producer_sequence&lt;/code&gt; is monotonically-increasing within an epoch. The triple is unique for the lifetime of the producer.&lt;/p&gt;

&lt;p&gt;Second, on disconnect, every in-flight chunk's future fails with &lt;code&gt;LOST_ON_DISCONNECT&lt;/code&gt; and its credit footprint is released. The application sees the failure and chooses what to do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;failAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Consumer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PendingEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onEachEntry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;removed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getKey&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;removed&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;onEachEntry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;removed&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// restores credit&lt;/span&gt;
        &lt;span class="n"&gt;removed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;future&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PublishResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;LOST_ON_DISCONNECT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Third, the application's retry policy resubmits the chunk with the same logical content but a &lt;em&gt;new&lt;/em&gt; &lt;code&gt;(producer_epoch, producer_sequence)&lt;/code&gt;. The server, on the other side, might write the same content twice — once before the crash, once after the retry. The downstream consumer dedups by a stable application-level ID (the request ID or event ID carried in the payload).&lt;/p&gt;

&lt;p&gt;This is not exactly-once. Exactly-once is a research topic and the people who claim to have shipped it usually haven't, or have shipped it inside a single transactional boundary that isn't your boundary. At-least-once plus idempotent consumers is the boring, working answer. It's what every well-behaved event pipeline does. It's what we do here.&lt;/p&gt;

&lt;p&gt;Graceful shutdown is its own dance. When the server gets a SIGTERM, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stops returning healthy from its container health check, so the orchestrator stops sending it new traffic.&lt;/li&gt;
&lt;li&gt;Broadcasts a &lt;code&gt;DRAIN&lt;/code&gt; frame to every connected client. The DRAIN carries a deadline (server's stated time to finish draining).&lt;/li&gt;
&lt;li&gt;Stops accepting new connections.&lt;/li&gt;
&lt;li&gt;Waits for the drain grace period to let connected clients finish what they were doing.&lt;/li&gt;
&lt;li&gt;Closes the remaining channels.&lt;/li&gt;
&lt;li&gt;Removes the socket file (inode-checked).&lt;/li&gt;
&lt;li&gt;Exits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The client, on receiving &lt;code&gt;DRAIN&lt;/code&gt;, transitions to a &lt;code&gt;DRAINING&lt;/code&gt; state. New &lt;code&gt;publish()&lt;/code&gt; calls fail fast with &lt;code&gt;REJECTED_DRAINING&lt;/code&gt;. In-flight chunks are allowed to ack out. A local timer also force-closes the channel at the smaller of (server-stated deadline, client local shutdown budget), so a server that crashes mid-drain can't leave the client sitting forever.&lt;/p&gt;

&lt;p&gt;Reconnect uses bounded exponential backoff with full jitter. The first attempt happens after a small initial delay (50 ms is reasonable), each subsequent attempt doubles up to a ceiling (5 seconds), and the actual delay is uniformly distributed between 0 and the current ceiling. Jitter is the difference between "everyone retries at the same instant and DOSes the recovering server" and "retries are spread smoothly across the recovery window." It's also one line of code, so there is no excuse not to do it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;synchronized&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;nextDelay&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;attemptCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;unjittered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exponentialDelayMillis&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attemptCount&lt;/span&gt;&lt;span class="o"&gt;++);&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;jittered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jitterSupplier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;applyAsLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unjittered&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMillis&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jittered&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PING/PONG keeps the channel warm and lets each side detect a dead peer. The Netty &lt;code&gt;IdleStateHandler&lt;/code&gt; is configured with a reader-idle timeout (no inbound activity for X seconds → close the channel) and a writer-idle timeout (no outbound activity for Y seconds → emit a PING). Reader-idle closes are caught by the reconnect loop. Writer-idle PINGs solve the "TCP connection is silently dead because a middlebox dropped state" problem, which UDS doesn't have, but the same code path also gives you a periodic liveness signal for free, so we keep it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying this thing on ECS
&lt;/h2&gt;

&lt;p&gt;The deployment topology has a few pieces that have to fit together correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The server runs as an ECS daemon service.&lt;/strong&gt; Set &lt;code&gt;schedulingStrategy=DAEMON&lt;/code&gt;. The cluster will place exactly one task on every container instance. This is what gives you the "one IPC server per host" invariant. Without daemon scheduling, you'd have to coordinate placement constraints by hand, and you'd eventually end up with hosts that have zero servers or hosts that have two servers competing for the same socket file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Producers run as a regular ECS service.&lt;/strong&gt; Spread placement is fine. The producer doesn't need to know which host it's on; it just connects to a fixed socket path. Whatever host it lands on, there'll be a server listening at that path because daemon scheduling guarantees it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both run in &lt;code&gt;awsvpc&lt;/code&gt; networking mode.&lt;/strong&gt; The hot path is UDS, so the loopback namespace separation that bites TCP localhost doesn't matter to us at all. You can pick whatever networking mode is most convenient for the &lt;em&gt;other&lt;/em&gt; traffic the containers carry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The socket directory is a host bind-mount.&lt;/strong&gt; Both the producer task definition and the server task definition mount &lt;code&gt;/var/run/&amp;lt;service&amp;gt;&lt;/code&gt; from the host into the container. Bind mount, not &lt;code&gt;emptyDir&lt;/code&gt;, not &lt;code&gt;tmpfs&lt;/code&gt;. The reason is durability across container restarts: an &lt;code&gt;emptyDir&lt;/code&gt; is a fresh volume each time the task starts, so a producer restart would lose access to the running server's socket file. A host bind mount lives on the EC2 instance's local filesystem and persists for the life of the host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The server's container health check returns healthy only after the socket is bound.&lt;/strong&gt; The health check should do a real &lt;code&gt;connect()&lt;/code&gt; to the socket and immediately close. Don't use a TCP health check; the server isn't listening on TCP. Don't use a process health check; the process can be running but not yet bound. The check needs to verify the actual integration point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The producer does not depend on any ECS attribute or service discovery.&lt;/strong&gt; It does an exponential-backoff connect loop against the socket path. If the server isn't ready yet, the producer retries. If the server is restarting, the producer retries. If the server gets replaced during a rolling deploy, the producer's reconnect loop handles it. This is much simpler than wiring up cross-task readiness signals through PutAttributes or service discovery, and it works no matter what the orchestrator is doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-host resources matter.&lt;/strong&gt; The server is a long-lived JVM with a lot of pooled direct memory. Plan for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native epoll transport (&lt;code&gt;io.netty.transport.noNative=false&lt;/code&gt;), which on Linux is automatic but worth verifying you're picking up the right native library for your architecture (&lt;code&gt;linux-x86_64&lt;/code&gt; vs &lt;code&gt;linux-aarch64&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-XX:MaxDirectMemorySize&lt;/code&gt; sized to cover your pooled allocator high-water mark plus a comfortable safety margin. Direct memory is allocated outside the heap and will OOM your container if you under-size it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memlock=unlimited&lt;/code&gt; and the &lt;code&gt;IPC_LOCK&lt;/code&gt; capability if you're planning to lock pages, which you probably aren't in v1 but might in a future phase.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;stopTimeout&lt;/code&gt; long enough to cover your drain budget plus a buffer. If the orchestrator SIGKILLs you mid-drain, your producers will get hard disconnects instead of graceful drains.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A walk through one publish
&lt;/h2&gt;

&lt;p&gt;Let's trace one chunk end-to-end so the pieces fit together.&lt;/p&gt;

&lt;p&gt;The application calls &lt;code&gt;client.publish(chunk)&lt;/code&gt; from any thread. The client checks its state (must be &lt;code&gt;STARTED&lt;/code&gt;, not &lt;code&gt;DRAINING&lt;/code&gt; or &lt;code&gt;CLOSING&lt;/code&gt;), checks the handshake is complete, and tries to acquire 3-D credit for this chunk's footprint. If credit fails, the call returns a completed future with &lt;code&gt;REJECTED_NO_CREDIT&lt;/code&gt;. If state fails, similar.&lt;/p&gt;

&lt;p&gt;Assuming everything's good, the client assigns a &lt;code&gt;producer_sequence&lt;/code&gt;, registers the chunk in its &lt;code&gt;PendingChunkRegistry&lt;/code&gt; keyed by &lt;code&gt;(producer_epoch, producer_sequence)&lt;/code&gt;, gets back a &lt;code&gt;CompletableFuture&amp;lt;PublishResult&amp;gt;&lt;/code&gt;, schedules a per-chunk publish timeout, and calls &lt;code&gt;channel.writeAndFlush&lt;/code&gt; on the Netty channel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;producerSequence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAndIncrement&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChunkKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producerEpoch&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pendingChunks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eventCount&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payloadBytes&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;schedulePublishTimeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FrameHeader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setFrameType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;FRAME_TYPE_DATA&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProducerId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producerId&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;setProducerEpoch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producerEpoch&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProducerSequence&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEventCount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventCount&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;setPayloadBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadBytes&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeAndFlush&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onWriteComplete&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// releases credit on write failure&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Netty's encoder serializes the frame using the wire format, writes it into a pooled direct buffer, and hands it to the kernel via &lt;code&gt;writev&lt;/code&gt;. The kernel copies it into the receiving UDS socket's buffer.&lt;/p&gt;

&lt;p&gt;On the server side, the worker event loop wakes up on the epoll readiness notification, reads the bytes, and runs them through the pipeline. The decoder pulls one frame out of the buffer, the protocol handler routes it by &lt;code&gt;frame_type&lt;/code&gt; to &lt;code&gt;handleData&lt;/code&gt;, the credit gate checks that the chunk fits, the chunk sink processes it. Assuming &lt;code&gt;Accepted&lt;/code&gt;, the chunk's identity goes onto the pending ACCEPTED list.&lt;/p&gt;

&lt;p&gt;Other frames in the same read batch are processed the same way. At the end of the batch, &lt;code&gt;channelReadComplete&lt;/code&gt; fires and we flush the pending list as a single ACCEPTED frame containing every chunk identity from this batch.&lt;/p&gt;

&lt;p&gt;The ACCEPTED frame goes back through the same wire format, back through the kernel, back to the client. The client's decoder pulls it out, hands it to the protocol handler, which looks up each chunk identity in its registry, completes each future with &lt;code&gt;PublishResult.accepted()&lt;/code&gt;, and restores the credit footprint.&lt;/p&gt;

&lt;p&gt;The application's &lt;code&gt;await()&lt;/code&gt; on the future returns. The whole round-trip, on a moderate-sized r6i instance under reasonable load, is in the low tens of microseconds. The protocol contributes effectively nothing to that — most of the time is the syscall to write the bytes, the syscall to read them, and a tiny amount of Netty bookkeeping. The 50+ Gbit/s per-channel headroom that UDS gives you means we can do this thousands of times per millisecond without breaking a sweat.&lt;/p&gt;




&lt;h2&gt;
  
  
  When this is the wrong answer
&lt;/h2&gt;

&lt;p&gt;Honesty: not every problem wants this transport. Here's when I'd reach for something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Producer and consumer aren't on the same host.&lt;/strong&gt; Then the whole premise is gone. Use a real network protocol. gRPC is fine. Plain HTTP is fine. Whatever you'd normally reach for is fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need cross-language interop.&lt;/strong&gt; Netty is JVM. The pattern translates to Go or Rust or Python without much trouble — the wire format is language-neutral by design — but if you want a single off-the-shelf library that already works in five languages, gRPC over UDS gets you part of the way there with less custom code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need exactly-once semantics.&lt;/strong&gt; You don't. Nobody does. Build idempotency into your consumers and stop fighting the universe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your throughput is genuinely tiny.&lt;/strong&gt; If you're shipping ten requests a second, the operational cost of any custom protocol is going to outweigh the savings versus just calling an HTTP endpoint. Use HTTP, save your weekends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need sub-microsecond latency.&lt;/strong&gt; Now we're in shared-memory territory. Aeron, LMAX Disruptor patterns, kernel-bypass networking. UDS is fast but it's still doing two kernel copies; shared memory does zero. If you need that floor, build it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't get a host bind-mount.&lt;/strong&gt; Some platforms (managed Kubernetes flavours with strict security profiles, certain serverless container runtimes) don't let you bind-mount host paths. UDS is mostly off the table in those environments. You're back to network protocols.&lt;/p&gt;

&lt;p&gt;For the broad middle — same host, container-to-container, throughput in the gigabytes-per-second range, latency budget in the milliseconds, willing to write a small amount of careful protocol code — this design is hard to beat.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;The cloud-native consensus, for very good reasons, is "treat the network as the abstraction." A service has a name, you talk to it over a name-resolved endpoint, you don't care where it is. That model has earned its place.&lt;/p&gt;

&lt;p&gt;But it has a cost, and the cost is paid every time two services that &lt;em&gt;are&lt;/em&gt; on the same host are forced to pretend they aren't. The default tooling will route their bytes through ENIs and load balancers and TLS handshakes, and you'll see the bill at the end of the month and the tail latency on your dashboards and the strange flamegraphs in your profilers.&lt;/p&gt;

&lt;p&gt;UDS is the escape hatch. It's been sitting there in the kernel the whole time. The amount of code needed to use it well is not large — the implementation that backs this article is something like 2,000 lines of Java including tests — but the design has to be careful about a handful of specific things: the fixed prelude trick, the three-dimensional credit model, the ack coalescing on &lt;code&gt;channelReadComplete&lt;/code&gt;, the inode-checked socket cleanup, the discipline of never blocking on the IO event loop, the boring choice to commit to at-least-once delivery and idempotent consumers.&lt;/p&gt;

&lt;p&gt;If you do those things right, you get a transport that is fast, cheap, observable, and doesn't lie to you. That's a good list.&lt;/p&gt;

&lt;p&gt;I'd encourage anyone running a service-to-service workflow on the same host to at least measure what the current implementation is costing them. The number is usually larger than people expect, and the fix is much smaller than people expect.&lt;/p&gt;

&lt;p&gt;The pattern works. Steal it.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>java</category>
      <category>docker</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Stop Paying a Streaming Bus to Carry Bytes That Live for Ninety Seconds</title>
      <dc:creator>Samar Prakash</dc:creator>
      <pubDate>Sat, 30 May 2026 09:48:20 +0000</pubDate>
      <link>https://dev.to/samarprakash22/stop-paying-a-streaming-bus-to-carry-bytes-that-live-for-ninety-seconds-1d3h</link>
      <guid>https://dev.to/samarprakash22/stop-paying-a-streaming-bus-to-carry-bytes-that-live-for-ninety-seconds-1d3h</guid>
      <description>&lt;h3&gt;
  
  
  How a shared filesystem became the cheapest, fastest outbox I've ever built — and why FSx for OpenZFS is the version of that idea that finally scales
&lt;/h3&gt;




&lt;p&gt;I was staring at an AWS bill last quarter where a single Kinesis Data Streams line item was costing more than the entire S3 footprint sitting behind it. The events on that stream had a useful lifetime of about ninety seconds. They were written by one service, read by another, processed, and dropped. We were paying full streaming-bus price for bytes that barely outlived a TCP timeout.&lt;/p&gt;

&lt;p&gt;That bill is what got me thinking about transitional data as a category that deserves its own architecture, and about why every "use the right tool" instinct I had — Kinesis, Kafka, MSK — was the wrong tool for this particular shape of work. The right tool, it turns out, is a filesystem. Specifically, AWS FSx for OpenZFS, used as an outbox between producers and consumers, with only a tiny pointer message traveling through whatever messaging bus you already have.&lt;/p&gt;

&lt;p&gt;This article is the case for that pattern. It's also the design, the failure modes, the code, the cost math, and the honest list of when not to do it. I'll walk you through the architecture from first principles, show you the safe-write protocol that makes it correct under crashes and concurrent retries, compare the cost against Kinesis, MSK and EFS at a realistic petabyte-class workload, and explain why the recent addition of FSx Intelligent-Tiering changes the cost story in a way that makes the pattern attractive even for teams that don't ingest petabytes.&lt;/p&gt;

&lt;p&gt;If you've ever felt the queasy sensation of paying twice for the same bytes — once to land on a stream, again to land in storage — this is for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "transitional data" actually means
&lt;/h2&gt;

&lt;p&gt;Most data falls into one of two cleanly shaped buckets. &lt;strong&gt;Durable data&lt;/strong&gt; is the stuff you keep — user records, orders, financial events, audit trails. It needs to live for years; you pay storage costs for those years and you get value over those years. &lt;strong&gt;Streaming data&lt;/strong&gt; is data you process in motion — clickstreams, telemetry, alerts — where the value is in real-time consumption.&lt;/p&gt;

&lt;p&gt;Transitional data is the awkward middle child. It's data that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is &lt;em&gt;produced&lt;/em&gt; in continuous high volume.&lt;/li&gt;
&lt;li&gt;Is &lt;em&gt;consumed&lt;/em&gt; shortly after production — usually within seconds or minutes, almost never more than an hour or two.&lt;/li&gt;
&lt;li&gt;After consumption, is either archived for compliance/audit or deleted entirely.&lt;/li&gt;
&lt;li&gt;Has &lt;em&gt;no value&lt;/em&gt; sitting on a transport between producer and consumer beyond getting from one side to the other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The classic examples are event-pipeline workloads: a producer ingests external events, enriches them, then hands them to one or more downstream consumers for further processing. The "stream" in the middle is conceptually a pipe, not a database. The events have ordering constraints within partitions and at-least-once delivery requirements, but nobody is querying the stream itself — it's purely a carrier.&lt;/p&gt;

&lt;p&gt;The instinct, drilled into us by ten years of cloud-native architecture talks, is to put transitional data on a streaming bus. Kinesis. MSK. Pub/Sub. Event Hubs. It's the default. It's what the conference slides recommend. It's what every reference architecture diagram shows.&lt;/p&gt;

&lt;p&gt;And it works fine — right up until your volume gets serious. At that point, the streaming bus stops being a transport and starts being the largest line item on your bill.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Kinesis math at scale, written out plainly
&lt;/h2&gt;

&lt;p&gt;Let me walk through the actual numbers. A Kinesis Data Streams shard in provisioned mode gives you, per &lt;a href="https://docs.aws.amazon.com/streams/latest/dev/service-sizes-and-limits.html" rel="noopener noreferrer"&gt;the AWS service limits documentation&lt;/a&gt;, &lt;strong&gt;1 MB/s of write throughput OR 1,000 records per second&lt;/strong&gt;, whichever you hit first. Read throughput is 2 MB/s per shard (5 transactions per second). Whichever cap you smash into first is the cap you actually have.&lt;/p&gt;

&lt;p&gt;Suppose you're ingesting at a steady 700 MB/s, which is what a healthy event pipeline at ~100 TB/day looks like. You need 700 shards just to hold the write rate, before any consumer fan-out, before any headroom for spikes, before any consideration of hot keys.&lt;/p&gt;

&lt;p&gt;Hot keys make the picture worse. The shard you land on is determined by &lt;code&gt;hash(partitionKey) % shardCount&lt;/code&gt;. If 5% of your traffic comes from one customer or one stream of telemetry, that 5% all goes to one shard. One shard, 1 MB/s. The other 699 shards sit there underutilized while your hot shard throttles. You can solve this with key spreading, but key spreading breaks per-key ordering, which is often the entire reason you picked a partitioned stream in the first place. So you scale up — to 1,000 shards, to 1,500 shards — to give the hot key room.&lt;/p&gt;

&lt;p&gt;Shards are billed per shard-hour. At list price the per-shard cost is small, but at 700–1,500 shards it adds up to tens of thousands of dollars per month before you've ingested a single byte. Add PUT payload unit charges (each 25 KB chunk counts as a unit, so 700 MB/s of small events is around 28,000 units per second), enhanced fan-out per consumer, extended retention, and you're well into the high five figures per month for a transport that exists solely to move bytes that won't be relevant in five minutes.&lt;/p&gt;

&lt;p&gt;On-demand mode looks nicer until you read the small print. It's billed per GB ingested, per GB retrieved, plus a baseline stream-hour fee. At 100 TB/day, the per-GB charges alone clear $30K/month before you count consumer reads.&lt;/p&gt;

&lt;p&gt;You can argue with the exact dollar figures — they vary by region, by reserved-capacity discounts, by your actual fan-out fanout pattern — but the shape is the same regardless. &lt;strong&gt;The cost grows linearly with the bytes you push through, and the bytes you push through are exactly the same bytes you have to store somewhere durable anyway.&lt;/strong&gt; You are paying twice.&lt;/p&gt;




&lt;h2&gt;
  
  
  MSK isn't the answer either
&lt;/h2&gt;

&lt;p&gt;The instinct after "Kinesis is expensive" is "let's use Kafka instead." Kafka is open-source, it's mature, the ecosystem is enormous. AWS gives us MSK so we don't have to operate the brokers ourselves. Surely that's cheaper.&lt;/p&gt;

&lt;p&gt;It's not, for this shape of workload. Let me show the math.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/msk/pricing/" rel="noopener noreferrer"&gt;MSK pricing&lt;/a&gt;, as of 2026, looks roughly like this for provisioned clusters: broker instances at around $0.20/hour for the small kafka.m7g.large class and proportionally more for larger ones, EBS storage at $0.10 per GB-month for the broker volumes, plus inter-AZ data transfer for replication. MSK Express adds another $0.01/GB ingested. MSK Serverless charges $0.75 per cluster-hour, $0.0015 per partition-hour, $0.10/GB in and $0.05/GB out.&lt;/p&gt;

&lt;p&gt;For a 100 TB/day workload you'll want at least 6–9 brokers of a non-trivial instance class to handle the throughput with replication-factor-3 durability. That's $3K–$6K/month in broker hours alone. Then you need EBS sized to hold a few hours of buffer per broker — say 2 TB per broker times 9 brokers times $0.10/GB-month, another $1,800/month. Then cross-AZ replication of every byte ingested: at 100 TB/day across three AZs you're shifting roughly 200 TB/day cross-AZ for replication, and AWS charges around $0.01/GB for that — call it $60K/month if you're unlucky on routing.&lt;/p&gt;

&lt;p&gt;Add the operational burden: MSK frees you from broker installs but not from partition rebalancing decisions, broker right-sizing, version upgrades, ZooKeeper-to-KRaft migration, ACL management, monitoring, and the eternal "why is my consumer lag spiking" investigations. That's not a billed line item but it's a real cost.&lt;/p&gt;

&lt;p&gt;Rough total for the same 100 TB/day shape: $70K–$90K/month, give or take. Comparable to Kinesis, more operational headache, no architectural advantage for transitional data because — and this is the key point — &lt;strong&gt;you are still paying a transport service to carry every single byte through brokers and across AZs&lt;/strong&gt;, even though those bytes are going to be discarded within minutes.&lt;/p&gt;

&lt;p&gt;The rule of thumb in the industry has settled at MSK being roughly 3–5× more expensive than Kinesis once you count operational overhead. For specifically transitional data, where the value of the byte-time on the wire is approximately zero, either choice is the wrong economic shape.&lt;/p&gt;

&lt;p&gt;There has to be a different model.&lt;/p&gt;




&lt;h2&gt;
  
  
  The mental shift: payload on a filesystem, pointer on the bus
&lt;/h2&gt;

&lt;p&gt;Here's the model that actually fits. It's the &lt;strong&gt;outbox pattern&lt;/strong&gt;, but the outbox is a shared filesystem instead of an in-process table.&lt;/p&gt;

&lt;p&gt;The producer doesn't push bytes through a transport. Instead it writes a file to a shared filesystem — a real file, on a real path, with normal POSIX semantics. Then it publishes a tiny pointer message through whatever bus you have lying around — Kinesis, SNS, SQS, even a small Kafka topic. The message is a hundred bytes: file path, batch id, size, checksum, maybe a couple of routing keys.&lt;/p&gt;

&lt;p&gt;The consumer reads the pointer message, mounts the same filesystem, opens the file at the given path, streams the bytes, processes them, and acknowledges the message. The bytes never traverse the transport. The transport only carries metadata about where to find the bytes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdu4lyw0slbgwx6ukbp5y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdu4lyw0slbgwx6ukbp5y.png" alt="Architecture: producer writes payload to FSx, pointer flows through bus, consumer mounts FSx and reads" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That diagram is the entire pattern at the architectural level. Three boxes. The interesting part is everything you don't see in it: backpressure, atomic writes, idempotent retry, partitioning, batching, compression, file-system tuning. We'll get to all of that.&lt;/p&gt;

&lt;p&gt;But first, why this change of frame is so cost-effective:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The bus carries small messages, so the bus cost collapses.&lt;/strong&gt; Pointer messages are roughly two hundred bytes each. A bus that was strained to carry 700 MB/s of payload is now carrying maybe 200 KB/s of pointers. Shard counts can drop by an order of magnitude or more. One real architecture I've seen reduced a Kinesis shard count from 700 to 32 — a 95% reduction in transport spend, with no change to the actual byte volume being moved.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The filesystem is billed for what it stores, not for what passes through it.&lt;/strong&gt; You pay for the bytes that exist, not the bytes that have existed and have already been consumed and deleted. If your retention is one hour, you pay for one hour's worth of bytes resident at any given moment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The filesystem gives you primitives the bus doesn't.&lt;/strong&gt; Snapshots. Random access. Concurrent multi-reader semantics. Standard POSIX tools to inspect, validate, and debug. Your data-engineering team can &lt;code&gt;ls&lt;/code&gt; your transitional data. They cannot &lt;code&gt;ls&lt;/code&gt; a Kinesis stream.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The compute path is the same speed or faster.&lt;/strong&gt; A modern NFS mount on the same VPC moves data faster than any HTTP-based streaming bus. Once you understand the math, the streaming-bus model is the &lt;em&gt;slow&lt;/em&gt; option, not the fast one.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So why didn't everyone do this ten years ago? Because the filesystem options didn't scale. EFS exists, but at the throughput required for petabyte-class workloads EFS becomes the most expensive option in the room. We'll get to that comparison. The reason this pattern is suddenly viable is that FSx for OpenZFS, particularly with Intelligent-Tiering, gives you the performance and the multi-AZ durability of a real production filesystem at a cost that beats every transport on the market.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why FSx for OpenZFS, specifically
&lt;/h2&gt;

&lt;p&gt;There are at least six AWS storage services you could plausibly use here. Let's eliminate them one by one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S3 direct from producers.&lt;/strong&gt; Tempting because S3 is cheap, well-understood, and infinite. But: producers write small files (a few hundred KB to a few MB per micro-batch), and S3 has a small-files problem. Per-PUT overhead, eventual-consistency caveats on listings (mostly fixed but historically a footgun), and a multipart-upload model that's clumsy for the size of files you actually produce. Consumers also have to do an LIST or rely on S3 event notifications, which adds latency and complexity. S3 is the &lt;em&gt;destination&lt;/em&gt; tier, not the &lt;em&gt;working&lt;/em&gt; tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EFS Standard.&lt;/strong&gt; Native NFS, multi-AZ, easy to mount everywhere. At $0.30/GB-month for storage and elastic throughput costing $0.03/GB read and $0.06/GB written, a 100 TB/day workload runs about $300K/month in throughput charges alone in elastic mode. EFS Provisioned trades that for a fixed throughput fee — at the throughput levels we need, around $90K/month. Still expensive, and EFS latency is consistently in the single-digit-millisecond range rather than the sub-millisecond range you'd want for a hot working tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FSx for Lustre.&lt;/strong&gt; Genuinely fast — sub-millisecond latency, hundreds of GB/s aggregate throughput in larger deployments, the workhorse of HPC. But it's single-AZ only, and the failure model for transitional data really wants multi-AZ. You can stitch together cross-AZ Lustre with replication, but the cost balloons and you lose simplicity. Lustre also requires a kernel module on clients, which is operationally awkward in mixed container environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FSx for Windows / FSx for ONTAP.&lt;/strong&gt; Both work, both support multi-AZ, both add complexity (SMB licensing, ONTAP feature surface). Neither is wrong, neither is the obvious right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FSx for OpenZFS.&lt;/strong&gt; Multi-AZ with synchronous replication to a standby in another AZ. NFS protocol (v3 / v4.0 / v4.1 / v4.2) — clients are the standard Linux kernel NFS client, no special drivers. SSD-backed. Sub-millisecond latency. Native LZ4 compression at the file-system level. POSIX semantics, including the all-important atomic-rename guarantee we need for safe writes. Snapshots, encryption at rest, KMS integration. And — this is the new part — an Intelligent-Tiering storage class that prices in at roughly 85% less than the SSD class.&lt;/p&gt;

&lt;p&gt;The recent Intelligent-Tiering announcement is what tipped this from "viable" to "obvious." Let's look at what it actually gives you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Intelligent-Tiering, and why it matches transitional-data access patterns perfectly
&lt;/h2&gt;

&lt;p&gt;FSx Intelligent-Tiering is a separate storage class for FSx for OpenZFS that introduces three tiers within a single namespace. Per &lt;a href="https://aws.amazon.com/blogs/aws/announcing-amazon-fsx-intelligent-tiering-a-new-storage-class-for-fsx-for-openzfs/" rel="noopener noreferrer"&gt;the AWS announcement&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frequent Access&lt;/strong&gt; — data touched within the last 30 days. Baseline tier, sub-millisecond reads from cache, full performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrequent Access&lt;/strong&gt; — data not touched for 30 to 90 days. Roughly 44% cheaper than Frequent Access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archive Instant Access&lt;/strong&gt; — data not touched for 90+ days. Roughly 65% cheaper than Infrequent Access. Still online, no restore needed; first-byte latency in the tens of milliseconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multiply the discounts and you get a storage cost in the Archive tier that's roughly 80% lower than the Frequent Access baseline. The marketing line is "up to 85% lower than the existing SSD storage class," which checks out arithmetically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feupl17dmsqsuo7falgyw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feupl17dmsqsuo7falgyw.png" alt="Intelligent-Tiering: three tiers, age-driven migration, single namespace" width="799" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A file system using Intelligent-Tiering supports up to 400K IOPS and 20 GB/s of throughput, with a minimum provisioned throughput floor of 160 MBps. You can optionally provision an SSD read cache on top of the tiered storage to keep hot files at sub-millisecond latency even after they've technically migrated to a colder tier.&lt;/p&gt;

&lt;p&gt;Here's why this matches transitional data so perfectly: the access pattern for an outbox is "hot for minutes, cold forever after." A batch file is written, read once or twice by consumers within the first few minutes, and then never touched again unless someone is doing forensic analysis. That's exactly the pattern Intelligent-Tiering is optimized for. The hot files stay on the fast tier and serve consumers at sub-ms latency; the post-consumption files quietly slide down to the Archive Instant Access tier, where they cost almost nothing but are still readable on demand if you need to audit.&lt;/p&gt;

&lt;p&gt;You don't have to design this. You don't have to set up lifecycle rules. The file system does it. From the application's perspective, every file is at the same path in the same namespace. The pricing optimization happens underneath.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture in two passes
&lt;/h2&gt;

&lt;p&gt;Let's walk through the producer side and the consumer side in turn. This is the actual shape of a working implementation, lightly cleaned up from a real codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Producer pass
&lt;/h3&gt;

&lt;p&gt;A producer's job is to take a batch of events, compress them, write the result durably to FSx, and publish a notification message to the bus. The full sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.  Build payload                  → ZSTD-compressed protobuf in a direct ByteBuffer
2.  Resolve partition folder       → sanitize id, validate against FSx root
3.  Create batch directory          → /fsx/&amp;lt;part&amp;gt;/year=Y/month=M/day=D/hour=H/batch-&amp;lt;uuid&amp;gt;/
4.  Reserve in-flight bytes         → byte-based backpressure (more on this below)
5.  Write to a unique temp file     → data.bin.tmp.&amp;lt;uuid&amp;gt;.1
6.  fsync the temp file             → optional, controlled by config
7.  Atomic rename to data.bin       → Files.move(tmp, final, ATOMIC_MOVE)
8.  Publish pointer message         → { filePath, batchId, sizeBytes, crc32c }
9.  Release in-flight bytes         → permit auto-closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 5 onwards is the &lt;strong&gt;safe-write protocol&lt;/strong&gt;, and it's the heart of correctness. We'll spend the next section on it. Steps 1–4 are application logic and bookkeeping; they're conceptually simple but the byte-based backpressure deserves its own discussion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consumer pass
&lt;/h3&gt;

&lt;p&gt;Consumers do the inverse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.  Pull notification message from bus
2.  Validate pointer (path within allowed root, checksum optional)
3.  Open the file via the local NFS mount
4.  Stream the bytes — gather them, decompress, deserialize
5.  Process the events
6.  Acknowledge / delete the bus message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no LIST, no scan, no polling for new files. Consumers are driven by the bus. The bus tells them what to read; the filesystem holds what they read.&lt;/p&gt;

&lt;p&gt;This separation matters operationally. A slow consumer doesn't back up the producer's writes — the producer's writes are already complete on the filesystem. A bus outage doesn't stall the producer beyond the publish step; the file is already durable. A consumer crash doesn't lose data; the next consumer pulls the same notification (because we hadn't acked) and reads the same file.&lt;/p&gt;




&lt;h2&gt;
  
  
  The safe-write protocol, in detail
&lt;/h2&gt;

&lt;p&gt;The single most important piece of code in this entire pattern is how a producer commits a batch to the filesystem. Get this wrong and you have torn writes, partial reads, racing retries deleting each other's work. Get it right and you have an outbox that survives crashes without complicated coordination.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcz93gxmap9van02h7tjs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcz93gxmap9van02h7tjs.png" alt="Safe-write protocol: temp file, vectored write, fsync, atomic rename, then publish" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The protocol has six steps. Each one is doing real work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — create the batch directory.&lt;/strong&gt; &lt;code&gt;Files.createDirectories(batchDirectory)&lt;/code&gt;. POSIX &lt;code&gt;mkdir -p&lt;/code&gt;. This is idempotent and cheap. The batch directory is computed deterministically from the partition id and a UUID-based batch id, with time-based folder buckets above it to avoid one directory eventually holding millions of entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — open a unique temp file.&lt;/strong&gt; Critically, the temp filename includes a per-attempt unique token. The template I use is something like &lt;code&gt;data.bin.tmp.&amp;lt;uuid&amp;gt;.&amp;lt;attempt&amp;gt;&lt;/code&gt;. The reason for the uniqueness is that retries must not collide with each other or with a previous crashed attempt. If attempt 1 crashed mid-write and left a &lt;code&gt;data.bin.tmp.abc123.1&lt;/code&gt; orphan, attempt 2 must open &lt;code&gt;data.bin.tmp.def456.2&lt;/code&gt; — a different name — so neither attempt steps on the other. The &lt;code&gt;StandardOpenOption.CREATE_NEW&lt;/code&gt; flag enforces this: the open fails if the file already exists, which is exactly what we want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — vectored write.&lt;/strong&gt; This is where the implementation rewards careful engineering. A naïve writer would loop over composite buffer components and issue one &lt;code&gt;write()&lt;/code&gt; per component. A composite payload of a hundred small buffers becomes a hundred syscalls.&lt;/p&gt;

&lt;p&gt;Instead we use &lt;code&gt;FileChannel.write(ByteBuffer[])&lt;/code&gt;, which is the JDK's bridge to the kernel's &lt;code&gt;writev&lt;/code&gt; syscall. The kernel takes a whole array of buffer descriptors and does the gather in one call. Many small components, one syscall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;PayloadWriteOutcome&lt;/span&gt; &lt;span class="nf"&gt;writeGatheredBuffers&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="no"&gt;CRC32C&lt;/span&gt; &lt;span class="n"&gt;checksumCrc32c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checksumEnabled&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="no"&gt;CRC32C&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;totalBytesWritten&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nextBufferIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nextReadableBufferIndex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadBuffers&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextBufferIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;payloadBuffers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ByteBuffer&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;gatherSources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nextGatherSources&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadBuffers&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nextBufferIndex&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;updateChecksum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checksumCrc32c&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gatherSources&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasRemaining&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatherSources&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;bytesWritten&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fileChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatherSources&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// writev under the hood&lt;/span&gt;
            &lt;span class="n"&gt;totalBytesWritten&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;bytesWritten&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;nextBufferIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nextReadableBufferIndex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadBuffers&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                  &lt;span class="n"&gt;nextBufferIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;gatherSources&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PayloadWriteOutcome&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totalBytesWritten&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;checksumCrc32c&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;checksumCrc32c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a chunking detail: most kernels cap the gather size somewhere around &lt;code&gt;IOV_MAX&lt;/code&gt; (1024 on Linux). The code respects that with &lt;code&gt;GATHER_WRITE_BUFFER_LIMIT = 1024&lt;/code&gt;. A payload with more components is gathered in 1024-buffer slices.&lt;/p&gt;

&lt;p&gt;The CRC32C is computed during the same pass — one walk over the bytes, two outputs. Computing the checksum as a separate post-pass would double the byte traffic for nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — fsync.&lt;/strong&gt; Optional. &lt;code&gt;fileChannel.force(forceMetadata)&lt;/code&gt; issues &lt;code&gt;fdatasync&lt;/code&gt; (or &lt;code&gt;fsync&lt;/code&gt; if metadata is included) to push the bytes to persistent storage before returning. On a synchronously-replicated multi-AZ FSx, this also blocks until the standby AZ has acknowledged the write. It's expensive (in latency, not bytes) but it gives you durability before the rename. Most workloads can skip it if they tolerate the small window between rename and standby-AZ sync; high-durability workloads should turn it on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — atomic rename.&lt;/strong&gt; &lt;code&gt;Files.move(tempFile, finalFile, StandardCopyOption.ATOMIC_MOVE)&lt;/code&gt;. POSIX guarantees that a &lt;code&gt;rename(2)&lt;/code&gt; is atomic on the same filesystem: an observer either sees the old name or the new name, never both, never neither, never a partial state. Translated to our case: a consumer either sees &lt;code&gt;data.bin&lt;/code&gt; (and can read the full payload) or doesn't see it at all (and will retry later). It never sees a partial &lt;code&gt;data.bin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guarantee is what makes the whole pattern correct. There is no other coordination, no manifest file, no two-phase commit. The single atomic rename is the publish moment.&lt;/p&gt;

&lt;p&gt;There's a subtle case: &lt;code&gt;ATOMIC_MOVE&lt;/code&gt; providers on some implementations will &lt;em&gt;replace&lt;/em&gt; an existing target without erroring. Before issuing the move we therefore probe for an existing &lt;code&gt;data.bin&lt;/code&gt;. If it exists, the producer treats this as an idempotent retry — more on idempotency in a moment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6 — publish pointer.&lt;/strong&gt; Only after the rename succeeds do we publish the notification message. The order matters: if we published first and then the rename failed, consumers would chase a phantom file.&lt;/p&gt;

&lt;p&gt;The pointer message is intentionally tiny:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/fsx/&amp;lt;partition&amp;gt;/year=2026/month=05/day=24/hour=14/batch-abc123/data.bin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"batchId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partitionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;partition&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sizeBytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;524288&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"crc32c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f3c19a2b"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two hundred bytes, give or take. The bus carries the pointer. The filesystem holds the payload. The transport bill goes from "scale with bytes" to "scale with message count" — and message count for a typical batching workload is in the hundreds-per-second range, well within any bus's comfort zone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Idempotent retry without re-reading the bytes
&lt;/h2&gt;

&lt;p&gt;Retries are unavoidable. Producers crash, networks blip, NFS metadata operations stall. The framework has to handle all of these without producing duplicate files or losing the in-flight write.&lt;/p&gt;

&lt;p&gt;The idempotency rule I use is intentionally simple: &lt;strong&gt;if the final &lt;code&gt;data.bin&lt;/code&gt; already exists at the expected path, treat it as a successful prior write — without re-reading the bytes.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fileSystemOperations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finalFilePath&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;existingResultAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finalFilePath&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedBytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// in toExistingResult:&lt;/span&gt;
&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;existingBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fileSystemOperations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finalFilePath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingBytes&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;expectedBytes&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FsxWriteException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Existing FSx file does not match expected payload size: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;finalFilePath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;toWriteResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finalFilePath&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;existingBytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedChecksumCrc32c&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The size-only check is deliberate. Re-reading a multi-megabyte compressed payload over NFS just to verify a checksum would double the IO cost of every retry, and the atomic-rename protocol already ensures that &lt;em&gt;if&lt;/em&gt; the final file exists, it contains a complete, validated payload. The size check is a cheap sanity guardrail against bizarre cases where another process wrote a different file with the same name.&lt;/p&gt;

&lt;p&gt;If the size matches, we return success with an &lt;code&gt;idempotentExistingWrite=true&lt;/code&gt; flag. The caller sees a normal success result and continues. The notification publish step is then idempotent on its own end — most buses dedup on a message id you can derive deterministically from the batch id.&lt;/p&gt;

&lt;p&gt;This is at-least-once delivery, not exactly-once. The consumer side has to dedup by &lt;code&gt;batchId&lt;/code&gt;. That's the standard contract for event pipelines and it's fine; building exactly-once on top of at-least-once with idempotent consumers is a solved problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Byte-based backpressure, not request-based
&lt;/h2&gt;

&lt;p&gt;Naive backpressure limits the &lt;em&gt;number&lt;/em&gt; of concurrent operations. "No more than 16 writes at a time." That works when every write is roughly the same size. It breaks the moment one write is 10 KB and the next is 100 MB. The 16-operation limit lets a single bad batch consume gigabytes of in-flight memory while the limiter thinks it's still doing the right thing.&lt;/p&gt;

&lt;p&gt;Instead, the limiter I use bounds &lt;em&gt;bytes in flight&lt;/em&gt;, not operation count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FsxInFlightByteLimiter&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;maxInFlightBytes&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxInFlightOperations&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;inFlightBytes&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;inFlightOperations&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PendingAcquire&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pendingAcquires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayDeque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;CompletableFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Permit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;requestedBytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                      &lt;span class="nc"&gt;ScheduledExecutorService&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;PendingAcquire&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PendingAcquire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toReservedBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestedBytes&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="kd"&gt;synchronized&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pendingAcquires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;canAcquire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;acquireNow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;completeNow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;pendingAcquires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toMillis&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MILLISECONDS&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;future&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;canAcquire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PendingAcquire&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inFlightOperations&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxInFlightOperations&lt;/span&gt;
                &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;inFlightBytes&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reservedBytes&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;maxInFlightBytes&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two dimensions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;maxInFlightOperations&lt;/code&gt; — caps the number of concurrent writes (still useful to prevent thundering herds on the file I/O thread pool).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxInFlightBytes&lt;/code&gt; — caps the aggregate payload size of concurrent writes (the real protection against memory blow-up).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both must be satisfied before a write is admitted. When neither is satisfied, the request is queued; when capacity becomes available (a running write completes), the queue is drained in FIFO order. The whole thing is non-blocking — callers get a &lt;code&gt;CompletableFuture&amp;lt;Permit&amp;gt;&lt;/code&gt; and can compose it into their own async chain.&lt;/p&gt;

&lt;p&gt;The detail that matters in production: oversized requests (those larger than &lt;code&gt;maxInFlightBytes&lt;/code&gt; on their own) need to be handled. The implementation clamps &lt;code&gt;reservedBytes&lt;/code&gt; to the limit, so an oversized write &lt;em&gt;can&lt;/em&gt; run, but it runs alone. The caller is responsible for not handing in 10 GB requests; the framework's protection is against the runaway accumulation of "merely large" requests, not against a single pathologically-large one.&lt;/p&gt;

&lt;p&gt;Why is this fancy enough to need its own class? Because the difference between request-based and byte-based limiting is the difference between "the limiter does what I think it's doing" and "memory exploded at 3 AM because one batch was unusually large." Subtle bugs there are expensive to debug.&lt;/p&gt;




&lt;h2&gt;
  
  
  Naming, layout, and the partition directory tree
&lt;/h2&gt;

&lt;p&gt;The filesystem layout matters more than it might seem. Here's the convention I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/fsx-root/
  &amp;lt;partitionId&amp;gt;/
    year=2026/
      month=05/
        day=24/
          hour=14/
            batch-&amp;lt;uuid&amp;gt;/
              data.bin
              [optional metadata files]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few rationales worth calling out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partition as the top-level grouping.&lt;/strong&gt; Within a single partition, writes are serialized by the producer's own logic. Across partitions, writes are independent. Putting partition first in the path lets consumers scan or process one partition without walking the whole tree, and lets retention policies operate at partition granularity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hive-style time-bucket folders.&lt;/strong&gt; &lt;code&gt;year=Y/month=M/day=D/hour=H/&lt;/code&gt; is the convention Hive and Spark and a hundred analytics tools recognize. If you ever want to plug a query engine over the FSx tree (DuckDB, Presto, anything), the partitioning is already in a shape it understands. More importantly, time-bucketing prevents any single directory from accumulating millions of entries — a real performance issue for NFS metadata operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-batch directory.&lt;/strong&gt; Each batch gets its own folder, not just its own file. This gives you room to add sidecar files later (index files, per-batch metadata, manifest JSON) without breaking the existing readers. The batch id is a UUID, so collisions are not a concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;data.bin&lt;/code&gt; as the final filename.&lt;/strong&gt; Boring, predictable, descriptive. The temp file uses the same final name with a &lt;code&gt;.tmp.&amp;lt;token&amp;gt;.&amp;lt;attempt&amp;gt;&lt;/code&gt; suffix so the atomic rename target is unambiguous.&lt;/p&gt;

&lt;p&gt;There's also a critical security check: the partition id is sanitized and validated to stay under the configured FSx root. A producer cannot pass a partition id like &lt;code&gt;../../../../etc/passwd&lt;/code&gt; and write outside the intended tree. The sanitizer rejects path-traversal characters and the framework asserts the resolved path is a descendant of the root before doing anything with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Java library question, and why "kernel NFS + plain NIO" wins
&lt;/h2&gt;

&lt;p&gt;When you start looking for a Java library to talk to FSx OpenZFS, you find a mess. There's no official AWS SDK for FSx data-plane operations — the AWS SDK only handles the management plane (create/destroy file systems). For the actual file I/O you're on your own.&lt;/p&gt;

&lt;p&gt;I went down the rabbit hole:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;aws-sdk-java-v2&lt;/code&gt; + S3AsyncClient + S3 Access Points for FSx.&lt;/strong&gt; Works if you expose your FSx file system through an S3 access point. Mature, async, multipart, retries, integrates with the AWS SDK ecosystem. The downside is the S3-style access has tens-of-milliseconds latency rather than the sub-millisecond NFS-mount latency. For batch jobs that's fine; for hot transitional data you give up most of the perf advantage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;dCache/nfs4j&lt;/code&gt;.&lt;/strong&gt; A pure-Java NFSv3 and NFSv4 implementation. The most serious Java NFS library available. It's actively maintained, has a JMH benchmarks module, runs on Java 17. If you absolutely need to write your own NFS client (or extend one), this is where you'd start. But it's not a turnkey FSx client — you'd be building protocol code, and AWS's own performance documentation is opinionated about mount-time options that are easier to apply with the kernel client.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;EMCECS/nfs-client-java&lt;/code&gt;.&lt;/strong&gt; NFSv3 only, dependent on a years-old version of Netty. Workable for legacy use, not a foundation for a petabyte-scale system in 2026.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;SMBJ&lt;/code&gt;, &lt;code&gt;jcifs&lt;/code&gt;.&lt;/strong&gt; SMB protocol clients. Wrong protocol family — FSx for OpenZFS doesn't speak SMB.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hadoop / Spark NFS connectors.&lt;/strong&gt; Useful for ideas about request pipelining, not a foundation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conclusion I came to is the boring one: &lt;strong&gt;mount the FSx file system with the kernel NFS client, and use the standard Java NIO &lt;code&gt;FileChannel&lt;/code&gt; / &lt;code&gt;AsynchronousFileChannel&lt;/code&gt; against the mount point.&lt;/strong&gt; AWS's tuning guidance is much more opinionated about client-side mount options than about language libraries. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;nconnect=16&lt;/code&gt; (or up to your kernel's supported maximum) to parallelize NFS over 16 TCP connections.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;rsize=1048576&lt;/code&gt; and &lt;code&gt;wsize=1048576&lt;/code&gt; for 1 MiB read/write chunks.&lt;/li&gt;
&lt;li&gt;Use NFSv4.1 for the locking and the cleaner failover semantics.&lt;/li&gt;
&lt;li&gt;Place producers and consumers in the same AZ as the file system primary for sub-ms latency and to avoid cross-AZ data transfer charges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you do those things, the kernel does the heavy lifting. Java NIO becomes the thinnest possible wrapper over the syscalls. You're competing with the kernel for performance, and the kernel wins.&lt;/p&gt;

&lt;p&gt;So the implementation pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;producer/consumer container
        │
        ▼
   /fsx-root/  (Linux NFS mount, nconnect=16, NFSv4.1)
        │
        ▼
   AsynchronousFileChannel, FileChannel.write(ByteBuffer[])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No custom protocol code. No special drivers. No language-specific clients. Just the things the kernel is already tuned to do well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure semantics, in painful detail
&lt;/h2&gt;

&lt;p&gt;The honest list of what happens when things go wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Producer crashes after writing the temp file but before the atomic rename.&lt;/strong&gt; The temp file is an orphan. No consumer ever sees it (consumers only look for &lt;code&gt;data.bin&lt;/code&gt;). The producer's next attempt uses a new unique temp filename, so it doesn't collide. The orphan is cleaned up by a TTL job or a periodic sweep — the framework explicitly does not handle this, because the cleanup cadence is a deployment decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Producer crashes after the rename but before publishing the notification.&lt;/strong&gt; This is the dangerous one. The file is on FSx, but no consumer knows it exists. On the next retry, the producer attempts to write again with the same batch id, sees the existing &lt;code&gt;data.bin&lt;/code&gt;, recognizes it as an idempotent retry, and proceeds to publish the notification. The consumer dedups by batch id so it doesn't matter that the producer might publish twice. The net result is at-least-once delivery, with at-most-once &lt;em&gt;processing&lt;/em&gt; preserved by the consumer's dedup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FSx write succeeds, notification publish fails.&lt;/strong&gt; Same picture as above. The file stays on FSx. The framework returns the publish failure to the caller, who is expected to retry. Cleanup is deferred to the retention policy. The pattern is explicitly non-transactional: FSx and the bus are composed, not atomically coupled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notification publish succeeds, FSx file is later corrupted or deleted.&lt;/strong&gt; This shouldn't happen — FSx is durable storage — but if it did, the consumer would get a read error and fail the message. With a dead-letter queue it would surface as an actionable alert. The CRC32C in the pointer message lets the consumer detect a corrupted file before deserializing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumer crashes mid-read.&lt;/strong&gt; Bus message isn't acked. After visibility timeout, another consumer picks it up and reads the same file. Same file, same bytes, same processing. Dedup by batch id at the consumer side keeps the processing semantics correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FSx becomes unavailable mid-write.&lt;/strong&gt; Writes fail with retryable exceptions. The framework retries with exponential backoff and jitter (the standard schedule: base delay, doubled on each failure, with a random jitter component bounded by the current ceiling). After the max attempts, the failure is propagated to the caller, who is expected to translate it into a rate-limit response or a circuit-break upstream. Critically, &lt;strong&gt;the framework does not sleep on the I/O executor thread&lt;/strong&gt;. Retries are scheduled on a dedicated scheduler so the I/O threads stay free to handle other writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FSx is healthy but slow (a metadata operation stalls).&lt;/strong&gt; The outer write timeout protects callers. The framework wraps the write future in a &lt;code&gt;withTimeout&lt;/code&gt; that completes the caller-visible future with a &lt;code&gt;TimeoutException&lt;/code&gt; after a configured deadline, while the actual write is allowed to complete in the background. The framework holds the pooled direct buffer reference until the real write finishes, not until the timeout fires, so we never release memory that NIO is still using. The size of this subtle bookkeeping difference, in production, is the difference between "occasional timeouts" and "occasional segfaults."&lt;/p&gt;

&lt;p&gt;That last point is worth dwelling on. Caller-visible timeouts must not free resources that the underlying I/O still owns. The pattern I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CompletableFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FsxFileWriteResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;writeFuture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inFlightByteLimiter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;acquire&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenCompose&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permit&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;writeWithPermit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payloadBytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;permit&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;writeFuture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whenComplete&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;throwable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writeFuture&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getWriteTimeout&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whenComplete&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;throwable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;recordWriteMetrics&lt;/span&gt;&lt;span class="o"&gt;(...));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;payload.close()&lt;/code&gt; only runs when the inner &lt;code&gt;writeFuture&lt;/code&gt; completes for real, regardless of whether the outer timeout-wrapped future has already returned. The buffer reference outlives the caller-visible future, by design. This is the kind of detail that doesn't show up in any architecture diagram but determines whether your system stays up under load.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost worked example at 100 TB/day
&lt;/h2&gt;

&lt;p&gt;Let's do the actual math. The workload: 100 TB/day of compressed transitional data, 24-hour retention, multi-AZ durability required. List prices in us-east-1.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xkykdea09npa9g96106.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xkykdea09npa9g96106.png" alt="Approximate monthly cost at 100 TB/day across the candidate transports" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Approximate monthly cost&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EFS Elastic Throughput&lt;/td&gt;
&lt;td&gt;~$300K&lt;/td&gt;
&lt;td&gt;$0.30/GB-month storage + $0.03/GB read + $0.06/GB written kills you at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EFS Provisioned Throughput&lt;/td&gt;
&lt;td&gt;~$90K&lt;/td&gt;
&lt;td&gt;Storage + ~5 GB/s provisioned throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSK provisioned cluster&lt;/td&gt;
&lt;td&gt;~$70–90K&lt;/td&gt;
&lt;td&gt;Brokers + EBS + cross-AZ replication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kinesis Data Streams (700 shards)&lt;/td&gt;
&lt;td&gt;~$70K&lt;/td&gt;
&lt;td&gt;Shards + PUT units, before consumer fan-out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 direct from producers&lt;/td&gt;
&lt;td&gt;~$67K&lt;/td&gt;
&lt;td&gt;$0.023/GB storage cheap, but the small-files PUT overhead and operational complexity push effective cost up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FSx for OpenZFS SSD&lt;/td&gt;
&lt;td&gt;~$27K&lt;/td&gt;
&lt;td&gt;$0.18/GB-month storage + ~5 GB/s provisioned throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FSx for OpenZFS Intelligent-Tiering&lt;/td&gt;
&lt;td&gt;~$5K&lt;/td&gt;
&lt;td&gt;Same throughput, but storage tier-shifts to Archive within days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two clarifications. First, these are illustrative ballpark figures for the &lt;em&gt;same shape of workload&lt;/em&gt;, not benchmarked actuals. Your numbers will differ. Second, the FSx Intelligent-Tiering number assumes a typical transitional-data access pattern — files are read within the first few minutes and then never touched again, so they migrate to the Archive tier quickly. If your access pattern is heavier (consumers re-reading historical data frequently), the savings shrink because more data stays warm.&lt;/p&gt;

&lt;p&gt;The headline numbers are real, though. Moving from a streaming-bus model to a filesystem-pointer model knocks the cost down by roughly an order of magnitude at this scale. Adding Intelligent-Tiering knocks it down by another factor of five-ish. For a 100 TB/day workload you're looking at moving from ~$70K/month to ~$5–27K/month. That's not a marginal optimization. That's the kind of saving that funds the project on its own.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability and what to watch
&lt;/h2&gt;

&lt;p&gt;The framework I work with emits a small set of metrics, all tagged by partition id, that have proven repeatedly useful in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fsx.write.success&lt;/code&gt; / &lt;code&gt;fsx.write.failed&lt;/code&gt; — counters of completed writes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsx.write.latency&lt;/code&gt; — write latency in milliseconds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsx.write.bytes&lt;/code&gt; — total bytes written&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsx.write.retry&lt;/code&gt; — counter of retry attempts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsx.write.inflight.bytes&lt;/code&gt; — current bytes reserved by running writes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsx.write.inflight.operations&lt;/code&gt; — current running write count&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsx.write.backpressure.pending&lt;/code&gt; — current queue depth on the limiter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watching &lt;code&gt;inflight.bytes&lt;/code&gt; against &lt;code&gt;maxInFlightBytes&lt;/code&gt; tells you immediately whether you're sized correctly. Watching &lt;code&gt;backpressure.pending&lt;/code&gt; tells you whether producers are being throttled by the limiter (a good sign that your downstream is saturated) versus by FSx itself (which would show up as slow &lt;code&gt;write.latency&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;On the FSx side, watch the published CloudWatch metrics: &lt;code&gt;DataReadBytes&lt;/code&gt;, &lt;code&gt;DataWriteBytes&lt;/code&gt;, &lt;code&gt;ClientConnections&lt;/code&gt;, &lt;code&gt;NetworkThroughputUtilization&lt;/code&gt;, &lt;code&gt;FileServerDiskIopsUtilization&lt;/code&gt;, &lt;code&gt;FileServerCacheHitRatio&lt;/code&gt;. The cache hit ratio in particular is the early-warning signal for "I should provision an SSD read cache" if your access pattern starts re-touching aged files.&lt;/p&gt;




&lt;h2&gt;
  
  
  When this pattern is the wrong answer
&lt;/h2&gt;

&lt;p&gt;The honest list, because the worst thing you can do with an architecture article is sell the pattern as universal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your traffic is small.&lt;/strong&gt; If you're moving gigabytes per day, not terabytes per day, the cost gap closes and the operational overhead of a shared filesystem outweighs the savings. Use the streaming bus. It's fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your producers and consumers are in different VPCs / regions.&lt;/strong&gt; Cross-VPC NFS works but it's awkward. Cross-region defeats the whole point — at that point you're back to needing a real transport. If your topology is multi-region active-active, this pattern fits poorly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need true streaming, with downstream subscribers wanting to react to each event individually.&lt;/strong&gt; This pattern is fundamentally batched. The minimum unit of work is a batch file, not an event. If your downstream wants per-event push semantics within single-digit-millisecond latency, you want a real streaming bus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your consumers are serverless functions that can't mount NFS.&lt;/strong&gt; Lambda can't mount FSx for OpenZFS (it can mount EFS, but not FSx OpenZFS). If your consumer side is Lambda, your options are (a) use EFS instead, (b) front FSx with S3 access points and have Lambda read via S3 (sacrificing latency), or (c) use a small ECS task as an intermediary. None of those are great. Pick a different pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need the bus to provide ordering, replay, or stream-processing semantics.&lt;/strong&gt; A pointer-on-bus model gives you ordering only if the bus already provides it (Kinesis with strict shard partitioning, Kafka with partition keys). It does not give you stream replay or windowed aggregations or any of the operations that real stream processors do. If your processing needs those, you need a real stream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your team's operational maturity doesn't include filesystem operations.&lt;/strong&gt; Shared filesystems have failure modes. Stale NFS handles, mount drift, permission issues, capacity-planning surprises. If your team has historically only operated stateless services, they will be surprised by the first NFS-related incident. That's a fixable gap, but it's a gap.&lt;/p&gt;

&lt;p&gt;For the broad middle — large volumes, in-VPC consumers, batched processing, cost-sensitive ingest — this pattern is the right answer. The decisive factor is usually the cost math. Once you see your own version of the chart above, the choice tends to make itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;There's a recurring blind spot in cloud architecture that I've watched cost teams six- and seven-figure sums over the past few years: treating every data-in-motion problem as a streaming-bus problem. The streaming bus is a wonderful tool when its semantics — per-event delivery, low-latency push, multi-subscriber fan-out — actually match your workload. It is a remarkably expensive tool when your workload is "land bytes here, pick them up over there, then forget them."&lt;/p&gt;

&lt;p&gt;Transitional data is that second shape. It deserves a different tool. A shared filesystem with sub-millisecond latency, multi-AZ durability, native compression, atomic rename semantics, and now an intelligent-tiering storage class that automatically migrates cooling data to cheap storage — that's the tool. FSx for OpenZFS is the specific implementation that happens to package all of those properties together at AWS today, but the pattern works against any filesystem with the same properties (in another decade, on another cloud, it'll be something else).&lt;/p&gt;

&lt;p&gt;The architecture is small. Producers do an atomic write to FSx and publish a tiny pointer message. Consumers read the pointer and stream the bytes from a mount. The bus carries metadata, not payload. The cost shifts from "scale with byte volume" to "scale with stored volume," and the storage class itself takes care of the cooling story.&lt;/p&gt;

&lt;p&gt;The code that implements this well — backpressure by bytes not requests, safe write protocol with temp+rename, idempotent retry without re-reads, careful timeout/buffer-lifetime bookkeeping — is small enough to fit in one engineer's head and robust enough to run for years untouched. The hard part was never the protocol. The hard part was unlearning the reflex that says "data in motion = put it on a stream."&lt;/p&gt;

&lt;p&gt;If you take one thing from this, take this: put a calendar reminder for next week to look at your own AWS bill, find the line item with the highest cost-per-byte-of-useful-life, and ask yourself whether the bytes are actually getting their money's worth from the transport they're on. The answer might surprise you. The fix is usually simpler than the architecture diagram makes it look.&lt;/p&gt;

&lt;p&gt;Steal the pattern. It works.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>java</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
