Shipping AI Agents Like A Pro

Your agent works on your laptop. It plans a beautiful 4-day hiking trip with a fancy dinner, stays under budget, and nails the itinerary. You hit enter, lean back, and feel like a wizard.

Now ship it to 10,000 users. …Still confident?

This is the final post in the series, and it’s the one that ties everything together. We’ve spent four posts building up the pieces — failure modes, Agentic RAG, MCP, design patterns — and now we’re going to talk about actually shipping this thing. Because the gap between a demo and production isn’t features or model size. It’s engineering discipline.

Most agents ship without idempotency, validation, budgets, or tracing. They work in the happy path and crumble everywhere else. Cool demos need hardening. Let’s harden.

The Reference Architecture: Putting It All Together

Before we get to the checklist, let’s zoom out and look at the full picture. Here’s a reference architecture for a production-grade multi-agent system — the kind of thing our travel-planning agent would actually run on.

Reference architecture for a production-grade multi-agent system

Let’s break this down.

The Central Orchestrator is the brain. When a user request comes in, it kicks off the process — invokes the Router to classify the request, dispatches work to specialist agents, and manages the baton-passing between them until a final result is ready. You can implement this with frameworks like LlamaIndex, LangChain, Semantic Kernel, or plain custom code. The framework doesn’t matter as much as the discipline.

MCP Servers sit on the right side. Each tool — flight search, weather API, hotel booking, database, restaurant lookup — runs as its own service. They can be written in different languages, maintained by different teams, deployed independently. They all speak the MCP protocol, so the orchestrator talks to them in a consistent way. We covered this in depth in Post 3.

The Observability Layer is integrated throughout. Every agent step, every tool call, every decision gets logged, traced, and measured. Traces show the end-to-end path of each request. When something breaks at step 7 of 12, you don’t guess — you look at the trace.

All four design patterns from Post 4 — router, specialists, PES, supervisor — slot into this architecture without coupling. They’re conceptual building blocks, not framework-specific features.

Reference: The Azure AI Travel Agents sample implements many of these ideas. It’s a great starting point — just remember it’s a demo, not production-ready as-is.

The Production Checklist

Here’s the checklist. Every item maps back to a failure mode we discussed in Post 1. Nothing here is theoretical — these are the things that catch fire when you skip them.

1. Idempotent Tools and Retries

Make your tools handle being called multiple times without side effects. This is non-negotiable.

In our travel agent, imagine the flight booking tool gets called, the API responds, but the network drops the response. The agent doesn’t know if it worked. It retries. If your tool isn’t idempotent, congratulations — you just booked two flights to Patagonia.

Idempotent tools fix this. The tool recognizes the duplicate request (via a request ID, a deduplication key, or a check-before-write pattern) and returns the existing result instead of creating a new one.

Pair this with retries and exponential backoff. When a tool call fails or times out, don’t just give up — retry with increasing delays. This alone dramatically improves reliability across multi-step workflows by handling the transient errors that are inevitable in any distributed system.

2. Schema Validation and Budgets

Use clear schemas for the data moving between steps and tools. Before calling a tool, validate that the required information is present and correctly formatted.

For our travel agent, before booking a flight, check:

  • Do we have confirmed dates?
  • Do we have a destination?
  • Does it fit the budget?
  • If any of these are missing — stop and get that info first.

This is the validation loop from Post 2 in action. Don’t let the agent barrel forward with incomplete data.

Then set budgets — hard limits on:

  • Maximum number of steps
  • Maximum tokens consumed
  • Maximum execution time
  • Maximum number of tool calls

Budgets prevent runaway agents. They stop the infinite loops. They stop the token burn. They’re the guardrails that keep your agent from driving off a cliff while cheerfully telling you about restaurant options.

3. Full Workflow Tracing

Instrument your agent and tool calls for end-to-end tracing. One user request generates many internal steps. You need to see all of them.

Here’s what a single request trace looks like for our travel agent:

End-to-end trace of a travel agent request

When something fails at step 7, you dive into the trace and see exactly where and why. Was it the tool that timed out? Did the specialist get bad data? Did validation catch something it shouldn’t have?

Use OpenTelemetry for tracing and metrics. Instrument per-node and per-tool. Make traces a first-class part of your system, not an afterthought.

4. Production-Ready Systems Mindset

This isn’t a checklist item you can tick off — it’s the mindset that makes all the other items happen. Treat your agent as a secure, testable, monitorable component with clear interfaces. Not a magic prompt.

Each checklist item maps directly to a failure mode:

Checklist Item Failure Mode It Addresses
Schema validation & budgets State drift, runaway agents
Timeouts & retries API timeouts, partial failures
Idempotent tools Double-execution, inconsistent state
Full tracing Debugging blind spots, compounding errors
Budget limits Token burn, infinite loops

Systematically checking these items ensures your agent is reliable, not just smart.

The Quick-Reference Checklist

Here’s the scannable version. Print it. Pin it. Tape it to your monitor.

# Item Why It Matters How to Implement
1 Idempotent tools Prevents double-execution on retries Request IDs, deduplication keys, check-before-write
2 Retries with backoff Handles transient failures gracefully Exponential backoff, jitter, max-retry limits
3 Schema validation Catches bad data before it propagates JSON Schema, Pydantic, Zod — validate at every boundary
4 Budget limits Stops runaway agents Cap steps, tokens, time, and tool calls
5 End-to-end tracing Makes debugging possible OpenTelemetry, correlation IDs across all steps
6 Per-tool telemetry Pinpoints slow or failing tools Latency histograms, error rates, call counts
7 Graceful degradation Keeps partial results useful Fallback strategies, partial-response handling
8 Supervisor loop Prevents infinite loops and drift Explicit termination conditions, step counters

Top 3 Takeaways

If you remember nothing else from this entire series, remember these three things:

  1. Schema validation between steps. Don’t let the agent run with missing or malformed data. Validate at every boundary.

  2. Timeouts and retries for tool calls — and make your tools idempotent so retries are safe.

  3. Trace everything. When your agent fails at step 7 of 12, you need to see the full path, not just the final error.

These three practices turn an AI agent from a fragile demo into a solid system.

Series Wrap-Up

We’ve covered a lot of ground across five posts. Here’s the arc:

  1. Why AI Agents Fail in Production — The compounding error problem, the reliability tax, and why 95% per-step accuracy gives you 60% end-to-end success over 10 steps.

  2. Agentic RAG: Smarter Retrieval for Smarter Agents — Moving from “always retrieve everything” to conditional, intentional retrieval with validation loops.

  3. MCP: Treating Tools as Contracts — Separating tool implementation from agent reasoning, enabling multi-team development and independent scaling.

  4. 4 Design Patterns That Make AI Agents Actually Reliable — Router, Specialist Agents, Plan-Execute-Summarize, and the Supervisor Loop.

  5. The Production Checklist ← You are here — Idempotency, validation, budgets, tracing, and the systems mindset that ties it all together.

The core message across all five posts is simple:

A good agent is a system, not a prompt.

The mental model that should stick with you: tool calls are distributed system calls. They fail, they time out, and they return partial results — just like any backend service. Once you internalize that, everything else follows naturally. You add retries because calls fail. You add validation because data gets corrupted. You add tracing because you can’t debug what you can’t see. You add budgets because distributed systems can run away.

That’s it. That’s the whole series.

Thanks for sticking with me through all five posts. I hope these ideas save you some production incidents — or at least help you fix them faster when they inevitably happen.

Now go build agents that don’t catch fire.

What’s on your production checklist that I missed? Share your thoughts in the comments below!

Welcome to the Slop KPI Era: How Tokenmaxxing Is Making AI Worse

On a recent episode of the All-In Podcast, Jensen Huang, the CEO of NVIDIA—a company with a market cap equivalent to that of 1.5 Frances, 4 Polands, or 12 Finlands—said this: “If that $500,000 engineer did not consume at least $250,000 worth of tokens, I am going to be deeply alarmed.”

At Meta, a smaller company worth just 4 Finlands, engineers have begun the process of “Tokenmaxxing.” They’ve even started a leaderboard to see who’s burning the most tokens. This is because it is unequivocally true that the more tokens you use, the more productive you’re being.

For this same reason, it is widely recognized by connoisseurs that Everclear Neutral Grain Alcohol—a digestif that comes in at 95% ABV and also works well as a hospital-grade antiseptic—is the superior alcoholic beverage. By comparison, a bottle of 1937 Domaine de la Romanee-Conti, a red wine that survived 89 years, including all of World War II, in a temperature-controlled cave in Southern France, comes in at just 13% ABV. That makes the Everclear over 7 times better than the chateau whatever-it’s-called.

Linus Torvalds, a man from Finland who also made something called “Linux,” may disagree that more equals better. In a conversation that we found on YouTube, he had this to say about those who measure the quality of their engineers by the number of lines of code (LoC) they’ve written: “Anybody who thinks that’s a valid metric is too stupid to work at a tech company.” Perhaps the Finnish man has a point.

Imagine you’re an engineer at Meta. Does that mean you can just log into Claude Code, fire up the extended-thinking Opus model, and use it to transcribe Tolstoy’s War and Peace in Hellenic Greek 80,000 times? Would that get you 61.6 billion tokens higher up the leaderboard than, say, the engineer in the next cubicle over—who actually paused to think about what they were doing that day?

If our industry is moving toward slop KPIs that reward consumption first, then who’s the winner? Is it the Tolstoy fan or the engineer who paused to think? Well, if the goal is to burn half of a Silicon Valley salary on tokens, guess which one’s getting the promotion.

The shift is clear: token consumption is now being measured as a proxy for productivity. But who’s measuring the quality of what those tokens actually produced? We need a metric for that.

Introducing the Slop Index: The Evaluative Layer for the Tokenmaxxing Era

Here’s how the Slop Index works:

  1. Acquire a human being. Instead of using tokens, the Slop Index requires ‘neurons’, which can only be found in the brain of a human being. The human being can combine its neurons into thoughts, which can then be used to complete a surprising number of tasks.

  2. Prompt the human being. “Use your thoughts to determine whether the output of this AI model is slop or not. Is it useful? Is it stupid? Is it hallucinating? Is it solving a problem? If so, did that problem ever even exist?” And so on.

  3. Receive the verdict. The human being then returns an evaluative output in the form of thoughts, determining: is it slop or not?

*A human being is a hardware requirement for the Slop Index.

Context Bloat: The Illogical Conclusion to Tokenmaxxing

Slop KPIs and Tokenmaxxing have ushered in a strange new reality for developers, who are now rewarded for consumption rather than the quality of their output exclusively. This industrial-scale waste campaign is even more absurd when you consider that most current AI workflows, by default, are already burning way more tokens than they need to.

When AI models connect to outside systems (a process called “tool calling”) they are often loaded up with giant bundles of instructions before they have even started the real task. Tell a model to connect directly to the Salesforce MCP server, for example, and it might inherit 50-plus of these tools, complete with detailed instructions on what they are and when to use them—even when most of them are irrelevant to the task at hand. It’s like if, every time someone spoke to you, they began with: “Hi, my name is X, and I am going to say something right now. Also, I know how to talk, chew, swim, move my fingers, wiggle my toes, lift my left arm, lift my right leg…” This is what the industry calls context bloat. And while it pays dividends for the tokenmaxxer, it doesn’t bode well for the quality of an AI model’s output.

Even Anthropic, which literally makes its money on tokens, had this to say in a 2025 report: “Context must be treated as a finite resource with diminishing marginal returns” and “As the number of tokens in the context window increases, the model’s ability to accurately recall information from that context decreases.”

So, what we’re left with isn’t just a competition to performatively burn as many tokens as possible, but a race to the bottom in terms of the quality of what we’re generating. And we’ll be stuck in this twilight zone until the industry shifts to measuring output quality over consumption.

But, for now, the Slop Index may be a good place to start.

Goose by Block and Prudential run on Port of Context. Code: github.com/portofcontext/pctx · Docs: docs.portofcontext.com

C# Networking Deep Dive with io_uring part 4 – Zero Copy Receive

In this part 4 we are exploring a “side quest”, io_uring zero copy receive mechanism, even though I’ll still add the scaffold for the code, this will be more of a theoretical part as I don’t have a network card that supports this so can’t test. Future parts will not use zero copy receive mechanism.

Feel free to skip this part as it isn’t required or will impact further ones.

We usually see a lot of “hot path zero allocation super ultra fast socket server” here and there when people advertise their projects or frameworks. Well that’s cute, pre allocate some memory and reuse it, but how about zero copy?

When we receive external data via network through a network interface card (NIC) typically the NIC uses DMA to write this data to kernel memory, which the kernel then copies to a memory space our apps can access. This copy is what can be avoided by having the NIC use DMA to write the data bytes directly into user space accessible memory instead of kernel memory.

What is DMA?

Direct Memory Access is a hardware capability that lets a device read or write to RAM “on its own” with no interaction of the CPU. Without DMA, the only way to get data from a device into memory is through the CPU reading from the device’s register and copying it to RAM, the CPU is busy during the copy process, for a NIC pushing gigabits/sec this would consume the whole core.

For our case (NIC) the driver (CPU) pre populates a ring of Rx descriptors, each pointing at a RAM buffer. When a packet arrives off the wire, the NIC’s DMA writes the packet bytes straight into the next buffer in that ring, flags the descriptor done and interrupts the CPU via NAPI. The CPU never copied thee packet, the NIC placed it in RAM itself.

DMAs target physical addresses, not the virtual addresses our app/program sees. This address translation/pinning is why io_uring zcrx (zero copy receive) has to register our memory with the kernel first.

DMA is present on every normal network receive, the NIC DMAs the packet into kernel RAM buffers. The io_uring zcrx idea is to change the descriptor’s target address so that the DMA lands in our registered memory instead, avoiding the extra kernel copy.

So, what changed?

In previous parts our recv path used an io_uring provided buffer ring, we allocated a big slab, sliced it into buffers and handed it to the kernel. When data arrived the kernel picked a buffer and copied the data bytes into it. We want to remove that copying having the NIC DMA the received bytes directly to our buffers.

To avoid making this part too extensive I’ll focus on the main changes.

Let’s do an high level comparison between Minima (parts 1-3) and MinimaZero (part 4 with zcrx).

Who fills the buffer?
Minima – Kernel memcpys into our slab.
MinimaZero – NIC DMAs into our registered area.

This is the pivotal difference, everything that follows exists to support it. In parts 1-3 the kernel receives the packet into its own memory and memcpys the payload to one of our pre registered slab buffers. With zero copy rx the NIC DMAs the payload straight into a memory area we register, avoiding the the kernel copy.

What we register?
Minima – Provided buffer ring PBUF_RING.
MinimaZero – zcrx ifq bound to NIC Rx queue (ZCRX_IFQ).

Parts 1-3 register a provided buffer ring with IORING_REGISTER_PBUF_RING, a pool of our own memory the kernel can copy into. In part 4 we register a zcrx interface queue with IORING_REEGISTER_ZCRX_IFQ which binds our memory area to one specific NIC hardware receive queue, “wiring” our memory with the NIC’s DMA path.

Recv operation (to be multishotted)
Minima – RECV + IOSQE_BUFFER_SELECT
MinimaZero – RECV_ZC multishot, no buffer selection

Parts 1-3 use IORING_OP_RECV with IOSQE_BUFFER_SELECT flag which basically tells the kernel to pick a buffer from the provided ring only when data arrives, this makes idle connections cheap. In part 4 we use IORING_OP_RECV_ZC without buffer selection as the destination was set when the ifq is registered. Both work with multishot.

Completion
Minima – 16-byte CQE, buffer id in flags
MinimaZero – 32-byte CQE, CQE32 plus trailing zcrx_cqe

In part 4 the 16 bytes are not enough as the kernel needs to include information of where inside our area the NIC wrote.

Locating the data
Minima – slab + bid*size
MinimaZero – area + (off & ~AREA_MASK) from the token

Each completion points to the bytes, parts 1-3 own a fixed numbered slots so it’s simple arithmetic, slab pointer plus buffer id times buffer size. In part 4 we don’t own numbered slots, the NIC picks where to write in the provided area.

Returning a buffer
Minima – ReturnBuffer
MinimaZero – refill queue entry RefillRqe

Similar lifecycle for both, buffers must be “handed back” so that they can be used again. What changed is that in parts 1-3 we return the buffer id to the provided buffer ring with ReturnBuffer, in part 4 we post an entry to the RefillRqe, this entry is basically a descriptor in the DMA area.

Concurrency
Minima – N reactors
MinimaZero – 1 reactor, 1 ifq, one HW queue

Concurrency changed a lot and since I cannot test zcrx due to not having a NIC that supports it, I could not understand or optimize what is the best way to set this up.

In parts 1-3 we run N reactors, each has its own ring and buffer pool, using SO_REUSEPORT the kernel spreads incoming connetions across the reactors. zcrx breaks that, the ifq binds to one hardware receive queue and is steered by the NIC’s flow, this basically means that a connection and its zero copy bytes can end up on different threads which breaks the multi reactor architecture where connections are owned by reactors that never thread hop. While it is still possible to have multi reactor patter with zcrx by having multiple ifq, I could not test it so won’t cover it.

Host setup
Minima – None
MinimaZero – ethtool split + steering, NIC + kernel >= 6.15

Now the code

I decided to not include any code for this part as I cannot test it.

You can find my scaffolding here
It is a port to C# from an existing C implementation plus some “theoretical” changes I cannot test, might be useful in the future if I managed to get my hands on a NIC that supports this.

Sources:

https://docs.kernel.org/networking/iou-zcrx.html
https://www.youtube.com/watch?v=LCuTSNDe1nQ
https://www.youtube.com/watch?v=WQ22zAPBSnQ
https://github.com/torvalds/linux/blob/master/io_uring/zcrx.c

Recovering Corrupted ROS 2 Bags Without a ROS 2 Runtime

Robots do not always fail gracefully.

A delivery robot can hit an obstacle. A lawn-mowing robot can lose power. An inspection robot can emergency-stop in the middle of a mission. In many of these cases, the most valuable data is the last few seconds before the incident: camera frames, IMU samples, odometry, localization, control commands, and diagnostics.

For ROS 2 systems, that data is often stored in rosbag2.

But there is a painful failure mode: if the process exits abnormally or the device loses power, the underlying MCAP or SQLite3 file may not be closed cleanly. The data file can be missing a footer, summary, index, WAL checkpoint, or part of the final page. Official tools may refuse to open it, even though many complete messages are still inside.

That is the problem I started working on with LibRobotBagFix.

GitHub: https://github.com/Adiao1973/LibRobotBagFix

What is LibRobotBagFix?

LibRobotBagFix is an open-source C++ SDK and command-line tool for inspecting, repairing, and reading ROS 2 rosbag2 black-box data without requiring a ROS 2 runtime.

The project focuses on commercial robot field diagnostics:

  • autonomous delivery vehicles
  • lawn-mowing robots
  • inspection robots
  • mobile robot fleets
  • robotics support tools
  • accident analysis workflows

The goal is not to replace ROS 2 or implement a full rosbag player. The goal is more focused:

Recover as many complete messages as possible from damaged MCAP or SQLite3 rosbag2 files, then make the result readable again.

Why this matters

In real robot incidents, the most important data is usually near the end of the recording.

Unfortunately, the end of the file is also the part most likely to be damaged during power loss or a crash.

Common symptoms include:

  • ros2 bag info cannot open the bag
  • an MCAP file is missing its summary or footer
  • a SQLite3 .db3 file has uncheckpointed WAL data
  • the last SQLite page is partially written
  • the file contains complete earlier messages, but normal tools cannot reach them

For field teams, there is another practical issue: they may not have a full ROS 2 environment available. They may be using macOS, Windows, a tablet, or a lightweight diagnostic tool.

That is why LibRobotBagFix is designed as a small embeddable SDK with a CLI, not as a ROS 2 node.

Current features

The CLI currently provides two main commands.

Inspect a bag directory or single data file:

./build/robotbagfix inspect path/to/bag_or_file --json

Repair a damaged MCAP file:

./build/robotbagfix repair damaged.mcap -o repaired.mcap --json

Repair a damaged SQLite3 rosbag2 database:

./build/robotbagfix repair damaged.db3 -o repaired.db3 --json

The project currently supports:

  • rosbag2 directory detection
  • MCAP detection and record scanning
  • SQLite3 .db3 detection
  • SQLite WAL sidecar discovery
  • MCAP tail repair
  • SQLite fast-path recovery
  • SQLite page-level salvage fallback
  • C++ SDK message iteration
  • stable C ABI for FFI
  • lightweight CDR helpers
  • optional Qt desktop demo

How MCAP repair works

MCAP has a structured binary container format. A valid file includes magic bytes, a header, a data section, optional summary sections, a footer, and trailing magic bytes.

A common corruption pattern is tail truncation. For example, the file may contain many complete records, but the final summary or footer was never written.

LibRobotBagFix scans the MCAP file sequentially, keeps complete records, discards the partial tail record if one exists, and rebuilds the required trailing structure.

The simplified idea looks like this:

damaged.mcap
  -> scan complete records
  -> stop at the first incomplete record
  -> discard damaged tail bytes
  -> rebuild Data End / Summary / Footer
  -> write repaired.mcap

The source file is never modified in place.

How SQLite3 repair works

The SQLite3 backend is more complex because rosbag2 data is stored inside database tables.

The first strategy is to let SQLite do what SQLite is good at:

  • open the database safely
  • apply WAL recovery when possible
  • run integrity checks
  • export complete rows
  • rebuild a clean .db3

If that fails, LibRobotBagFix falls back to page-level salvage. It scans SQLite pages, tries to recover sqlite_schema, locates rosbag2 tables such as topics and messages, and writes only rows that can be proven complete.

Uncertain data is reported, not silently written into the output database.

Design principles

A few principles guide the project:

No ROS 2 runtime dependency

The SDK should be usable in diagnostic tools, desktop applications, mobile apps, and support workflows without installing a full ROS 2 environment.

Repair by container, not by sensor

At the repair layer, an image, IMU sample, odometry message, and point cloud are all payload bytes inside MCAP or SQLite3.

The repair engine focuses on the container format. Sensor-specific interpretation belongs in a higher-level parser.

Do not keep half-written messages

If a message payload itself is incomplete, LibRobotBagFix does not pretend it is valid. It keeps complete messages and reports discarded bytes, records, pages, or uncertain rows.

Keep the SDK embeddable

The project provides a C++ API and a stable C ABI. That makes it easier to integrate with tools written in Swift, Kotlin/JNI, C#, Python FFI, Qt, or other application layers.

Quick start

Build the default SDK and CLI:

git clone https://github.com/Adiao1973/LibRobotBagFix.git
cd LibRobotBagFix
scripts/build.sh

Check the version:

./build/robotbagfix --version

Run tests:

ctest --test-dir build --output-on-failure

Inspect a file:

./build/robotbagfix inspect path/to/bag_or_file --json

Repair a file:

./build/robotbagfix repair damaged.mcap -o repaired.mcap --json

Read repaired output through the SDK examples:

./build/rbf_read_bag_cpp repaired.db3
./build/rbf_read_bag_c repaired.db3

Who this is for

This project may be useful if you work on:

  • robotics incident analysis
  • autonomous robot field support
  • ROS 2 data tooling
  • fleet diagnostics
  • lightweight robot data viewers
  • C++ SDKs for robotics products
  • mobile or desktop tools that need to read robot black-box data

It is especially relevant when you need to inspect or recover robot data outside a full ROS 2 workstation.

Current boundaries

LibRobotBagFix is still focused. It does not try to do everything.

Current non-goals include:

  • modifying damaged bags in place
  • recovering half-written image or point-cloud payloads
  • implementing a full rosbag player
  • publishing or subscribing to DDS topics
  • making ROS 2 a runtime dependency
  • making Qt or mobile UI frameworks part of the core SDK

What I am looking for

This project is open source, and I would appreciate feedback from people who work with ROS 2 bags in real systems.

Useful feedback includes:

  • damaged bag patterns you have seen in production
  • MCAP or SQLite3 edge cases
  • cross-platform build feedback
  • SDK API suggestions
  • ideas for minimal fixtures and regression tests
  • field diagnostic workflows where this could help

GitHub repo:

https://github.com/Adiao1973/LibRobotBagFix

If you work with ROS 2 robot logs and have ever lost the last seconds of data after a crash or power failure, I would be interested to hear what failure modes you have seen.

The Promotion Happened. The Job Did Not.

Orgs often mistake meeting voice for delivery leverage … confidence without multiplication. This piece is the mirror. What happens when someone earns the title and the system does not update around them? The promotion happened. The job did not. That gap is where resentment grows quietly.

On the other side, I have also lived the better version of this story.

When an engineer asked what it would take to reach the next level, the honest answer was not want it more. It was partnership on a real map … behaviors, scope, evidence. We built clarity with HR, wrote what each level meant, and tracked progress like adults. Within a year he moved to senior. Within two and a half years he replaced me. That is what promotion support is supposed to feel like when the system works.

So I am not cynical about growth. I am cynical about growth theater.

Here is what the job did not usually means in practice.

Same scope, new title. More pressure, no new authority, no new tools, no real sponsorship.

Ambiguous expectations. Leadership says senior now, but the team still treats them like yesterday’s version.

No air cover. They get pulled into every emergency because they are now one of the strong ones, which is not a job description. It is extraction.

None of that requires a villain. It is what happens when calendars are full and nobody owns the transition.

If you promote someone and do not change what they are allowed to own, you did HR paperwork, not leadership work.

The org side receipt I keep returning to

The org side receipt is the same headline as the top of this page … interview voice without leverage.

I have watched a contractor shaped lead show up with strong language, strong opinions, and lead shaped signals, then fail where leverage matters … delegation, sequencing, delivery through others. A basic navigation effort can stretch for months not because the problem is impossible, but because the seat was wrong and the system rewarded the wrong signals.

That is not a personal attack on contractors as a category. It is a warning about what we optimize for when we confuse confidence for competence.

The healthy counterweight is the map we built when someone asked what next level actually meant … behaviors, scope, evidence, not vibes.

The first ninety days after leveling

If you are the manager, treat the first ninety days like a small product launch.

Rewrite scope in writing. What decisions can they make without you? What decisions still require you? Where are the edges?

Assign one real ownership domain. Not busywork. Something with consequences and visibility.

Pair them with a sponsor outside your chain when the role needs cross functional credibility, not only your private praise.

If you are the newly leveled engineer, push for the same clarity with respect and persistence. A title without a contract is a costume.

Why quiet quitting starts in the promotion gap

People do not always leave because they are unhappy.

Sometimes they leave because they finally admit the growth story was mostly theater.

The promotion was real on paper. The job was not real in practice. The cognitive dissonance is exhausting because it forces a smart person to choose between two bad stories … either leadership is incompetent, or they are not actually valued. Humans tend to pick the story that protects the ego longest and hurts the career fastest.

If you want the bus factor adjacent warning here, it is simple. Hero dependent systems punish everyone when the hero needs air. Promotions that add title without distributing ownership create a different kind of heroism … the kind where the newly titled person becomes a sponge for risk.

The HR update is not the ceremony

Here is where promotions go quietly wrong even when HR does everything right on paper.

The system updates. The title changes. Slack congratulations roll in. Then Monday shows up and the work looks identical.

If you are the manager, that is not a win. That is a trust leak.

A promotion without a scope delta is like shipping a major version bump with no breaking changes listed. It might be true occasionally. Usually it means you are not being honest about what changed.

The growth story that is not mine, but belongs in the same file

I once told a front end developer the harder truth … his growth had stalled relative to where the market was moving. Not because he was bad. Because he was good at the wrong narrow thing for too long. He did the work, learned new depth, and it helped him land his next role with confidence.

That story is not the same as a title without a job. It is the mirror image.

One is a system failing to define reality. The other is a person choosing reality after someone finally named it.

Your job as a manager is to name reality early enough that people do not have to discover it through pain.

Sponsorship is not a compliment

If you want a promotion to come with a job, you need sponsorship that shows up in rooms the engineer cannot access yet.

Not cheerleading. Air cover. Conflict navigation. Budget defense. Cross functional credibility that cannot be earned only by shipping tickets faster.

If your promotion plan stops at “you are doing great,” you are not building leaders. You are collecting talent like trading cards.

Internal mobility is still promotion work

Sometimes the best promotion is not a new title at all. It is a move that gives a strong engineer a new hill to climb before they flatline.

I have said yes fast when a critical engineer asked to move, even when my first reaction was an oof in my gut. The team survived because context was distributed and two engineers stepped into the space instead of pretending the gap was invisible. She stayed years longer because someone treated growth as real, not as a retention threat.

That is promotion quality too. It is just wider than HR paperwork.

The first twelve weeks are not decoration

If you want a practical shape for the post promotion window, think in weeks, not vibes.

Early weeks are where you renegotiate scope in writing without making the person feel punished for being promoted. Middle weeks are where you intentionally create a visible win that belongs to them, not to you, so the org updates its mental model. Later weeks are where you tighten feedback because the title changed the power dynamic even if nobody admits it.

If you cannot describe what should be harder and what should be easier after twelve weeks, you did not promote someone into a new job. You promoted them into a new stress profile.

The confession

I have promoted optimism before I promoted clarity.

Not on purpose. Not with bad intent. Because optimism feels kind in the moment and clarity feels like you are picking a fight with someone’s dream.

Clarity is the kind part long term.

The identity threat

If you are a manager who thinks a promotion conversation ends when HR updates the system, you are not developing people.

You are updating a database.

Titles are cheap. Scope is expensive. Pay the bill on purpose.

This week

If you recently promoted someone, send them a one page note … new ownership boundaries, first deliverable, and how you will measure success in thirty days.

If you were recently promoted, ask for that same page.

If the room goes fuzzy, you learned something valuable about whether the promotion came with a job.

If you do not like what you learned, do not update your resume first.

Update the contract.

One email a week from The Builder’s Leader. The frameworks, the blind spots, and the conversations most leaders avoid. Subscribe for free.

Open Source Email Server Setup: The 2026 Infrastructure Guide

The Definitive Guide to Self-Hosted Email Infrastructure in 2026

Why would anyone self-host email in 2026? AWS SES, SendGrid, Mailgun, and Postmark exist. They’re cheap, reliable, and require zero maintenance.

And yet — serious senders are moving back to self-hosted. Here’s why, and how to do it right.

The Hidden Cost of Cloud Email Services

Cloud SMTP services charge by the email, not by the infrastructure. For transactional email at scale, this adds up fast:

Provider Volume Monthly Cost
SendGrid 100K emails ~$89
SendGrid 1M emails ~$499
Mailgun 50K emails ~$50
AWS SES 1M emails ~$100

But the real cost isn’t the per-email fee. It’s the 30-70% revenue markup on your email program when you hit serious volume. A sender doing 10M/month emails pays $500-1000 on SES vs $2500-5000 on SendGrid.

More importantly: you don’t own your infrastructure. When AWS has an SES outage (it happens), your transactional emails stop. When SendGrid gets flagged by Gmail (it happens), you have no control over remediation.

Why Open Source MTAs Are Having a Moment

The open source MTA landscape has matured dramatically:

KumoMTA — The modern successor to PowerMTA. Built by the team that created PowerMTA. Written in Rust for performance and memory safety. Full commercial support available. Handles 10M+ messages/hour on commodity hardware.

Postfix — The de facto standard for Linux mail servers. Ubiquitous, rock-stable, well-documented. Best as a mail transfer agent (receiving/proxy) rather than a high-volume sender.

Exim — Flexible and powerful, but increasingly showing its age. Still popular in UK hosting environments.

Haraka — High-performance SMTP server written in Node.js. Excellent plugin architecture. Scales well but complex to operate at very high volume.

KumoMTA vs Postfix: When to Use Each

Use KumoMTA when:

  • You’re sending transactional email at scale (100K+/day)
  • You need granular bounce handling and routing logic
  • You want detailed per-message logging and analytics
  • You need commercial support with SLA guarantees

Use Postfix when:

  • You’re handling inbound mail routing
  • You need a mail relay/proxy in front of a cloud service
  • Simplicity and ubiquity matter more than raw performance
  • You’re already running Postfix and just need basic relay

Hardware Requirements for Self-Hosted Email

One of the biggest misconceptions: you need expensive hardware. You don’t.

KumoMTA benchmarks on commodity cloud hardware:

Instance vCPUs RAM Emails/Hour
AWS t3.medium 2 4GB ~500K
AWS t3.large 2 8GB ~1.2M
AWS c5.xlarge 4 8GB ~3M
AWS c5.2xlarge 8 16GB ~8M

For most applications, a single t3.large ($60/month) handles 1-2M emails/day comfortably.

Critical: Email deliverability is IP-based. Separate your transactional email infrastructure from your web servers. Never send from the same IPs that run your web application.

Network Architecture for Self-Hosted Email

[Application Servers]
       ↓ SMTP
[KumoMTA - Outbound MTA] ← Your IP reputation lives here
       ↓
[接收服务器 - Gmail, Outlook, etc.]

Key architectural decisions:

Dedicated IPs: Use 1 IP per 50K-100K daily volume. Mix warm IPs with cold ones so a new IP warming doesn’t affect your main traffic.

InboundMX servers: Separate servers to receive bounces, FBL notifications, and inbound mail. Don’t mix inbound and outbound on the same infrastructure.

Analytics relay: A light SMTP relay that copies headers to your logging system before forwarding. Useful for debugging delivery issues without slowing down main send path.

DNS Configuration: Your Sender Identity

This is where most teams fail. Your DNS is your sender identity. Configure it before you send a single email.

The Authentication Trinity

SPF: Authorize your sending servers:

v=spf1 include:_spf.postmta.com include:_spf.google.com ~all

DKIM: Sign every message with your private key:

selector._domainkey.postmta.com IN TXT (
  "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..."
)

DMARC: Tie it together and get reports:

_dmarc.postmta.com IN TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100"

Reverse DNS (PTR Records)

Your sending IP must resolve to your domain. Check:

$ host 162.222.226.207
207.226.222.162.in-addr.arpa domain pointer postmta.com.

If PTR doesn’t match your HELO hostname, Gmail and Microsoft will start rejecting your mail.

MX and SPF for Bounce/Feedback Domains

Use separate domains for bounce handling:

  • Return-Path: via bounce.postmta.com
  • SPF: v=spf1 include:_spf.postmta.com ~all
  • DMARC on postmta.com, separate DMARC on bounce.postmta.com

This lets you track bounce rates by domain and isolate reputation problems.

Setting Up KumoMTA: Step by Step

# Install on Ubuntu 22.04
curl -L https://github.com/KumoCorp/kumomta/releases/latest/download/kumomta_latest_amd64.deb -o kumomta.deb
sudo dpkg -i kumomta.deb

# Configure
sudo nano /etc/kumomta/kumomta.conf

Basic sending configuration:

# /etc/kumomta/kumomta.conf

# Outbound queue settings
remote_queue "outbound" {
  concurrency = 500
  rate = "10000/hour"  # Adjust for IP warming stage
}

# DKIM signing
dkim {
  sign = true
  selector = "mail"
  domain = "postmta.com"
  private_key = "/etc/kumomta/dkim/mail.private"
}

# Bounce handling
bounce {
  use_8bitmime = false
  log_bounces = true
  log_path = "/var/log/kumomta/bounces.log"
}

Send your first test:

echo "From: [email protected]
To: [email protected]
Subject: Test

Test message body" | /usr/bin/kumomta-send 
  --server 127.0.0.1:25 
  --from [email protected] 
  --to test@gmail.com

IP Warming: The Right Way

New sending IPs take 4-6 weeks to build reputation. Rushing this is the most common mistake.

Week 1: 100 emails/day to your most engaged recipients only
Week 2: 500 emails/day, expand to broader list
Week 3: 2,000 emails/day
Week 4: 10,000 emails/day
Week 5: 50,000 emails/day
Week 6: Full volume

Monitor daily: If bounce rate exceeds 1%, reduce volume and investigate before continuing.

Monitoring Your Self-Hosted Email Infrastructure

Track these metrics daily:

Delivery Metrics:

  • Bounce rate (target: < 1% hard, < 3% soft)
  • Complaint rate (target: < 0.1%)
  • IP reputation score (target: 90+ on Sender Score)
  • Gmail Postmaster Tools: delivery errors, spam rate

Infrastructure Metrics:

  • Queue depth (target: < 1000 messages)
  • Delivery latency (target: < 30 seconds for transactional)
  • Connection errors
  • Disk I/O on mail server

Tools:

  • Google Postmaster Tools (free, Gmail reputation)
  • Microsoft SNDS (free, Outlook reputation)
  • Sender Score by Validity (free, overall IP score)
  • MXToolbox Blacklist Check (free, blacklist status)

Is Self-Hosted Right for You?

Yes, if:

  • You’re sending 500K+ emails/month
  • You want to own your sender reputation
  • You have engineering capacity to maintain infrastructure
  • Cost savings at scale matter to your business

Stick with cloud services if:

  • You’re under 100K emails/month
  • You have no infrastructure engineering capacity
  • You need SLA guarantees and 24/7 support
  • Your sending patterns are highly irregular

Next Steps

Ready to explore self-hosted? Start with our KumoMTA Setup Guide and IP Warmup Best Practices.

KumoMTA vs PowerMTA: The 2026 Enterprise Email Infrastructure Comparison

KumoMTA vs PowerMTA: The Definitive Guide to Enterprise Email Delivery in 2026

When your platform sends 10 million transactional emails a day, the difference between the right MTA and the wrong one costs you money, reputation, and inbox placement. Two names dominate the enterprise email delivery space: KumoMTA and PowerMTA. This guide cuts through the marketing to give you a technical comparison you can act on.

What Is KumoMTA?

KumoMTA is an open-source, Rust-based Mail Transfer Agent developed by Flying Circus / Prozesshell. Built from the ground up for modern cloud-native infrastructure, KumoMTA handles over 10 billion emails per month for enterprises worldwide.

Key characteristics:

  • Apache 2.0 open source license — no per-server fees
  • Rust-powered for memory safety and raw performance
  • AI-powered deployment assistant for automated optimization
  • Lua scripting for dynamic, flexible configuration
  • Built-in Prometheus metrics and Grafana dashboard support
  • Multi-tenant architecture with traffic shaping per tenant
  • Designed for containerized (Docker/Kubernetes) and bare-metal deployment

KumoMTA represents a greenfield approach: instead of patching legacy Sendmail/postfix architecture, it was designed from scratch for the modern internet — handling TLS 1.3, IPv6 natively, and scale-out clustering without legacy baggage.

What Is PowerMTA?

PowerMTA is a commercial MTA from Port25 (now part of Spin子) with a 15+ year track record in enterprise email delivery. It runs on Linux and is trusted by ESPs, financial institutions, and high-volume senders globally.

Key characteristics:

  • Commercial proprietary license — annual subscription per server
  • C++ core for proven performance and stability
  • Native configuration via flat-text config files (PMTA-specific syntax)
  • Multi-tenant with per-domain and per-IP traffic policies
  • Advanced bounce handling and loop detection
  • DKIM signing, SPF, and DMARC support built in
  • Virtual MTA (vMTA) architecture for multi-campaign handling

PowerMTA’s strength is its battle-tested reliability. It’s been audited, optimized, and refined over years of production use at massive scale.

Key Differences

Dimension KumoMTA PowerMTA
License Apache 2.0 (open source) Commercial (annual subscription)
Language Rust (memory safe, fast) C++ (proven, mature)
Configuration Lua scripting + YAML Native PMTA config syntax
AI Features Yes — AI deployment optimizer No
Monitoring Prometheus + Grafana (native) Built-in web admin, SNMP
Traffic Shaping Per-tenant, granular Per-IP, per-domain
Clustering Native scale-out Multi-server via shared config
Startup Support Prozesshell/Flying Circus Port25/Spinute
Min. Cost Free (self-hosted) ~$2,000/server/year
Max Volume 10B+/month 10B+/month

Licensing Cost

KumoMTA eliminates the license fee entirely. Your costs are infrastructure + support if needed. For a company sending 100M emails/month, this saves $24,000–$60,000/year in licensing alone.

PowerMTA charges per server annually. For high-volume senders, this adds up — but includes official support and validated stability.

Architecture

KumoMTA’s Rust foundation means it handles connection concurrency far more efficiently than traditional thread-per-connection models. Under load, KumoMTA typically uses 40–60% less memory than comparable PowerMTA configurations.

PowerMTA’s configuration model, while older, is extremely well-documented by the community. Every edge case has been solved and written about.

AI Deployment

KumoMTA’s AI assistant analyzes your sending patterns, domain reputation, and ISP feedback to automatically tune delivery parameters. This is a genuine differentiator — especially for teams without dedicated email delivery engineers.

Why Companies Are Switching to KumoMTA

  1. Cost elimination — No per-server licensing fee; reallocate that budget to infrastructure or engineering
  2. Modern stack — Docker and Kubernetes native; fits into GitOps workflows
  3. AI optimization — Automatic tuning without hiring a specialist
  4. Rust performance — Better handling of connection storms without memory exhaustion
  5. Open source transparency — Audit the code; no hidden behavior
  6. Multi-tenant design — Native support for running multiple clients/tenants cleanly

Who Should Use Each

Choose KumoMTA if:

  • You want to eliminate licensing costs
  • Your team runs modern cloud infrastructure (Kubernetes, Terraform)
  • You need AI-assisted optimization
  • You’re a startup or growth-stage company scaling email volume rapidly
  • You want Lua scripting flexibility for custom logic

Choose PowerMTA if:

  • You need a vendor-backed SLA with guaranteed support response times
  • Your team has years of PMTA configuration expertise
  • Your compliance framework requires a vendor-supported commercial tool
  • You’re running legacy infrastructure that would require a full migration to change

Conclusion

Both KumoMTA and PowerMTA are production-grade MTAs capable of handling enterprise email volumes. The choice comes down to your budget, infrastructure philosophy, and team skills.

If you want open source freedom, AI optimization, and modern cloud-native deployment, KumoMTA wins. If you need commercial support guarantees and a decade of battle-tested config patterns, PowerMTA is the safer bet.

Not sure which is right for your infrastructure? PostMTA provides free technical consultation for email delivery architecture. We help you evaluate, migrate, and optimize your MTA setup — whether you choose KumoMTA, PowerMTA, or both.

👉 Schedule a free 30-minute consultation →

How to Set Up KumoMTA: A Complete Guide for High-Volume Email Senders – Updated 2026 Guide

How to Set Up KumoMTA: A Complete Guide for High-Volume Email Senders

KumoMTA’s combination of open-source licensing and modern architecture makes it an attractive choice for teams ready to move beyond legacy MTA solutions. This guide walks you through a production-ready KumoMTA deployment — from first install to warm IP and monitoring.

Prerequisites

Before installing KumoMTA, ensure you have:

  • Linux server (Ubuntu 22.04+ or RHEL 9+ recommended)
  • Docker (for containerized deployment) or kubectl (for Kubernetes)
  • Domain names with DNS access for MX, SPF, DKIM, and DMARC records
  • Dedicated IP addresses (at least 2 for warmup rotation)
  • PostgreSQL or SQLite for delivery tracking (optional but recommended)
  • Prometheus + Grafana for metrics (optional but strongly recommended)
  • Root or sudo access

Installation Methods

Option 1: Docker (Recommended for Most Teams)

# Pull the official KumoMTA image
docker pull ghcr.io/prozesshell/kumomta:latest

# Create configuration directory
mkdir -p /opt/kumomta/{config,data,log}

# Start KumoMTA with basic configuration
docker run -d 
  --name kumomta 
  -p 25:25 
  -p 587:587 
  -p 465:465 
  -v /opt/kumomta/config:/etc/kumomta 
  -v /opt/kumomta/data:/var/lib/kumomta 
  -v /opt/kumomta/log:/var/log/kumomta 
  ghcr.io/prozesshell/kumomta:latest

Option 2: Kubernetes with Helm

# Add the KumoMTA Helm repository
helm repo add kumomta https://charts.kumomta.com
helm repo update

# Install with custom values
helm install kumomta kumomta/kumomta 
  --set replicaCount=3 
  --set config.mail.tls.enabled=true 
  --set resources.requests.cpu=500m 
  --set resources.requests.memory=1Gi

Basic Configuration

KumoMTA’s main configuration file lives at /etc/kumomta/kumomta.conf. Here’s a production-ready baseline:

-- KumoMTA Configuration
kumo.start_server()

-- SMTP Listener
kumo:define_smtp_listener({
  listen = '[::]:25',
  relay_hosts = { '127.0.0.1' },
  -- Allow authenticated relays
  submission = true,
})

-- DKIM Signing
kumo:define_dkim_signer({
  domain = 'yourdomain.com',
  selector = 'mail',
  key_path = '/etc/kumomta/keys/dkim.pem',
  headers = { 'From', 'To', 'Subject' },
})

-- Traffic Shaping (per tenant)
kumo:define_traffic_shaper({
  name = 'default',
  max_message_rate = 1000,  -- per second
  max_connection_rate = 100,
  max_outbound_connections = 1000,
})

-- Prometheus Metrics
kumo:define_source({
  name = 'prometheus',
  protocol = 'prometheus',
  listen = '[::]:8000',
})

-- Logging
kumo:define_log({
  path = '/var/log/kumomta/smtp.log',
  level = 'info',
})

After saving, validate and reload:

kumomta config validate /etc/kumomta/kumomta.conf
kumomta reload

DKIM and DMARC Setup

Generate DKIM Keys

# Generate a 2048-bit DKIM key pair
openssl genrsa -out /etc/kumomta/keys/dkim.pem 2048
openssl rsa -in /etc/kumomta/keys/dkim.pem -pubout > /etc/kumomta/keys/dkim.pub
chmod 600 /etc/kumomta/keys/dkim.pem

DNS Records

Add these records in your DNS provider:

DKIM Record (TXT record at mail._domainkey.yourdomain.com):

v=DKIM1; k=rsa; p=YOUR_PUBLIC_KEY_HERE

SPF Record (TXT at your domain root):

v=SPF1 include:_spf.yourdomain.com ~all

DMARC Record (TXT at _dmarc.yourdomain.com):

v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100

IP Warmup Strategy

Never send high volume from a cold IP. Use this rotation schedule:

Week Daily Volume Cap Notes
1 1,000 emails/day Warmup phase — monitor bounces
2 10,000 emails/day Watch complaint rates
3 50,000 emails/day Check inbox placement
4 200,000 emails/day Observe reputation
5+ Scale as reputation builds Add second IP, repeat

KumoMTA’s multi-tenant traffic shaping makes rotating warmup easy — assign each tenant a specific IP pool and let the shaping policies enforce the warmup schedule.

Monitoring with Prometheus and Grafana

KumoMTA exposes metrics at http://yourserver:8000/metrics. Add this to your Prometheus config:

scrape_configs:
  - job_name: 'kumomta'
    static_configs:
      - targets: ['your-kumomta-host:8000']

Key metrics to watch:

  • kumomta_smtp_messages_total — total messages processed
  • kumomta_smtp_delivery_latency_seconds — delivery latency histogram
  • kumomta_smtp_bounce_rate — bounce percentage by type
  • kumomta_tls_connections_total — TLS vs plaintext ratio

Import the official KumoMTA Grafana dashboard (ID: 19876) for instant visibility.

Common Pitfalls

  1. Skipping IP warmup — Cold IPs get blacklisted fast. Follow the rotation schedule strictly.
  2. Missing DKIM keys — Without DKIM, Gmail and Outlook will junk your mail.
  3. No DMARC monitoring — You won’t know you’re failing authentication until inbox placement drops.
  4. Insufficient connection limits — KumoMTA’s default limits are conservative; tune them for your volume.
  5. Ignoring bounce codes — Hard bounces damage reputation; process them within hours, not days.

Conclusion

KumoMTA’s modern architecture, Lua configuration flexibility, and AI-assisted deployment make it a powerful choice for high-volume senders ready to leave legacy MTA solutions behind.

Getting it right the first time matters — misconfigured DKIM, inadequate warmup, or missing monitoring will cost you inbox placement that takes months to rebuild.

Need a production-ready KumoMTA deployment without the guesswork? PostMTA’s engineering team specializes in KumoMTA setup, IP warmup, and deliverability optimization. We’ll have you sending at full volume within weeks, not months.

👉 Get a free KumoMTA setup consultation →