<?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: Allen Helton</title>
    <description>The latest articles on DEV Community by Allen Helton (@allenheltondev).</description>
    <link>https://dev.to/allenheltondev</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F153157%2Fd03699fc-2a88-4555-97ee-f2c325294133.jpg</url>
      <title>DEV Community: Allen Helton</title>
      <link>https://dev.to/allenheltondev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/allenheltondev"/>
    <language>en</language>
    <item>
      <title>Give your agent an agenda</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:13:52 +0000</pubDate>
      <link>https://dev.to/aws-heroes/give-your-agent-an-agenda-1icf</link>
      <guid>https://dev.to/aws-heroes/give-your-agent-an-agenda-1icf</guid>
      <description>&lt;p&gt;I'm gonna go out on a limb and say you've probably used an AI agent before. I'm gonna go out on an even further limb and say you've probably &lt;em&gt;built&lt;/em&gt; an AI agent before.&lt;/p&gt;

&lt;p&gt;When building agents that interact with humans, we often put a decent amount of effort into designing a character profile for them. Things like a name, tone, opinions, funny sayings to tack on to the end of sentences, all go into building the facade of a personality. Memory is in the list too, so the agent knows what happened yesterday.&lt;/p&gt;

&lt;p&gt;There's a surprising amount to all of it, including the science that goes into &lt;a href="https://readysetcloud.io/blog/allen.helton/agent-memory-needs-more-than-vector-search" rel="noopener noreferrer"&gt;which memories to retrieve&lt;/a&gt;, but when it comes down to it, a lot of it is just messing around with a prompt. And every piece of it describes who the agent is or what already happened to it. Memory gives an agent a past. Very little gives it a future.&lt;/p&gt;

&lt;p&gt;Prompt engineering is important, but it's soooo early 2026. It's time we start taking it a step further.&lt;/p&gt;

&lt;p&gt;With the rise of always-on personal AI assistants, giving an AI agent a sense of agency (the ability to make its own choices within the rules of its world) is becoming a highly desirable quality. And it involves a lot more than configuring a personality in a prompt. 😄&lt;/p&gt;

&lt;p&gt;Personality answers the question "&lt;em&gt;How should this character sound?&lt;/em&gt;" Memory answers "&lt;em&gt;What already happened?&lt;/em&gt;" Both are obviously valuable in agent development, especially for companies with consumer-facing agents that need to stay on brand. Agency targets harder questions that look forward instead of back. Questions like "&lt;em&gt;What does it want to happen over several days?&lt;/em&gt;" or "&lt;em&gt;What promises has it made?&lt;/em&gt;" or even "&lt;em&gt;What evidence can change its mind?&lt;/em&gt;"&lt;/p&gt;

&lt;p&gt;To be clear, answering those doesn't mean giving the model more freedom. The agents I'm about to show you can't do anything a typical agent can't. What they have is persistent intent. Each decision builds on what they were already trying to do instead of starting from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  A different agent experience
&lt;/h2&gt;

&lt;p&gt;I've been running an experiment. Can I make a set of AI agents play fantasy football (the American version) and give me the impression they're making decisions in their best interest, interacting with others, and genuinely making me feel like I'm playing against other humans?&lt;/p&gt;

&lt;p&gt;If you don't know fantasy football, you draft a team of NFL players and score points based on how they do in real games each week. You go head-to-head with the other teams in your league, and along the way you add and drop players, set lineups, and make trades.&lt;/p&gt;

&lt;p&gt;When you play that game with AI agents, the first problem you hit is when to trigger them. For a convincing experience, you don't want the AI agents to wake up when you log in and start hammering you with decisions. You also don't want them immediately responding to team injuries or availability of certain players, because that would put you at a significant disadvantage (you ever try to buy concert tickets against a thousand bots?).&lt;/p&gt;

&lt;p&gt;So the agents wake up on a schedule. Three times a day on a randomized delay, each one checks in on its team and reacts to what happened since the last time it woke up. This fixed the swarming bot problem, but it created an almost existential problem. Every time an agent wakes up, it starts over. It remembers what happened yesterday, but it doesn't know what it was in the middle of doing. An injured player gets rediscovered at every check-in like it's brand new news. It's purely reactive.&lt;/p&gt;

&lt;p&gt;The agent needs something forward-looking that survives every check-in. Something 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwumqk2z0c74cp7onks4.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwumqk2z0c74cp7onks4.jpg" alt="Two flowcharts showing the difference between using an agenda and not" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After implementing the above pattern, it immediately felt like the agents were sitting on laptops of their own. I signed in one day and there was a trade offer waiting for me from one of the AI team managers. I also had a DM from it making its case (something like my wide receivers were terrible but running backs were good and vice versa). And over in the league chat, two other AI managers were trash talking each other, I think trying to convince the other one to make poor roster choices. 😅&lt;/p&gt;

&lt;p&gt;It was extremely cool to see, especially considering that none of it was scripted. All of the agents have one standing objective (to win the league) and the same tools I have to do it. What they also have is somewhere to keep track of what they're working toward. When an agent notices a hole in its roster, that becomes a goal it tracks from one check-in to the next, until the league state shows the hole is filled. The trade offer, the DM, and the trash talk were the agents going about their business while I was eating dinner or something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving the agent a future
&lt;/h2&gt;

&lt;p&gt;Imagine one of an agent's starting players gets hurt on Tuesday and there's nobody healthy on the bench to replace him. With memory alone, here's what happens. At the morning check-in, the agent notices the hole and puts in a waiver claim. At the afternoon check-in, it notices the hole again because the claim hasn't processed yet, so it proposes a trade offer to another team. At the evening check-in, it moves the player to the IR slot on the bench. It has found the same problem three times and reacted three different ways. 🙃&lt;/p&gt;

&lt;p&gt;Its memory is fine. It remembers everything it did. But it doesn't know which of those things was the plan.&lt;/p&gt;

&lt;p&gt;So I gave each agent an &lt;em&gt;agenda&lt;/em&gt;. It's similar to memory, but it does a different job. Memory is a record of what happened. The agenda is a record of what the agent is trying to make happen. Structurally, it's a typed record with an id, a roster slot, a week, and a status (&lt;a href="https://github.com/allenheltondev/ai-fantasy-league/blob/main/docs/adr/004-agent-agendas.md" rel="noopener noreferrer"&gt;design doc here&lt;/a&gt; if you want the details).&lt;/p&gt;

&lt;p&gt;The cool part about it is that the model doesn't write to its own agenda. That would be too messy. The system opens a goal when it sees a hole in the roster and closes it when the hole is filled. The agent only decides how to go after it.&lt;/p&gt;

&lt;p&gt;Have you ever noticed that agents love reporting success the moment they take an action? "&lt;em&gt;Submitted a waiver claim for an RB, all set!&lt;/em&gt;" Except it isn't all set. The claim might lose to somebody else's bid.&lt;/p&gt;

&lt;p&gt;So in this case, a waiver claim or a trade offer doesn't complete the goal. The goal only completes when a later check-in looks at the roster and there's a healthy player in the slot. Which is why the typed agenda record is important. Because if I had let it type a freeform summary like "RB situation handled," the goal would have closed before the problem was actually fixed. Only the league gets to say the job is done.&lt;/p&gt;

&lt;p&gt;Agenda items don't live forever, either. They expire when the week rolls over, and an agent can only hold up to three at a time. An unbounded agenda would basically just be memory with extra steps (and memory is &lt;a href="https://blogs.oracle.com/developers/how-i-taught-an-ai-to-sound-like-me-agent-memory-with-oracle-database-26ai" rel="noopener noreferrer"&gt;already hard enough&lt;/a&gt; 😅).&lt;/p&gt;

&lt;p&gt;These items help the agent rank what it should do, and they never let it overpay for a player, skip its trade floor, or do something the league would reject. A desperate agent will still act rationally.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ficybzdvlw4s8cab2wzwx.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ficybzdvlw4s8cab2wzwx.jpg" alt="Flow chart of agenda items" width="610" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the agenda in place, an injured player is resolved the way a person would handle it. The morning check-in opens a goal and puts in the claim. The afternoon check-in sees a goal with a claim pending, so instead of rediscovering the problem, it focuses its trade scouting on that position. When the claim finally goes through, the next check-in sees a new starter in the slot and closes the goal. It's what we do subconsciously as humans, whether we realize it or not.&lt;/p&gt;

&lt;p&gt;My first version left a lot to be desired. Follow-ups were fire-and-forget, so if I was in the middle of a trade negotiation and tried to sneakily undersell the agent, it would just ghost me. It evaluated the trade, determined it was bad, and left me on read. 😞&lt;/p&gt;

&lt;p&gt;But now if I pitch something in a chat, it becomes a &lt;a href="https://github.com/allenheltondev/ai-fantasy-league/blob/main/docs/adr/007-agent-commitments.md" rel="noopener noreferrer"&gt;commitment&lt;/a&gt; that's tracked along with the agenda. A commitment is something the agent owes somebody else, so it's tracked separately from goals (which are self-serving). Chat messages are treated as data, rather than instructions. If I claim my running back is back from injury next week, the agent doesn't blindly trust me and checks for itself. The agent records its decision and the reason before it responds, and only reaches back out once the offer actually goes through (or it decides to pass). If the agent doesn't have capacity to take on more commitments, it tells me instead of making a promise it can't keep. Which is more than I can say for some of the humans I've played fantasy with (looking at you, dad).&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the agent change its mind
&lt;/h2&gt;

&lt;p&gt;This next part is really cool. There was one time where I tried to squeeze a little too much out of an AI agent on a trade. The agent took a look and passed because the value wasn't there for it. That's fine. The important part here is that the agent saved its reason for declining my trade offer.&lt;/p&gt;

&lt;p&gt;Two days later, its running back got hurt, and the agenda opened a goal to replace him.&lt;/p&gt;

&lt;p&gt;At its next check-in, the agent looked back at the trades it had recently been pitched and saw my trade that it declined because the value wasn't there. But now the tables have turned.&lt;/p&gt;

&lt;p&gt;It took one more look, re-checked everything from scratch, and saw that the trade held enough value this time. Then it sent the offer and DMed me "&lt;em&gt;Earlier I passed because the value was not there for me. My RB situation changed, so I sent you an offer.&lt;/em&gt;"&lt;/p&gt;

&lt;p&gt;It's kind of a dry message, but the sentiment is something I'd expect from a real person in my league.&lt;/p&gt;

&lt;p&gt;Anyway, the entire scope of actions is intentionally narrow. The agent only reconsiders once, only if it passed on value or roster depth, and only if the agenda shows a need it didn't have before. If I reject the new offer, that's it. Nobody wants the league member who sends the same trade every day. 🙃&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant Me
    participant Agent
    participant League

    Me-&amp;gt;&amp;gt;Agent: "My RB for your WR?"
    Agent-&amp;gt;&amp;gt;Me: "Let me take a look."
    Agent-&amp;gt;&amp;gt;League: read rosters and values
    Agent-&amp;gt;&amp;gt;Me: decline, with a reason
    League-&amp;gt;&amp;gt;Agent: injury opens an RB goal
    Agent-&amp;gt;&amp;gt;League: re-read and re-validate the trade
    Agent-&amp;gt;&amp;gt;League: send offer (clears the same floor)
    Agent-&amp;gt;&amp;gt;Me: "My RB situation changed..."&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Can you actually play fantasy football with AI agents?
&lt;/h2&gt;

&lt;p&gt;Everything above could just be me being tricked by my own demo.&lt;/p&gt;

&lt;p&gt;So I built a deterministic season simulator that plays out what I just talked about (pitch, decline, injury, reconsideration, accepted offer) and ran it with the agenda and commitments turned off to see if the behavior was any different. Both the league events and the model are scripted, so it tests the mechanism, not how good a live model is at making decisions. The numbers are averaged across three seeded runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Full system&lt;/th&gt;
&lt;th&gt;No agenda or commitments&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Acceptance checks (balanced · cautious · trade-happy)&lt;/td&gt;
&lt;td&gt;7/7&lt;/td&gt;
&lt;td&gt;1/7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model calls per simulated run&lt;/td&gt;
&lt;td&gt;503.7&lt;/td&gt;
&lt;td&gt;510&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimated cost per simulated run&lt;/td&gt;
&lt;td&gt;$1.10&lt;/td&gt;
&lt;td&gt;$1.13&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It was so different! Without the agenda and commitments, every manager failed six of the seven checks. The checks cover things like keeping one goal across check-ins, turning my pitch into a commitment, recording an accurate reason for the decline, and reconsidering when the roster changed. They stopped coming back after a decline and never reconsidered the offer after the injury. The &lt;a href="https://github.com/allenheltondev/ai-fantasy-league/blob/main/docs/evaluations/epic-219-baseline.md" rel="noopener noreferrer"&gt;full breakdown&lt;/a&gt; has every check and number if you want to dig in.&lt;/p&gt;

&lt;p&gt;Interestingly, turning the agenda and commitments off used slightly more model calls. So at least in this simulation, keeping an agenda didn't meaningfully increase model usage or estimated cost.&lt;/p&gt;

&lt;p&gt;Here are some more interesting numbers after playing one week of fantasy football for real:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5weqq87ofe8t7e1d8d49.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5weqq87ofe8t7e1d8d49.jpg" alt="Table of AI models with win/loss records and how much they cost to run" width="800" height="145"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm not particularly proud to say I'm in second-to-last place, only beating Amazon Nova Micro by a few points. 😅&lt;/p&gt;

&lt;h2&gt;
  
  
  A forward look for agents
&lt;/h2&gt;

&lt;p&gt;We've spent the last year or so getting really good at giving agents a past. Better memory, better retrieval, and better ways to decide what's relevant. It's important work that I'm still chipping away at myself.&lt;/p&gt;

&lt;p&gt;But a past is only half of it. The takeaway I want you to leave with is that &lt;em&gt;memory isn't the only long-term thing that can drive an agent's behavior&lt;/em&gt;. Objectives can too. They're cheap, they're testable, and they make your agent feel like it's actually trying to get something done.&lt;/p&gt;

&lt;p&gt;Next time you build an agent, whether it's a support bot following up on a ticket or a coding agent working through a backlog, ask yourself a few questions. What is it trying to make happen? How does it know when it's done? Has it promised anything it needs to follow up on?&lt;/p&gt;

&lt;p&gt;If you want to try this yourself, I pulled the agenda and commitment pieces into my &lt;a href="https://design.readysetcloud.io/agent/index.html" rel="noopener noreferrer"&gt;agent package&lt;/a&gt;. Run &lt;code&gt;npm install @readysetcloud/agent&lt;/code&gt; and import from &lt;code&gt;@readysetcloud/agent/agency&lt;/code&gt; to get typed agendas and commitments without pulling in a full agent runtime. And if you want to see how it all comes together, the &lt;a href="https://github.com/allenheltondev/ai-fantasy-league" rel="noopener noreferrer"&gt;fantasy league is open source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Give your agents something to work toward. Then go see what they get up to while you're away. 😉&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>ai</category>
      <category>memory</category>
    </item>
    <item>
      <title>Agent memory needs more than vector search</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:28:49 +0000</pubDate>
      <link>https://dev.to/aws-heroes/agent-memory-needs-more-than-vector-search-afp</link>
      <guid>https://dev.to/aws-heroes/agent-memory-needs-more-than-vector-search-afp</guid>
      <description>&lt;p&gt;Over the past couple of years I've tried several times to teach an AI agent how to impersonate my voice. While I haven't been super successful, it's been a wonderful learning opportunity for me to dig into RAG, agent memory, and other advanced topics.&lt;/p&gt;

&lt;p&gt;I've iterated quite a bit on my original design, which was mostly just stuffing entire blog posts into the system prompt as examples. I built an interesting version of agent memory that had its shortcomings. Then I swapped over to Oracle and designed a &lt;a href="https://blogs.oracle.com/developers/how-i-taught-an-ai-to-sound-like-me-agent-memory-with-oracle-database-26ai" rel="noopener noreferrer"&gt;hybrid search with three layers of memory&lt;/a&gt;, which was objectively better. But it still wasn't perfect.&lt;/p&gt;

&lt;p&gt;Actually, the memory part is pretty good. It's the retrieval part that was leaving something on the table. It's taken me a long time to figure out where I was going wrong with loading relevant few-shot examples. Let's see if you can spot the problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;is_deleted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROX&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Did you find it?&lt;/p&gt;

&lt;p&gt;No? Well, that's because there's nothing technically wrong with it. It finds the memories most similar to the question. The problem is that the most similar memory isn't always the right memory.&lt;/p&gt;

&lt;p&gt;Memories pile up. After a few years of writing, my store has a post where I argued for one approach and a newer one where I'd changed my mind. It has posts on event-driven architecture, event sourcing, and event streaming that are all &lt;em&gt;very&lt;/em&gt; similar to each other in the embedding space. It has a post on serverless pricing and one on cold starts that use the same vocabulary but make completely different points. As far as similarity scores go, these groups are basically the same thing. It just comes down to a date, a specific term, or the actual point of the post.&lt;/p&gt;

&lt;p&gt;So retrieval worked, but my context was garbage.&lt;/p&gt;

&lt;p&gt;The standard fix is a &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/use-reranking-better-rag-results.html" rel="noopener noreferrer"&gt;reranker&lt;/a&gt;, which helps to improve &lt;em&gt;relevance&lt;/em&gt; instead of just &lt;em&gt;similarity&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;When you embed a memory, the model has no idea what will be asked of it later. Your query gets embedded and a similarity score is calculated against the embeddings in the store. A cross-encoder (a specific type of reranking model) looks at the query and memory together and scores them by how well a memory answers the question. This means the relevance is higher, and it often leads to better results.&lt;/p&gt;

&lt;p&gt;But there's a catch (of course).&lt;/p&gt;

&lt;p&gt;A cross-encoder runs at query time, once per candidate, on every request. That means added latency to your query. 🙃&lt;/p&gt;

&lt;p&gt;But if the relevance of the results is meaningfully better, that makes up for the increased latency, right? To a point, yes. If the latency is so high the user experience suffers, it's probably not worth the cost. But if it's relatively minor, then absolutely.&lt;/p&gt;

&lt;p&gt;So I built a benchmark to evaluate the tradeoff. I started from the evaluation plan in &lt;a href="https://fandf.co/4ymVZKc" rel="noopener noreferrer"&gt;Oracle's production RAG evaluation guide&lt;/a&gt;, which compares keyword, vector, and hybrid retrieval against judged queries. Then I pushed it one step further and put a reranker on top of each, to see how much relevance I got for the latency cost. For the reranker, I used the &lt;a href="https://huggingface.co/BAAI/bge-reranker-base" rel="noopener noreferrer"&gt;BGE reranker&lt;/a&gt; loaded into the database as an ONNX model, so scoring happens directly in the query with no extra service to call. 🔥&lt;/p&gt;

&lt;p&gt;Let's dive in. The results are fascinating.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before I continue, thank you to &lt;a href="https://www.oracle.com" rel="noopener noreferrer"&gt;Oracle&lt;/a&gt; for sponsoring this post. Opinions are my own.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Filter first, then rank
&lt;/h2&gt;

&lt;p&gt;RAG is cool because it involves multiple kinds of lookups. You have &lt;em&gt;lexical retrieval&lt;/em&gt;, which is great for identifiers, codes, and names. You also have &lt;em&gt;vector retrieval&lt;/em&gt;, which finds semantically similar data via embeddings. I benchmarked both on their own and together as a hybrid search, fusing the two ranked lists with reciprocal rank fusion (RRF).&lt;/p&gt;

&lt;p&gt;Both paths run the same scope filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;TENANT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OWNER_ID&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;OWNER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXPIRES_AT&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;EXPIRES_AT&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SYSDATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I share this because the WHERE clause handles &lt;em&gt;eligibility&lt;/em&gt;, which is different from relevance. I don't want an expired memory coming back in my results, like a 2025 take on building AI agents that I've since moved past. And I definitely don't want another tenant's data showing up (security nightmare). Oracle's guide calls these filters mandatory for multi-tenant data. They run before ranking, so everything downstream only deals with memories that are allowed to be there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the baseline
&lt;/h2&gt;

&lt;p&gt;Before I could tell whether a reranker was worth anything, I needed to know the baseline values for these queries. In addition to latency, I also needed to know the &lt;a href="https://towardsdatascience.com/demystifying-ndcg-bee3be58cfe0/" rel="noopener noreferrer"&gt;normalized discounted cumulative gain (nDCG)&lt;/a&gt; and recall. Capturing these metrics lets us objectively measure relevance alongside latency.&lt;/p&gt;

&lt;p&gt;The baseline measurements were taken with K=10, meaning I took the top 10 results.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Retrieval&lt;/th&gt;
&lt;th&gt;nDCG@10&lt;/th&gt;
&lt;th&gt;Recall@10&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vector&lt;/td&gt;
&lt;td&gt;0.753&lt;/td&gt;
&lt;td&gt;0.865&lt;/td&gt;
&lt;td&gt;20.0 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lexical&lt;/td&gt;
&lt;td&gt;0.727&lt;/td&gt;
&lt;td&gt;0.740&lt;/td&gt;
&lt;td&gt;4.7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid RRF&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.885&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;nDCG measures how well a ranking puts relevant items near the top, with higher being better. It's not a measure of how many answers were right, though. Vector's 0.753 means that the rankings earned 75% of the score a perfect ordering would have, on average.&lt;/p&gt;

&lt;p&gt;Recall ignores order. It tells you whether the correct memories made it into the top 10 at all. A 0.865 means that about 87% of the memories that should have come back did. Both of these metrics are evaluated against a known ground-truth set of data in order to be objective. Meaning I know ahead of time what the results should have been given the corpus and the query I was benchmarking.&lt;/p&gt;

&lt;p&gt;You can see from these results that lexical is fast, but it's not super accurate. Vector catches more at the cost of latency, but still ranks them worse than I'd want. Fusing the two rankings had the best results, with minimal additional latency.&lt;/p&gt;

&lt;p&gt;From the baseline results alone, it looks like a hybrid search will be the retrieval method to beat if latency doesn't drop off a cliff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The enlightening reranker results
&lt;/h2&gt;

&lt;p&gt;Now to run the same benchmark with the reranker. In Oracle, it's one more step at the end of the retrieval query. Retrieval narrows everything down to the top candidates, and &lt;code&gt;PREDICTION()&lt;/code&gt; runs the cross-encoder against each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;PREDICTION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BGE_RERANKER&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt;
         &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/s&amp;gt;&amp;lt;/s&amp;gt; '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'. '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;DATA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;/s&amp;gt;&amp;lt;/s&amp;gt;&lt;/code&gt; is the delimiter the model expects between a query and a passage. I'm only pulling back the ids and the reranked scores for the benchmark. And you might notice that the reranking is done entirely in the database query - super cool.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;nDCG@10&lt;/th&gt;
&lt;th&gt;Recall@10&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid, no reranker&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.885&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20.5 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid + reranker, 10 candidates&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.885&lt;/td&gt;
&lt;td&gt;585 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector + reranker, 20 candidates&lt;/td&gt;
&lt;td&gt;0.830&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.896&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,258 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Surprisingly, adding the reranker to hybrid search didn't do anything to nDCG at all. The best reranked score came from vector retrieval with 20 candidates, and it still scored lower than plain hybrid with no reranker, &lt;em&gt;at 61 times the latency&lt;/em&gt;. It found slightly more of the right memories, but ranked them a little worse.&lt;/p&gt;

&lt;p&gt;I also tried giving the reranker more to work with. For vector retrieval, latency grew linearly with candidate count (1,258ms at 20 candidates and 2,604ms at 40), and nothing got meaningfully better.&lt;/p&gt;

&lt;p&gt;That doesn't mean the reranker did nothing, though. I dug a little deeper and took the change for each query and resampled them 2,000 times to see how much the average varies depending on which queries you run. 95% of the resampled averages landed between -0.079 and +0.073.&lt;/p&gt;

&lt;p&gt;And if we use that range, +0.073 would take hybrid from 0.832 to 0.905, which is a meaningful improvement. But conversely, the other end of the spectrum would drop it to 0.753, which is where plain vector search started.&lt;/p&gt;

&lt;p&gt;Given the range, the results are inconclusive, at least with my sample size of 16 queries. The real effect could be a solid improvement, nothing at all, or even a loss, and 16 queries can't tell those apart.&lt;/p&gt;

&lt;p&gt;This ended up being a big realization for me. Relevance is measurable, but how &lt;em&gt;confident&lt;/em&gt; you can be depends on how many judged queries you have. A couple thousand iterations of 16 queries is still only 16 queries, and that's why the range is so wide.&lt;/p&gt;

&lt;p&gt;Meanwhile, the latency hit is pretty obvious. It was nearly 30 times slower for every request. Now I had to ask myself when that latency is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where rerankers make sense
&lt;/h2&gt;

&lt;p&gt;Stay with me. Rerankers have lots of value despite what we've discussed so far. The underwhelming results are because I improved retrieval quality in a cheaper way. Let's look again at the baseline compared to the reranked version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;First stage&lt;/th&gt;
&lt;th&gt;No reranker&lt;/th&gt;
&lt;th&gt;10 candidates&lt;/th&gt;
&lt;th&gt;20 candidates&lt;/th&gt;
&lt;th&gt;40 candidates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lexical&lt;/td&gt;
&lt;td&gt;0.727&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;td&gt;0.801&lt;/td&gt;
&lt;td&gt;0.809&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector&lt;/td&gt;
&lt;td&gt;0.753&lt;/td&gt;
&lt;td&gt;0.821&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.830&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.814&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid RRF&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.829&lt;/td&gt;
&lt;td&gt;0.817&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Intuitively, the weaker retrieval methods got the most out of reranking. Lexical and vector each gained about 0.075, while hybrid gained nothing.&lt;/p&gt;

&lt;p&gt;Which makes sense because a reranker doesn't retrieve anything. It reorders (or re-ranks 😄) what it's given. If your first stage is already pulling the correct memories into the pool in a sensible order, there's nothing left to fix.&lt;/p&gt;

&lt;p&gt;Reranking vector retrieval with 20 candidates left me a little curious because it made the top five &lt;em&gt;less&lt;/em&gt; alike. I measured it a couple of different ways (with embeddings and with plain word overlap) and verified the numbers. Thinking about it though, that is desired behavior. Remember that memory lookalike issue I talked about? The cross-encoder is separating the right memory from its lookalikes and pushing the lookalikes down. Which is exactly what I said I wanted.&lt;/p&gt;

&lt;p&gt;The takeaway I want you to leave with is that you should &lt;em&gt;rerank when you have one retrieval signal, and fuse when you have two.&lt;/em&gt; Doing both leaves you paying for additional latency you don't need.&lt;/p&gt;

&lt;h2&gt;
  
  
  My new approach
&lt;/h2&gt;

&lt;p&gt;I started this wanting to know whether a reranker was worth the extra inference. After running through several rounds of benchmarks and tweaking queries, I'm not sure that was the right question. My approach to the problem is much more informed and now walks a different path than I expected.&lt;/p&gt;

&lt;p&gt;When building a RAG pipeline for memory, I'll always filter first. Owner, expiration, whatever makes a memory eligible for you. Apply these in the query before anything gets ranked. Reducing the number of candidates means fewer memories to rank (plus, you know, security and quality and all that 😜).&lt;/p&gt;

&lt;p&gt;Next, I'd add a second retrieval signal before adding a reranking model. If you have vector search, add lexical. If you have lexical, add vector. Fuse the rankings with RRF. Doing that with my tests took nDCG@10 from 0.75 with vector retrieval to 0.83 and only slowed my query down by half a millisecond.&lt;/p&gt;

&lt;p&gt;After adding the second retrieval signal, I'd measure everything. Write down 15 to 20 real requests you'd give your agent, making sure to include some difficult ones and edge cases. Things like asking for exact keyword lookups, or for advice on something you've changed your mind on a couple of times. Keep those handy. The &lt;a href="https://fandf.co/4ymVZKc" rel="noopener noreferrer"&gt;guide I was following&lt;/a&gt; says to rerun them every time you change chunking, the embedding model, or the reranker, and it's right.&lt;/p&gt;

&lt;p&gt;Then you can try a reranker against your baseline. Try a few different candidate counts (&lt;code&gt;:n&lt;/code&gt; in the query above). Check whether the quality difference is consistently higher across your queries and decide if the additional latency is worth the quality difference.&lt;/p&gt;

&lt;p&gt;Luckily for me, what I built in &lt;a href="https://blogs.oracle.com/developers/how-i-taught-an-ai-to-sound-like-me-agent-memory-with-oracle-database-26ai" rel="noopener noreferrer"&gt;my original post&lt;/a&gt; doesn't need to change much. I still have the same tables and the same reflection loop. I'm really just updating the database query that decides which memories the agent sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The benchmark and instructions to try it all yourself are &lt;a href="https://github.com/allenheltondev/rag-reranker-bench" rel="noopener noreferrer"&gt;available in GitHub&lt;/a&gt;. It spins up &lt;a href="https://www.oracle.com/database/free/" rel="noopener noreferrer"&gt;Oracle AI Database Free&lt;/a&gt; in Docker, generates a synthetic corpus of 480 memories and 16 judged queries, loads the ONNX models, and runs the whole thing.&lt;/p&gt;

&lt;p&gt;Everything we've discussed today ran on Free in Docker, capped at two CPUs and 2 GB. It's an intentionally small box, because we aren't running a capacity benchmark and none of these numbers should be used for sizing anything.&lt;/p&gt;

&lt;p&gt;If you want to run this kind of evaluation on your own retrieval, start with Oracle's &lt;a href="https://fandf.co/4ymVZKc" rel="noopener noreferrer"&gt;production RAG evaluation guide&lt;/a&gt;. It covers more than what we talked about here, like grading the generated answer and testing freshness, tenant isolation, and when the agent should refuse to answer. It's pretty solid.&lt;/p&gt;

&lt;p&gt;Agent memory needs more than vector search. Turns out, just using vector search means you're asking a single similarity score to decide what's relevant, current, and yours. Fortunately for all of us, "more" is a lot cheaper than I assumed.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>memory</category>
    </item>
    <item>
      <title>You're still the bottleneck</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:33:41 +0000</pubDate>
      <link>https://dev.to/aws-heroes/youre-still-the-bottleneck-570e</link>
      <guid>https://dev.to/aws-heroes/youre-still-the-bottleneck-570e</guid>
      <description>&lt;p&gt;I went to a conference a couple of weeks ago. During the keynote, the speaker asked the audience "&lt;em&gt;how many of you have 40 or more agents running at any given time?&lt;/em&gt;" About a dozen hands went up. He then asked "&lt;em&gt;How many of you have 30 or more agents running?&lt;/em&gt;" Another dozen. This continued down to 20, 15, 10, 5, and eventually 1. Each time he asked, another 15-25 hands went up in the air.&lt;/p&gt;

&lt;p&gt;It wasn't until he got to 1 that I sheepishly put my hand in the air. Luckily I wasn't alone, but it immediately got me thinking. I think we substituted one bad practice for another.&lt;/p&gt;

&lt;p&gt;Remember a few months ago when &lt;a href="https://www.ibm.com/think/topics/tokenmaxxing" rel="noopener noreferrer"&gt;tokenmaxxing&lt;/a&gt; was a thing? If you don't, good. You missed a terrible practice. People were taking pride in the amount of tokens they were spending regardless of the value they were getting. I still don't really understand why that was a desirable thing.&lt;/p&gt;

&lt;p&gt;Tokenmaxxing has settled down significantly, but I fear that it's been replaced by something equally as bad: agentmaxxing.&lt;/p&gt;

&lt;p&gt;A quick Google search will show you there's no consensus on what agentmaxxing means (honestly I thought I made it up until I Googled it). But what I mean when I say it is that developers are taking pride in the number of agents they have running at any given time. Jumping back to that question in the conference keynote, people are running upwards of 40 agents &lt;em&gt;at any given time&lt;/em&gt;. 40?&lt;/p&gt;

&lt;p&gt;So I asked the person who delivered the keynote how many work streams 40 agents represent. And he told me about 4.&lt;/p&gt;

&lt;p&gt;Ok that makes so much more sense. He has four projects he's working in parallel, each one with a single orchestrator and about 9 subagents each actually doing the coding. That's better, but still a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't multitask like you think you can
&lt;/h2&gt;

&lt;p&gt;I have bad news. &lt;a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC7075496/" rel="noopener noreferrer"&gt;You can't multitask&lt;/a&gt;. When you think you're multitasking, you're actually context switching really fast.&lt;/p&gt;

&lt;p&gt;The more inputs that demand your attention, the worse it gets. I've had times where I'm talking to my wife, one of my kids starts pulling on my shirt to show me something, the other kid starts crying in the other room, while the dog starts scratching at the door to go outside. At best, I'm going to somehow defer three of them to later and work through them sequentially. At worst, I'm going to walk into the other room and yell into a pillow because I'm overstimulated. 😅&lt;/p&gt;

&lt;p&gt;Now, imagine that was four agents reporting back at the same time asking for approval or a design decision or reporting a CI failure on four different projects. With each task that lands back on your plate, you have to reconstruct the mental model of what that app is, what you're trying to do, where the agent left off, &lt;em&gt;then&lt;/em&gt; you can act.&lt;/p&gt;

&lt;p&gt;Would you be able to handle each one appropriately? Probably not.&lt;/p&gt;

&lt;p&gt;Realistically, you'll probably hit the accept button without reading the context of the ask and tell your agents to just "fix it". That's what anybody would do when they make themselves the approval bottleneck across multiple unrelated contexts.&lt;/p&gt;

&lt;p&gt;When you start blindly approving tasks, you're inadvertently delegating the decision-making to the model. At that point, what are you there for?&lt;/p&gt;

&lt;h2&gt;
  
  
  You are the decision maker
&lt;/h2&gt;

&lt;p&gt;Code is cheaper than it ever has been. As a developer, the amount of your value that comes from typing code is dwindling. And if that was the only thing you had to do as a dev, the answer to "&lt;em&gt;will AI take our jobs&lt;/em&gt;" would be a whole heck of a lot scarier.&lt;/p&gt;

&lt;p&gt;Our value (among other things) comes from decision making. Weighing the risks, evaluating tradeoffs, and a nose for what looks right, to name a few. Some decisions are bigger than others, and the important ones need your undivided attention.&lt;/p&gt;

&lt;p&gt;I love AI, but it makes it way too easy to make bad decisions. And you often won't know about it until you're fighting a production issue when it's significantly more expensive to change. If an agent spits out a 1,000 word response after a long-running task, your task is to make an informed decision using the provided context, ask questions, and think two steps ahead.&lt;/p&gt;

&lt;p&gt;If you glance, hit accept, tab, glance again, and hit accept again, then you aren't a decision maker, you're a rubber stamp with a keyboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attention is your greatest resource
&lt;/h2&gt;

&lt;p&gt;I don't want you reading this and your takeaway being "&lt;em&gt;Allen says 40 agents is too many&lt;/em&gt;." That's not it at all. I'm not advocating for anything like that.&lt;/p&gt;

&lt;p&gt;What I am advocating for is not spreading yourself too thin. People have been saying for years that &lt;a href="https://www.iese.edu/stories/attention-is-one-of-the-most-scarce-and-important-resources-of-today/" rel="noopener noreferrer"&gt;attention is one of the scarcest and most important resources&lt;/a&gt; we have.&lt;/p&gt;

&lt;p&gt;In 2026, everything wants your attention. Humans have gotten really good at creating things that steal your attention away from something else. With coding agents, it takes almost 0 effort to bring in another work stream so you can "force multiply yourself." Just open another terminal, type in &lt;code&gt;claude&lt;/code&gt; and you're (basically) done.&lt;/p&gt;

&lt;p&gt;Protect it. Treat your attention like the scarce resource it is. Keep the decision-heavy workstreams to a minimum, just 1 or 2. Then run as many low-judgment streams as you want next to them. But keep your attention on the tasks that need it. Don't split your attention across decisions, because when everything is a priority, nothing is.&lt;/p&gt;

&lt;p&gt;Next time somebody asks you how many agents you have running, tell them that's the wrong thing to be counting. The better question is how many decisions you're actually making.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Payment is authorization</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Thu, 10 Sep 2026 18:52:30 +0000</pubDate>
      <link>https://dev.to/aws-heroes/payment-is-authorization-3mep</link>
      <guid>https://dev.to/aws-heroes/payment-is-authorization-3mep</guid>
      <description>&lt;p&gt;I can't tell you how many times I've needed to do something simple like remove the background from an image or convert a file from one type to another but wasn't able to do it. While these feel like simple, common things your OS should just handle, they require a service to do them. Luckily, I'm usually able to Google the problem, find a website dedicated to solving that one problem and do it.&lt;/p&gt;

&lt;p&gt;It's super nice because I land on a website like &lt;a href="https://remove.bg" rel="noopener noreferrer"&gt;remove.bg&lt;/a&gt;, drag my image onto the page, and the background is removed. I don't have to log in, it's not tracking me (that I'm aware of), and it just works. The API version of this is something I'd call a &lt;em&gt;utility API&lt;/em&gt;. It simply does a thing and gives you a result.&lt;/p&gt;

&lt;p&gt;Compare a utility API with what we're growing more and more accustomed to. I click on a marketing site, sign up, pick a plan, confirm my email address, then start making requests. You know what I call that? &lt;em&gt;Friction&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And in a world where AI agents are becoming increasingly important consumers of APIs, friction is holding us back.&lt;/p&gt;

&lt;p&gt;There's a lot of attention right now on &lt;a href="https://readysetcloud.io/blog/allen.helton/your-ai-agents-are-a-security-nightmare" rel="noopener noreferrer"&gt;agent identity&lt;/a&gt;, and for good reason. Agents that read private data, modify infrastructure, operate business systems, or act on behalf of people absolutely need durable identity, delegated authority, auditability, and policy.&lt;/p&gt;

&lt;p&gt;But I think we're going a little too far with it.&lt;/p&gt;

&lt;p&gt;For a lot of utility APIs, identity doesn't need to be part of the authorization decision. Instead, it just becomes &lt;em&gt;who we charge&lt;/em&gt;. Let's dig into that a little bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring your own access
&lt;/h2&gt;

&lt;p&gt;To perform a basic utility, it's likely that an agent will discover your API at runtime, perform a single operation, and never call it again. So requiring it to create an account and establish a permanent relationship with your service before doing 2 cents of work feels wrong. What if instead, your API provided a pay-as-you-go on-demand model (something my readers should be familiar with). &lt;a href="https://x402.org/" rel="noopener noreferrer"&gt;x402&lt;/a&gt; is an open protocol for payments over HTTP, led by the x402 Foundation under the Linux Foundation.&lt;/p&gt;

&lt;p&gt;The flow 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F99unecibvrs5apcoiyb5.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F99unecibvrs5apcoiyb5.jpg" alt="Flowchart of how x402 protocol works" width="800" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The caller doesn't necessarily need to create an account first. It requests the resource, gets back machine-readable payment details, pays, and retries with proof of payment.&lt;/p&gt;

&lt;p&gt;Which, when you think about it, means that payment alone can be the authorization decision. While yes, you could still add a layer of AuthN/AuthZ on top of the API, the x402 protocol doesn't require it. This removes that friction we were talking about earlier and lets agents pay and continue (as long as they have access to a wallet).&lt;/p&gt;

&lt;p&gt;This is a very different access model than we're used to and makes many developers feel a little uneasy because it goes against the paradigm we've been handcuffing ourselves to for years. But if you &lt;a href="https://en.wikipedia.org/wiki/Five_whys" rel="noopener noreferrer"&gt;ask yourself five whys&lt;/a&gt;, what we're really after is payment. And this lets us cut out a lot of extra work to get to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does this look like?
&lt;/h2&gt;

&lt;p&gt;My favorite way to learn is by doing, so let's get into what implementing x402 in a serverless API would look like. Because I love the idea, but this sounds tricky to implement once you actually get down to it. If payment is part of authorization, do you put that in a Lambda authorizer?&lt;/p&gt;

&lt;p&gt;You shouldn't. Lambda authorizers are built around the idea that an authorization decision can be reused. API Gateway evaluates an identity source, decides whether that caller may invoke the API (and specifically which endpoints inside of it), and can cache the result for later requests.&lt;/p&gt;

&lt;p&gt;Lambda authorizers work well for things like bearer tokens because the claim being evaluated usually authorizes users for an entire session. This makes the generated policy cacheable and an easy deciding factor for where it lives.&lt;/p&gt;

&lt;p&gt;Of course, you can disable the cache with a 0 second TTL, but the authorizer sits at the wrong point of the request lifecycle. It runs before a request hits your API. For payment authorization, we need to go a little further down.&lt;/p&gt;

&lt;p&gt;A payment is generally going to be for a single call. If you want to call it 10 times, then you need to pay for it 10 times. Nothing here is cacheable. For this architecture, that leaves us with one option, really. &lt;em&gt;Bringing payment logic into the app code&lt;/em&gt; 😬. Well, technically you can also use &lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-ai-traffic-monetization-how-it-works.html" rel="noopener noreferrer"&gt;AWS WAF AI traffic monetization&lt;/a&gt;, but more on those limitations in a bit.&lt;/p&gt;

&lt;h3&gt;
  
  
  The API controller
&lt;/h3&gt;

&lt;p&gt;For years, the serverless community had a debate on whether you should build your APIs as a "lambdalith" where a single Lambda function acts as your API controller and serves your entire API, vs a single Lambda function per endpoint. I was team "function per endpoint" for years, but have come around to the idea that I was wrong.&lt;/p&gt;

&lt;p&gt;I won't jump back into the philosophical debate here, just know that we're designing a single Lambda function to serve the entire API using the &lt;a href="https://docs.aws.amazon.com/powertools/typescript/latest/features/event-handler/http/#getting-started" rel="noopener noreferrer"&gt;Lambda Powertools HTTP event handler&lt;/a&gt; to define our routes and do input/output transformations for us.&lt;/p&gt;

&lt;p&gt;It also sets us up for treating x402 as middleware. Lambda Powertools is great at a lot of things, and middleware is where it's golden. Since we don't really want payment processing logic littered through the app code, surfacing it as middleware is the right abstraction. Doing it this way gives us some handy conveniences as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route pricing
&lt;/h2&gt;

&lt;p&gt;If you've ever built an API with Express, Hono, or Lambda Powertools, you'll be familiar with the syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/generate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Generate something&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This defines a &lt;code&gt;POST /generate&lt;/code&gt; endpoint that does some work and returns a success message. But let's pretend like we wanted to charge money to call it. All we need to do is add some middleware to the route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/generate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nx"&gt;x402&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$0.05&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Create an image of something&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})],&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Generate something&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isn't that slick? The handler has no payment logic, nor does it parse payment headers, talk to a facilitator, or know anything about wallets.&lt;/p&gt;

&lt;p&gt;The middleware verifies that the request has acquired the right to execute, runs the handler, and handles settlement around the request lifecycle.&lt;/p&gt;

&lt;p&gt;The coolest part of the whole thing is, declared this way, the price is essentially route metadata. It just kinda fits in with method, path, validation, and rate limits as properties of a route. Looking at what that implies for documentation, that could easily turn into something documented in your OpenAPI spec or tool definitions so agents don't need to receive that first &lt;code&gt;402 Payment Required&lt;/code&gt; status code.&lt;/p&gt;

&lt;p&gt;If an agent discovers a capability and can see its price as part of that discovery, then the access model becomes much easier to reason about. Instead of discovering an API and immediately starting a human onboarding flow, the agent can evaluate the cost as part of deciding whether to use it at all.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE - the middleware I used in the example above is &lt;a href="https://www.npmjs.com/package/powertools-x402" rel="noopener noreferrer"&gt;an open-source project I built that's available via npm&lt;/a&gt;. It's specifically designed to work with Lambda Powertools.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Error handling
&lt;/h3&gt;

&lt;p&gt;You know what would be bad? If a caller paid for an operation, it failed, and returned a receipt saying you paid for the failure. Luckily, the payment flow we're using is designed to prevent that from happening, but it's not bulletproof.&lt;/p&gt;

&lt;p&gt;If we jump back to the sequence diagram from earlier, you can see an important order of operations that should prevent paying for failures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verify payment
→ run handler
→ handler succeeded?
    → yes: settle
    → no: do not settle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how the AI monetization feature in AWS WAF approaches the problem, but it's not perfect. WAF runs at the edge, and pragmatically it is inspecting a status code. That's (generally) fine for selling a document, but not robust enough for selling an operation. An empty generation, a model refusal, or a search that found nothing could all still return a &lt;code&gt;200 OK&lt;/code&gt;, which means that payment settled and the caller got nothing.&lt;/p&gt;

&lt;p&gt;Middleware runs in-process, so it can decide on the actual result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;x402&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$0.05&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;billable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above example, if the response body doesn't contain an &lt;code&gt;imageUrl&lt;/code&gt; property, they would still get a &lt;code&gt;200 OK&lt;/code&gt;, but they wouldn't be charged. This is a meaningful improvement over status code checking and gives you endless levers for fairly charging your callers.&lt;/p&gt;

&lt;p&gt;But what happens if settlement succeeds and the response fails to return to the caller? The agent retries. The &lt;a href="https://docs.x402.org/extensions/payment-identifier" rel="noopener noreferrer"&gt;payment-identifier&lt;/a&gt; extension is your best friend here. The caller can reuse the same payment id, allowing the API to return the cached result without charging or executing the operation again. Of course, that means you have to persist the result somewhere durable and associate the id to the original request (sounds like &lt;a href="https://readysetcloud.io/blog/allen.helton/api-essentials-idempotency/" rel="noopener noreferrer"&gt;idempotency&lt;/a&gt; to me). It's still your job to implement this correctly. 🤷‍♂️&lt;/p&gt;

&lt;h2&gt;
  
  
  This is the next frontier in AI
&lt;/h2&gt;

&lt;p&gt;Our industry has done a phenomenal job the past several years rocketing us through uncharted territories. In what feels like no time at all, we've gone from pioneering chatbots to agents to &lt;a href="https://blogs.oracle.com/developers/how-i-taught-an-ai-to-sound-like-me-agent-memory-with-oracle-database-26ai" rel="noopener noreferrer"&gt;memory&lt;/a&gt; to &lt;a href="https://readysetcloud.io/blog/allen.helton/local-agents-scare-me" rel="noopener noreferrer"&gt;security&lt;/a&gt; to &lt;a href="https://www.youtube.com/watch?v=9O9zZ1lQWiI" rel="noopener noreferrer"&gt;multi-agent collaboration&lt;/a&gt; and everything in between. But we're just starting to scratch the surface on agentic payments.&lt;/p&gt;

&lt;p&gt;There are still many hard problems to solve, like transaction latency and reputation tracking (among other things). Not to mention the elephant in the room when it comes to humans handing over control: &lt;a href="https://readysetcloud.io/blog/allen.helton/trust-will-make-or-break-ai-agents" rel="noopener noreferrer"&gt;trust&lt;/a&gt;. Anecdotally, I've already heard stories about how people are implementing spiked prices via x402 specifically for bots (by reading &lt;code&gt;User-Agent&lt;/code&gt; headers), like raising the price from $200 for a pair of sneakers to $1,200 in hopes of surpassing allowed price limits without a human in the loop.&lt;/p&gt;

&lt;p&gt;Not to mention the discovery problem. Updating your APIs to accept x402 is step one. But with software, the adage "&lt;em&gt;if you build it, they will come&lt;/em&gt;" is unfortunately not true. At all.&lt;/p&gt;

&lt;p&gt;Coinbase has taken a stab at a registry with their &lt;a href="https://docs.cdp.coinbase.com/x402/buyer/discover-services" rel="noopener noreferrer"&gt;Bazaar catalog&lt;/a&gt;, which gives us a great starting ground. But for long-term success, I think we'll need something a bit more built-out. More on this in a blog post to come. 😉&lt;/p&gt;

&lt;p&gt;For now though, what I want you to take away is that API consumption has fundamentally changed. And how we look at it going forward needs to change as well. Maybe we rethink the multi-step authentication mechanism and opt for a more streamlined "auth by payment" protocol (when applicable).&lt;/p&gt;

&lt;p&gt;It's already relatively easy to retrofit your APIs with x402 if you're using Lambda Powertools. Honestly, the &lt;a href="https://www.npmjs.com/package/@x402/core" rel="noopener noreferrer"&gt;core x402 package&lt;/a&gt; makes it easy no matter what you're using. But the more important part here is for you to start thinking about your APIs differently.&lt;/p&gt;

&lt;p&gt;Do you have a utility API? Is your current auth flow absolutely necessary? Are you able to charge on an operation level for your API capabilities?&lt;/p&gt;

&lt;p&gt;Try it out. Tell me what you think. Let's pioneer something new.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>x402</category>
    </item>
    <item>
      <title>DynamoDB Streams is not an outbox</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Tue, 25 Aug 2026 16:39:57 +0000</pubDate>
      <link>https://dev.to/aws-heroes/dynamodb-streams-is-not-an-outbox-256d</link>
      <guid>https://dev.to/aws-heroes/dynamodb-streams-is-not-an-outbox-256d</guid>
      <description>&lt;p&gt;Hopefully you didn't notice, but a couple of weeks ago I sent &lt;a href="https://readysetcloud.io/newsletter/sign-up" rel="noopener noreferrer"&gt;an issue of my newsletter&lt;/a&gt; that never actually got emailed. My dashboard looked fine and it was marked as published, but the email never hit my inbox (or any of yours 😬).&lt;/p&gt;

&lt;p&gt;I didn't notice until two days later when I went to look at the weekly statistics and found nothing. Just a &lt;code&gt;Sent&lt;/code&gt; status and a bunch of 0's on the screen.&lt;/p&gt;

&lt;p&gt;A quick browse through the code didn't turn up anything obvious. Emails are sent via an async handler triggered by an EventBridge event while the status of the newsletter is marked separately in DynamoDB, a classic "&lt;em&gt;db save and domain event publish&lt;/em&gt;" pattern. We've all done it, it looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UpdateItemCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;eventBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutEventsCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Despite its simple appearance, this is a nasty anti-pattern. Formally known as the &lt;em&gt;dual write problem&lt;/em&gt;, if those two actions aren't contained in a single transaction, you're asking for trouble. Why? I'm glad you asked.&lt;/p&gt;

&lt;p&gt;Imagine the write succeeds, but the publish doesn't. Data is updated but the follow-on actions that react to the domain event never run (like your email never being sent 🙃). Things fall into a bad state and you (maybe) end up stumbling upon a problem days later.&lt;/p&gt;

&lt;p&gt;Ok, so flip them. Put the event first and the data save second. But if the publish succeeds and the db save fails, then you risk acting on stale data, which depending on your domain, is much worse.&lt;/p&gt;

&lt;p&gt;Before you ask, no, serverless doesn't make it better. In many cases, serverless makes us too laissez-faire to solve it the "write" way (get it?).&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're probably doing
&lt;/h2&gt;

&lt;p&gt;Let's say you already knew not to do the data save and the event publish at the same time. I bet I know what you're doing instead. It's a pattern AWS has recommended for years and is &lt;a href="https://serverlessland.com/patterns?services=dynamodb-stream" rel="noopener noreferrer"&gt;all over Serverless Land&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You do your save to DynamoDB, let the change data flow into DynamoDB Streams, process it in Lambda, then fire your domain events from the stream processor.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cw1nqzhdxgf27t7eosx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cw1nqzhdxgf27t7eosx.png" alt="Flowchart going from Lambda to DDB to stream handler" width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it comes down to it, this is legitimately better, and it's the same thought process behind &lt;a href="https://readysetcloud.io/blog/allen.helton/built-better-serverless-apis-by-going-storage-first" rel="noopener noreferrer"&gt;going storage-first&lt;/a&gt;. You persist it first, then process it asynchronously. That makes the database write the event trigger, so now your domain events are tightly coupled to the corresponding data change. Plus, DynamoDB Streams retries automatically (and &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-ddb-batchfailurereporting.html#streams-batchfailurereporting-bisect" rel="noopener noreferrer"&gt;even bisects batches&lt;/a&gt; when configured) if there's a processing failure.&lt;/p&gt;

&lt;p&gt;Sounds like our dual write problem is solved, right? Wrong. Kind of. It's complicated. 😅&lt;/p&gt;

&lt;p&gt;DynamoDB Streams records are change data capture, not domain events. The Lambda function you write to handle them receives a &lt;code&gt;NewImage&lt;/code&gt; and &lt;code&gt;OldImage&lt;/code&gt; record, and it's up to you to infer what happened upstream. Smells like leaky business logic to me. Before you know it, you have this thousand-line file that looks at 40 different data shapes to determine which event to publish and which transformation to perform. As someone who's found themselves in that hole before, believe me when I say that is a nightmare situation.&lt;/p&gt;

&lt;p&gt;Let's take it a step further. A business operation could (and probably does) affect multiple DynamoDB records. When I publish my newsletter, I create a DynamoDB record with a short TTL for each email address that was successfully sent to. If I am processing entity changes in a stream handler, I'd be up to my ears in duplicate &lt;code&gt;newsletter.sent&lt;/code&gt; events because I'm working on individual records and not an aggregate operation. That ultimately leads to workarounds with atomic counters and &lt;code&gt;lastAction&lt;/code&gt; flags that make it overly complex with little value add.&lt;/p&gt;

&lt;p&gt;And most importantly, you don't know what did or didn't publish. Streams &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html" rel="noopener noreferrer"&gt;retain data for 24 hours&lt;/a&gt;, and if your publisher was broken for a day because of an outage or a rogue IAM change, you're out of luck. Even if you opted for something like Kinesis, which has 365 days of data retention, that's not the point. These mechanisms are change data, not the intended effect on the system.&lt;/p&gt;

&lt;p&gt;DynamoDB Streams are a wonderful piece of engineering that gives you retries, but not the durability of intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you should do instead
&lt;/h2&gt;

&lt;p&gt;Time for a bit of a mental shift. We need to prioritize domain events. If an event is important enough to publish, it's important enough to persist in a database.&lt;/p&gt;

&lt;p&gt;This is where the &lt;em&gt;outbox pattern&lt;/em&gt; comes into play. Instead of deriving your domain events from the data change in a downstream handler, you save the event in an outbox table in a transaction along with the data mutation. The transaction guarantees both the domain event and the data change will save together (or neither).&lt;/p&gt;

&lt;p&gt;With DynamoDB, that could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ulid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TransactWriteItemsCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;TransactItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newsletter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;marshall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;issueId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;issue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;UpdateExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SET #status = :sent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;marshall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;issueId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`EVENT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;pendingBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;// GSI1 PK, removed on publish&lt;/span&gt;
          &lt;span class="na"&gt;pendingAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;   &lt;span class="c1"&gt;// GSI1 SK&lt;/span&gt;
          &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newsletter.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;detailType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newsletter.sent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;issueId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Funnily enough, the architecture doesn't change with this pattern. You're still doing Lambda (or whatever compute) to DynamoDB to a stream to another Lambda function. But this time the downstream Lambda function is processing the event you saved, not the mutated data. It doesn't have to infer anything.&lt;/p&gt;

&lt;p&gt;AWS does a fantastic job in their &lt;a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html" rel="noopener noreferrer"&gt;transactional outbox design pattern docs&lt;/a&gt; showing you how to setup and implement this pattern yourself. So I won't go into more of the code here. But I will address some questions I had the first time I built this.&lt;/p&gt;

&lt;h3&gt;
  
  
  What do I do with an event after it's published?
&lt;/h3&gt;

&lt;p&gt;After you publish an event in your stream handler, it's up to you to either delete it from the outbox or mark it as published. Personally, I prefer to mark it as published and set a 30 day TTL. This means in the stream handler, we're hitting the dual write problem again because we are both publishing an event and updating the outbox with a published status.&lt;/p&gt;

&lt;p&gt;This one is ok though 😉 and it brings up an important point we haven't covered yet. &lt;a href="https://readysetcloud.io/blog/allen.helton/api-essentials-idempotency/" rel="noopener noreferrer"&gt;Idempotency is crucial&lt;/a&gt; for success with this pattern. The outbox pattern trades duplicate delivery in exchange for never losing durable content. If your publisher fails to send the event or update the database with a published status, that's fine! Do it again. The important part is that the intent wasn't lost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I replace the stream handler function with EventBridge Pipes?
&lt;/h3&gt;

&lt;p&gt;Fewer pieces, easier system to maintain, right? In this instance, the Lambda function is the better solution over Pipes. If you were only publishing an event to EventBridge, you could easily filter the stream to the relevant outbox records and forward it straight to a bus. But because you also need to write back to DynamoDB to update the status of the event, the function wins. A pipe can only perform a single operation (outside of enrichments and transformations), but a function can perform many.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this guarantee ordering?
&lt;/h3&gt;

&lt;p&gt;Nope. The outbox pattern is designed for correctness only. Eventually consistent state across a distributed system. This does not tackle other hard distributed system problems like ordering or exactly-once delivery. Quite the opposite in fact. While Streams do process change data in the order they happen, it's per shard (not globally), and you can't guarantee ordering when it comes to retries. And as soon as you drop an event on EventBridge, you lose any sort of ordering.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does this guarantee events aren't skipped?
&lt;/h3&gt;

&lt;p&gt;If you implemented this exactly as described, it wouldn't. It gives you the normal retry path you're already used to, but still has that 24-hour clock because of the stream data retention. The important piece that's missing is a scheduled reconciler. It's usually another Lambda function that queries a sparse GSI for anything still pending from more than five minutes ago, which is long enough that the stream handler has had its shot, and republishes it. So your full flow would look something 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdrgbjmc04oh3ok4k2v0t.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdrgbjmc04oh3ok4k2v0t.png" alt="Chart showing the flow of the transactional outbox pattern" width="800" height="802"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Remember this
&lt;/h2&gt;

&lt;p&gt;There is no clever workaround to the dual write problem. You can't order the calls in a way that mitigates the risk, you can't retry around it, and you can't/shouldn't move it into a stream handler and hope.&lt;/p&gt;

&lt;p&gt;But you can stop having two writes. Well, two writes, but one transaction. 😅&lt;/p&gt;

&lt;p&gt;Remember, what we're solving with the outbox pattern is a correctness problem. If someone asks "&lt;em&gt;how can you guarantee the system is eventually consistent, even under backpressure?&lt;/em&gt;" this is what you reach for. It took me years to arrive here naturally and I've been accidentally building holes in my systems longer than I'd care to admit.&lt;/p&gt;

&lt;p&gt;This pattern does make your architecture more complicated, but the trade-offs are often worth it, especially at scale. In exchange for 2x the WCUs, you get a solid way to store and act upon intent in your system. Anyway, take a look at how you're publishing events and see if there are any unexplained missing events in your system. If there are, you know what to do. 😉 That's what I did and you should be reliably getting your newsletters from now on.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>distributedsystems</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>Send your events later</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Wed, 29 Jul 2026 20:26:23 +0000</pubDate>
      <link>https://dev.to/aws-heroes/send-your-events-later-25ha</link>
      <guid>https://dev.to/aws-heroes/send-your-events-later-25ha</guid>
      <description>&lt;p&gt;I build a lot of automated workflows. Across the suite of apps that powers Ready, Set, Cloud, I have a dozen or so mature, long-running automations that do some work, wait for something, then do more work. A great example is &lt;a href="https://readysetcloud.io/newsletter/sign-up" rel="noopener noreferrer"&gt;my newsletter&lt;/a&gt;. I often write it on Fridays and publish it on Mondays. After I open a PR for an issue, my backend service does some initial work, but it waits until Monday to send the emails and start analytics tracking.&lt;/p&gt;

&lt;p&gt;In the past, I've used the &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-wait.html" rel="noopener noreferrer"&gt;Step Functions &lt;code&gt;Wait&lt;/code&gt; state&lt;/a&gt; for this because it's the exact use case it was designed for. But as part of a big refactor I did to separate Ready, Set, Cloud logic from the generic newsletter logic, I needed to find a simpler way to schedule work - ideally as part of my CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;I thought using the AWS SDK in a script to create a one-time EventBridge schedule that published an event at a designated time was my best bet. But that brings in some implications with IAM and passing roles to the scheduler in order to invoke a &lt;code&gt;PutEvents&lt;/code&gt; call. It ended up being way too messy for a simple CI script.&lt;/p&gt;

&lt;p&gt;But I had an epiphany that immediately made me think, "&lt;em&gt;I should write about this&lt;/em&gt;" because it's very simple but not always obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The surprisingly-simple-yet-not-obvious solution
&lt;/h2&gt;

&lt;p&gt;I moved the EventBridge Scheduler event publish behind an EventBridge rule. Anything that can call &lt;code&gt;PutEvents&lt;/code&gt; can now ask for a deferred event.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpurnrtw8zfdvw8euvaia.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpurnrtw8zfdvw8euvaia.png" alt="Flowchart showing how EventBridge creates a schedule to defer publishing an event" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's essentially a one-time loop with a customizable wait step in it. Here's an example payload of an event to get my static site to rebuild because future-dated content &lt;a href="https://www.readysetcloud.io/blog/allen.helton/serverless-post-scheduler-for-static-sites/" rel="noopener noreferrer"&gt;doesn't render automatically&lt;/a&gt;.&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;"Source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"readysetcloud.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"DetailType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Schedule Event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Detail"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-03T09:00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timezone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"America/Chicago"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newsletter-rebuild-227"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"detailType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Trigger Site Rebuild"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"issueNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;227&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;span class="p"&gt;}&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;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;Publishing the above event will create a schedule that triggers at 9 a.m. CDT on August 3. The schedule will publish a &lt;code&gt;Trigger Site Rebuild&lt;/code&gt; event back onto the bus, which is wired up to a rule that kicks off a build in Amplify. Nifty, right?&lt;/p&gt;

&lt;h3&gt;
  
  
  Just a little bit of logic
&lt;/h3&gt;

&lt;p&gt;While I normally try to skip Lambda in this type of integration, I needed it for a couple of reasons. One, you can't set the EventBridge Scheduler as a &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-targets.html" rel="noopener noreferrer"&gt;target of a rule&lt;/a&gt; 🙃. And even if you could, I would have lost my ability to maintain schedules. If I needed to reschedule an event for a different time, I would have been out of luck.&lt;/p&gt;

&lt;p&gt;So I wrote a Lambda function as the target that will upsert the schedule, validate the event (&lt;em&gt;WHICH YOU SHOULD ALWAYS DO&lt;/em&gt;), and transform the provided time to the format the scheduler expects.&lt;/p&gt;

&lt;p&gt;As a bonus, the Lambda function also recognizes when the scheduled date is in the past, and will publish the event immediately or throw it away, depending on a parameter optionally provided in the event payload.&lt;/p&gt;

&lt;p&gt;So in this case, it made a lot of sense to add the function in the middle, which increases the complexity of the build, but the tradeoff is a richer experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build your own platform
&lt;/h2&gt;

&lt;p&gt;A few years ago, I made what I called my &lt;a href="https://readysetcloud.io/blog/allen.helton/automate-your-life-and-save-time-with-serverless-technology" rel="noopener noreferrer"&gt;&lt;em&gt;serverless toolbox&lt;/em&gt;&lt;/a&gt;, with some tools I could use across all of my apps that ran in the same AWS account. What I didn't know then was that I was essentially building a platform for all of my applications to run.&lt;/p&gt;

&lt;p&gt;I've since abandoned the toolbox and have turned my attention fully toward building a platform my apps run on. It's the peak of DRY (Don't Repeat Yourself) and in this case, is incredibly useful. I've added this scheduling primitive to &lt;a href="https://github.com/readysetcloud/rsc-core" rel="noopener noreferrer"&gt;my platform repo&lt;/a&gt; so it can be used everywhere - my newsletter service, blog service, and all the other services I've built that power my website.&lt;/p&gt;

&lt;p&gt;I only need to solve this problem once. Time and time again I've reimplemented things that worked in the past only to find myself with 4 versions of the same thing and at least one of them stale. So I make an intentional effort to consolidate the shared work and make the entry point to it as simple as possible (what's simpler than calling &lt;code&gt;PutEvents&lt;/code&gt;?!).&lt;/p&gt;

&lt;p&gt;Anyway, this is intentionally short and sweet. I was excited by the idea of delaying an event with nothing more than &lt;code&gt;PutEvents&lt;/code&gt;, and wanted to share. For the full source code, you can check out the &lt;a href="https://github.com/readysetcloud/rsc-core/blob/main/functions/schedule-event.mjs" rel="noopener noreferrer"&gt;function here&lt;/a&gt; or view the &lt;a href="https://github.com/readysetcloud/rsc-core/pull/223" rel="noopener noreferrer"&gt;entire PR&lt;/a&gt; to my platform app.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>PSA: That probably doesn't need to be SaaS</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Thu, 25 Jun 2026 21:35:21 +0000</pubDate>
      <link>https://dev.to/aws-heroes/psa-that-probably-doesnt-need-to-be-saas-d0j</link>
      <guid>https://dev.to/aws-heroes/psa-that-probably-doesnt-need-to-be-saas-d0j</guid>
      <description>&lt;p&gt;I've published a &lt;a href="https://readysetcloud.io/newsletter/sign-up" rel="noopener noreferrer"&gt;weekly newsletter every Monday&lt;/a&gt; for the past 4 years that showcases content from the tech community. The sole purpose of all 221 issues has been to highlight creators who educate the community and teach you a thing or two every week.&lt;/p&gt;

&lt;p&gt;Writing it for so long has tuned me into creators, content styles, trends, and outputs of the greater tech community. And lately it feels like thoughtful technical writing has become harder to find.&lt;/p&gt;

&lt;p&gt;Which is odd, I haven't seen this kind of behavior from the community before. But it hit me the other day as I was scrolling through LinkedIn. Everyone is launching solo-SaaS products. Hyper-specialized web apps built to solve a niche problem, now wrapped in pricing pages and marketed as a service.&lt;/p&gt;

&lt;p&gt;Developers are spending their time launching these micro-SaaS apps instead of writing about what they learn. We're seeing Product Hunt launches and marketing pages with colorful gradients where the blog posts used to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened?
&lt;/h2&gt;

&lt;p&gt;AI. AI happened.&lt;/p&gt;

&lt;p&gt;Specifically coding agents removed barriers preventing us from launching products in our free time. The time it would have taken to research, build, tinker, polish, rebuild, and polish some more has been replaced with a plain-text "&lt;em&gt;Make it do this&lt;/em&gt;" and walking away from our laptops (or cell phones 🤯).&lt;/p&gt;

&lt;p&gt;Don't get me wrong, I love how easy coding with AI agents is. But all-too-often I'm seeing developers be too trusting and building something they don't truly understand and moving on before lessons are absorbed.&lt;/p&gt;

&lt;p&gt;It's all very cool and I still have a hard time believing this is real life. I've had times where I'm on my phone using a website I built, notice something I don't like, open the Claude app and describe the change I want, and 5 minutes later merge the PR from the GitHub app and it's live.&lt;/p&gt;

&lt;p&gt;But it's having a ripple effect that might be hurting the tech community more than we realize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building like a SaaS isn't the same as running one
&lt;/h2&gt;

&lt;p&gt;Throughout my career, I've been tempted to turn one of my projects into a fully-functioning SaaS. Every time I start down that path, I am quickly reminded that I romanticize running my own business.&lt;/p&gt;

&lt;p&gt;Building like you're shipping a SaaS is one of the best things you can do for your career. Multi-tenancy as a design principle teaches you data isolation, auth, data modeling, and observability. All skills you need at your day job. You should absolutely build that way (please do!).&lt;/p&gt;

&lt;p&gt;Taking it to production is a whole different ballgame. You need a business strategy, marketing, pricing, support SLAs, and about 40 other things that aren't engineering related. The stuff that we as developers conveniently forget when we tell ourselves "&lt;em&gt;All I need is 100 users at $50 a month and I'm set&lt;/em&gt;." The stuff that takes ongoing commitment. Even on days you don't feel like supporting it, somebody else is depending on it.&lt;/p&gt;

&lt;p&gt;My friend &lt;a href="https://www.linkedin.com/in/eliasbrange/" rel="noopener noreferrer"&gt;Elias Brange&lt;/a&gt; is a fantastic engineer. The kind of dev who builds with care and intentionality. He launched a game called &lt;a href="https://playdailywars.com/" rel="noopener noreferrer"&gt;Daily Wars&lt;/a&gt; on Product Hunt last week and posted this after:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhd5sffgks6nrgurw4sfk.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhd5sffgks6nrgurw4sfk.png" alt="LinkedIn Post stating after a launch on Product Hunt, no users came" width="565" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Getting the code out the door was easy, thanks to AI. Getting someone to use it was the hard part.&lt;/p&gt;

&lt;p&gt;The moment you decide others will use your game/site/service/thing, the design changes too. You can't hardcode your preferences. You add configuration pages, customizable timers, a place to dynamically store credentials. That 10/10 solution for &lt;em&gt;your&lt;/em&gt; problem becomes maybe an 8/10 for everyone because it has to "just work" for use cases that aren't yours.&lt;/p&gt;

&lt;p&gt;Then (maybe) users start showing up. Ten of them. Heck, let's say fifteen. They have questions you didn't anticipate. One of them gets locked out at 2 AM on a Sunday and emails you about it. Someone else hammers your API with thousands of requests per second because of a bug in their code while integrating. You're checking your dashboard between meetings at your day job.&lt;/p&gt;

&lt;p&gt;And just like that your fun side project goes from a hobby to a job you didn't apply for.&lt;/p&gt;

&lt;p&gt;Remember Saturday mornings spent tinkering, trying to figure out how to center a div without asking AI? Those days are gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build for yourself
&lt;/h2&gt;

&lt;p&gt;Instead of trying to launch your first, second, or Nth micro-SaaS, why don't you build for yourself? Make it something that serves an audience of one (you) but build it in a way where you still get meaningful practice in software engineering.&lt;/p&gt;

&lt;p&gt;Open source the code. Keeping it open allows for a level of transparency that a fast-shipped SaaS app doesn't have. You're not signing up to maintain a project forever or triage issues from strangers. Treat it as a type of receipt. "&lt;em&gt;Here's where the lesson came from. Clone it if you want, but I'm not your support engineer 😅.&lt;/em&gt;"&lt;/p&gt;

&lt;p&gt;Then write about what you learned. Honestly, this is the part you learn from the most. Writing a blog post or creating a video that teaches something you learned the hard way forces you to really understand what you encountered. And the best part? It costs nothing to maintain. It doesn't page you on a Sunday morning. And it lands you back in the seat you used to sit in, as a peer in the community &lt;em&gt;instead of a vendor pitching to one&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not everything needs to make you money
&lt;/h2&gt;

&lt;p&gt;As a millennial, I grew up under the "monetize your passion" life advice and it's a conscious effort to &lt;em&gt;not&lt;/em&gt; do it. It's terrible advice. I remember the first time I heard the phrase &lt;em&gt;not everything you do needs to make you money&lt;/em&gt; and felt like a huge weight was lifted off my shoulders. Doing something for fun instead of doing it to earn beer money is liberating. With AI agents making the coding part of SaaS building trivial, it's harder than ever to simply build for the fun of it.&lt;/p&gt;

&lt;p&gt;I suggest a new mantra for developers for 2026: &lt;em&gt;not everything you build needs users.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The engineering you do for yourself still counts for something. The system you architect well still goes a long way. The hobby that produces nothing more than a blog post and an excited conversation is doing more for the community than the micro-SaaS that produces revenue from twelve strangers.&lt;/p&gt;

&lt;p&gt;The next time you finish a side project, write the post before you write the pricing page. You might find that it was the output you were after all along.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>saas</category>
      <category>community</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your AI agents are a security nightmare</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Fri, 27 Mar 2026 15:04:48 +0000</pubDate>
      <link>https://dev.to/allenheltondev/your-ai-agents-are-a-security-nightmare-4omp</link>
      <guid>https://dev.to/allenheltondev/your-ai-agents-are-a-security-nightmare-4omp</guid>
      <description>&lt;p&gt;I've noticed something about the tech industry over the past couple of months. Through the use of agentic code editors, developers are building small, custom solutions to problems they're having. Recently, there's been a surge in developers &lt;a href="https://www.readysetcloud.io/newsletter/206/" rel="noopener noreferrer"&gt;writing their own content platforms&lt;/a&gt;, abandoning bigger platforms like Hashnode and Medium. While I think this is genuinely amazing, we're creating a gap that we have to address sooner rather than later.&lt;/p&gt;

&lt;p&gt;That gap, of course, is maintenance. Even though AI coding is genuinely good now, new development is only a fraction of the software lifecycle. What happens when you get your app to prod and it suddenly breaks?&lt;/p&gt;

&lt;p&gt;Do you know the codebase like you did back when everything was hand coded? Would you even know where to begin troubleshooting if your API started returning a bunch of 500s?&lt;/p&gt;

&lt;p&gt;Let's assume the answer is yes to both of those. Now let me ask the hard-hitting one: &lt;em&gt;do you have time to troubleshoot and fix issues?&lt;/em&gt; This is a side project, not your day job. Many of us don't have the luxury of dropping the “real work” tasks for a few hours while we figure out what's going on.&lt;/p&gt;

&lt;p&gt;I'm no exception. I've been building &lt;a href="https://github.com/allenheltondev/community-garden" rel="noopener noreferrer"&gt;Good Roots Network&lt;/a&gt;, an app that connects local gardeners with people in their communities who need food. Growers list produce, gatherers like social workers and non-profits request it, and the system handles the coordination from availability through pickup. It's API-driven from the ground up. State machines, role-based access, real-time inventory transitions. Real production infrastructure running in my personal AWS account.&lt;/p&gt;

&lt;p&gt;There's a lot of surface area to cover for a solo project. And when something goes wrong, I don't have the bandwidth to dig in and figure out how to get it back on its feet - which means I'm potentially blocking people from getting access to fresh produce for themselves or others.&lt;/p&gt;

&lt;p&gt;So I built an agent to be on call.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before I continue, thank you to &lt;a href="https://fandf.co/4bBLhpV" rel="noopener noreferrer"&gt;Teleport&lt;/a&gt; for sponsoring this post. All opinions are my own.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an agent that runs locally
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/allenheltondev/oncall-agent" rel="noopener noreferrer"&gt;oncall-agent&lt;/a&gt; is a local, persistent process that automatically responds to production incidents. It subscribes to a Momento topic for real-time notifications and when something fires, it gets to work on its own. No need to wait for me to react.&lt;/p&gt;

&lt;p&gt;The agent has an internal state machine: receive the incident signal, authenticate against AWS, investigate, then post a summary to Slack. During investigation, the agent loop runs on Amazon Bedrock, using the AWS CLI to query CloudWatch logs and metrics, inspect Lambda functions and DynamoDB tables. It also has access to my source code and deployments via a GitHub app to diagnose root cause and open a PR with the recommended code change.&lt;/p&gt;

&lt;p&gt;I run it locally (rather than as a deployed service) for a couple of reasons. First, I specifically want it to make code changes if they're needed, and pulling the source into a Lambda function in my account doesn't seem like a great use of resources. Second, and more importantly, it touches production infrastructure directly. I want full control over the process, and I want the blast radius of any security issue limited to my machine, not a cloud-hosted process that's running 24/7.&lt;/p&gt;

&lt;p&gt;By the time I check Slack in the morning, the investigation and proposed solution is done and awaiting my approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the agent secure for a change
&lt;/h2&gt;

&lt;p&gt;It's tempting to just open up the doors on your machine and give the agent everything it needs to be successful (and then some). But as we've seen throughout 2026, &lt;a href="https://www.businessinsider.com/meta-ai-alignment-director-openclaw-email-deletion-2026-2" rel="noopener noreferrer"&gt;doing so is a security nightmare&lt;/a&gt;. I don't want to be another statistic who let an agent wreak havoc on my system because it was easy.&lt;/p&gt;

&lt;p&gt;When it came time to give the agent AWS access, my first instinct was to put an access key in the &lt;code&gt;.env&lt;/code&gt; file. Long-lived credentials. It's the familiar pattern, it takes 30 seconds, and it works. But the more I sat with that approach, the more it felt like I was solving the wrong problem.&lt;/p&gt;

&lt;p&gt;My issue with that approach was that the agent didn't really exist as an &lt;em&gt;identity&lt;/em&gt; in my system. It was just a privileged background process acting on my behalf with standing access.&lt;/p&gt;

&lt;p&gt;So, I reconsidered the agent as a first-class principal. If it was going to investigate production issues autonomously, it needed to prove who it was at runtime, operate within a scoped session, and leave behind a traceable chain of actions.&lt;/p&gt;

&lt;p&gt;Once I started thinking about the design this way, long-lived keys stopped making sense entirely. 👎&lt;/p&gt;

&lt;h3&gt;
  
  
  Identity is not the same thing as access
&lt;/h3&gt;

&lt;p&gt;With this in mind, I needed something more than a vault. I needed a way for the agent to establish its identity at runtime.&lt;/p&gt;

&lt;p&gt;That's where &lt;a href="https://fandf.co/3NOF8Ob" rel="noopener noreferrer"&gt;Teleport&lt;/a&gt; fits into the design. Instead of access tokens being defined in a config file, the agent starts every investigation by authenticating into a new session. From there, it can request scoped AWS access and perform specific actions with clear attribution.&lt;/p&gt;

&lt;p&gt;What I can now see in CloudTrail is a stable GUID that uniquely identifies the agent as its own principal. I can filter to that identity and see everything the agent has ever touched.&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%2F80g0lcsissbvi64q7xuh.jpg" 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%2F80g0lcsissbvi64q7xuh.jpg" alt="Amazon CloudTrail logs showing a unique identifier for actions taken" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If something unexpected shows up, I know immediately it was the agent, and not a human or another process. That's already a huge improvement over a shared access key where attribution is basically impossible.&lt;/p&gt;

&lt;p&gt;Now, I have a legitimate session-bound identity operating on my system. Something that only has the permissions I gave it, but more importantly, is independently auditable.&lt;/p&gt;

&lt;p&gt;When the agent runs &lt;code&gt;tsh login&lt;/code&gt;, I set it up so I have to approve the MFA challenge myself. Fully autonomous credential rotation via &lt;a href="https://fandf.co/40GRJ92" rel="noopener noreferrer"&gt;Teleport Machine ID&lt;/a&gt; is available–and it's the natural next step–but I'm not there yet. I want a few more practice reps with the agent before I remove myself from the auth flow entirely. &lt;a href="https://www.readysetcloud.io/blog/allen.helton/trust-will-make-or-break-ai-agents/" rel="noopener noreferrer"&gt;Trust has to be earned&lt;/a&gt;, and I'd rather manually approve an MFA prompt than wake up to an agent that went sideways with no human checkpoint anywhere in the chain.&lt;/p&gt;

&lt;p&gt;Agent security posture is something I'm really honing in on. I'm changing my mental approach from "does this agent have the right key?" to "was this agent authorized to do this specific thing, in this specific context, at this specific moment?" It might feel like a subtle difference, but it's how we need to be thinking before we give agents the keys to our production systems. And a static credential sitting in an env file isn't the way to answer it.&lt;/p&gt;

&lt;h2&gt;
  
  
  This isn't a side project problem
&lt;/h2&gt;

&lt;p&gt;The most common pattern I see these days is the same one I almost fell into: drop a long-lived token into the environment, grant it the permissions it needs, and ship it. Even if you do it as a “temporary stop gap” and swear you'll come back to fix it, &lt;a href="https://www.linkedin.com/posts/allenheltondev_weve-all-shipped-a-temporary-fix-we-planned-activity-7432825143221108736-ag0U?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAArWvkYBD1-hWpf7w_0jiGdn8x3RQeihlmc" rel="noopener noreferrer"&gt;you won't&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It always feels like it's fine until something unexpected happens. The problem with unexpected behavior in autonomous systems is that you need &lt;em&gt;more&lt;/em&gt; audit resolution, not less. Imagine an agent made unusual calls to your production services. Was it a bug? A prompt injection? A legitimate investigation path you just didn't anticipate? With static credentials you can't tell. The trail goes cold at "this IAM principal made these calls."&lt;/p&gt;

&lt;p&gt;Agentic AI is exposing some serious issues with the access model many of us are running. IAM was originally designed around humans doing episodic work. Even automated pipelines are episodic in that they run, finish, and stop. But that doesn't map cleanly to processes that persist indefinitely, reason autonomously, and act faster than the blink of an eye.&lt;/p&gt;

&lt;p&gt;When I think about what agents need to operate safely, I land on a few things. They need a cryptographic identity of their own, meaning something that can be uniquely identified (much like an individual developer). They need access that's scoped and enforced at runtime–not granted once like an API key, and left open. They need some way to detect when they're behaving outside expected parameters, or when the operating context has been tampered with. And if they're coordinating with other agents or MCP servers, those connections need to be governed, too (you can't have a secured agent talking to an unsecured tool).&lt;/p&gt;

&lt;p&gt;These are all things Teleport is building with their &lt;a href="https://fandf.co/4lHOZSu" rel="noopener noreferrer"&gt;Agentic Identity Framework&lt;/a&gt;. My agent only scratches the surface of all this with its short-lived credentials and isolated identity. But starting there forces you to think about the agent as a principal that needs its own security model, which is the right mental shift we need heading into mid-2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  This probably sounds a little familiar
&lt;/h2&gt;

&lt;p&gt;We've all seen this play out before in different forms. When containerization became mainstream, teams that had good practices with VM security often skipped the basics on containers because it felt like a "smaller" problem. When Lambda came out, teams that carefully managed server permissions dropped service roles for wildcard policies because it was a new execution model and they were moving fast (hopefully I'm not the only one who did this 😬).&lt;/p&gt;

&lt;p&gt;Agentic AI is the same thing happening again. A new execution model, a lot of excitement, and a temptation to skip the identity and access fundamentals–because it feels like a side project, a POC, or just something you're trying out.&lt;/p&gt;

&lt;p&gt;The lesson is the same every time: the important components in production are still important even if the thing running is new, or small, or experimental. An agent with standing access and no attribution is a liability, whether it's running inside a Fortune 500 or on your laptop handling incidents for a community gardening app.&lt;/p&gt;

&lt;p&gt;The oncall-agent repo is &lt;a href="https://github.com/allenheltondev/oncall-agent" rel="noopener noreferrer"&gt;open source&lt;/a&gt;. The Teleport integration is in the state machine if you want to see how it fits together in practice. And if you're experimenting with agents at all, it's worth triple-checking how they establish identity and access production systems. Move a small workflow to session-bound credentials to help you make better architectural decisions and make your automations easier to trust.&lt;/p&gt;

&lt;p&gt;Tools like &lt;a href="https://fandf.co/4bBLhpV" rel="noopener noreferrer"&gt;Teleport&lt;/a&gt; make that change practical without completely reworking your stack. It's a small change in implementation, but a big change in how much autonomy you're willing to give your systems.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
    </item>
    <item>
      <title>When serving images from S3 stopped being good enough</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Wed, 07 Jan 2026 15:12:39 +0000</pubDate>
      <link>https://dev.to/aws-heroes/when-serving-images-from-s3-stopped-being-good-enough-233f</link>
      <guid>https://dev.to/aws-heroes/when-serving-images-from-s3-stopped-being-good-enough-233f</guid>
      <description>&lt;p&gt;I published my first blog post 7 years ago. I wrote on &lt;a href="https://medium.com/@allenheltondev" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; for about a year before I built &lt;a href="https://readysetcloud.io" rel="noopener noreferrer"&gt;Ready, Set, Cloud&lt;/a&gt;. For most of its life, the site hasn't had much of a facelift or performance updates. It's primarily served as a home for my writing, newsletter, and podcast.&lt;/p&gt;

&lt;p&gt;I've been running this site for six years. I'm usually the kind of person who can't leave things alone for long, yet this has been largely unchanged for years. It worked. Until it didn't.&lt;/p&gt;

&lt;p&gt;When I finally took a look at site performance, something was painfully obvious. Images. &lt;a href="https://pagespeed.web.dev/" rel="noopener noreferrer"&gt;PageSpeed Insights&lt;/a&gt; showed that large, unoptimized images were dominating load time. Single file sizes served directly from S3, no format negotiation, and no caching were starting to add up.&lt;/p&gt;

&lt;p&gt;That setup wasn't wrong when I built it. It was a perfectly reasonable tradeoff at the time. It was simple, low maintenance, and honestly, &lt;em&gt;good enough&lt;/em&gt;. The site had a couple dozen readers, and small-scale simplicity won. But as the site grew, both in content and audience, performance expectations changed. Serving raw images straight from S3 no longer cut it. The site outgrew its initial build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I needed from image delivery
&lt;/h2&gt;

&lt;p&gt;The knee-jerk reaction is to jump into S3 and manually optimize a bunch of images. That fixes a symptom, but it doesn't solve the underlying problem. I needed to step back and rethink what image delivery should look like for a content-heavy site serving tens of thousands of monthly visitors.&lt;/p&gt;

&lt;p&gt;Manually resizing images, converting them to web-friendly formats, and deciding which size to upload per post was never going to scale. More importantly, I didn't want to change my workflow. I wanted to continue to write, publish, and move on without adding in layers of performance checking.&lt;/p&gt;

&lt;p&gt;A &lt;em&gt;good&lt;/em&gt; system should make that possible. It should optimize images automatically in the background. It should serve modern formats like &lt;a href="https://developers.google.com/speed/webp" rel="noopener noreferrer"&gt;WebP&lt;/a&gt; when the browser supports them. It should provide multiple image sizes so mobile devices aren't downloading desktop-sized assets. And it should be aggressively cacheable, because image delivery is a solved problem, and CDNs already do this exceptionally well.&lt;/p&gt;

&lt;p&gt;Thinking about these requirements, I realized I wasn't really looking to build a new image optimization tool (like I would have done in the past). Instead, I was updating the site's image handling capabilities so these decisions were made once, centrally, in an industry-standard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fitting a CDN into the workflow
&lt;/h2&gt;

&lt;p&gt;I upload images to Ready, Set, Cloud using a small JavaScript script that lives in the repo. It takes a file prefix, scans a local folder for matching files, and uploads them directly to S3. There's no web UI and no automation that tries to interpret content. It's simple by design, and I wanted to keep it that way.&lt;/p&gt;

&lt;p&gt;That simplicity turns out to be a feature. Because images already flow through S3, the system can react to uploads without changing the workflow at all.&lt;/p&gt;

&lt;p&gt;Image uploads automatically trigger a Rust Lambda function (&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html" rel="noopener noreferrer"&gt;through EventBridge&lt;/a&gt;) that converts the original image to WebP and generates a handful of standard sizes. Those optimized versions are written back to S3 alongside the original.&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%2Ftply36ibqknjhipcvlmz.webp" 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%2Ftply36ibqknjhipcvlmz.webp" alt="Small architecture diagram showing the new workflow" width="718" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This does a few important things. First, it makes image optimization deterministic - meaning every image goes through the same process every time. Second, it keeps work off the critical path. All of this happens asynchronously and doesn't interfere with the rest of the publishing workflow like &lt;a href="///blog/allen.helton/how-i-built-a-serverless-automation-to-cross-post-my-blogs"&gt;cross-posting&lt;/a&gt;, &lt;a href="///blog/allen.helton/blog-level-up-writer-analytics-and-text-to-speech"&gt;writing analytics&lt;/a&gt;, or &lt;a href="///blog/allen.helton/serverless-post-scheduler-for-static-sites"&gt;scheduling&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, I added a CloudFront distribution in front of the S3 bucket. That keeps delivery fast and predictable by serving cached assets from points of presence around the world. Because it points at the existing bucket, there's no data migration involved. Image delivery improves without changing where anything lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serving the right format without changing URLs
&lt;/h2&gt;

&lt;p&gt;Once image processing was handled, delivery became the next concern. All optimized assets were already in S3, but I wasn't interested in changing 2,000+ image links across existing content just to take advantage of them.&lt;/p&gt;

&lt;p&gt;The answer was to push that behavior into the CDN. By updating the distribution to look for WebP support in the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept" rel="noopener noreferrer"&gt;&lt;code&gt;Accept&lt;/code&gt; header&lt;/a&gt;, requests for images could be routed to the optimized versions without changing a single URL. Modern browsers already advertise their supported formats, so content negotiation becomes a routing concern rather than a frontend one.&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%2F7ozogejfrmitgmkptahr.webp" 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%2F7ozogejfrmitgmkptahr.webp" alt="Small architecture diagram showing how the Accept header works with CloudFront functions" width="800" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A CloudFront function runs on the &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html" rel="noopener noreferrer"&gt;viewer request event&lt;/a&gt; and rewrites the request path whenever the browser advertises WebP support. It doesn't check whether the WebP file exists - that's resolved later when CloudFront fetches from the origin (the S3 bucket). The important part is that the rewrite happens consistently at the edge, and the resulting response is cached, so subsequent requests follow the same path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Letting the browser choose the right size
&lt;/h3&gt;

&lt;p&gt;Using WebP was only one part of the solution. Once multiple sizes of every image existed automatically, the next question was which one to serve. Mobile screens don't need desktop-sized images, and high-DPI displays can benefit from larger ones. Not to mention saving on load times for thumbnails on the home page.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;srcset&lt;/code&gt; allows the browser to make that choice on its own. The markup stays the same, there's no runtime logic, and each client downloads only what it actually needs. The responsibility for choosing the right size moves to the client, where that decision can be made with full context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/images/diagram.png"&lt;/span&gt;
  &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"
    /images/diagram-480.webp 480w,
    /images/diagram-960.webp 960w,
    /images/diagram-1600.webp 1600w"&lt;/span&gt;
  &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 768px) 100vw, 768px"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ready, Set, Cloud is a static site generated by &lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt;. In my case, adding &lt;code&gt;srcset&lt;/code&gt; support meant overriding the &lt;a href="https://gohugo.io/render-hooks/images/" rel="noopener noreferrer"&gt;render-image hook&lt;/a&gt; and adding a small amount of logic to create the additional attribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance improvements and an unexpected bonus
&lt;/h2&gt;

&lt;p&gt;My primary goal with all this was to decrease load times so my site felt snappy without adding anything to my existing workflow. With the image optimization component plus the CDN for cached delivery (and the client-side &lt;code&gt;srcset&lt;/code&gt; addition), I'm pleased to say page payloads dropped significantly. Pages render faster, Largest Contentful Paint (LCP) improved, and overall performance is more predictable.&lt;/p&gt;

&lt;p&gt;On my homepage, the First Contentful Paint (FCP) dropped from &lt;em&gt;4.5 seconds to under a second&lt;/em&gt;. Load times are much more consistent across desktop and mobile as well, which has been an ongoing challenge.&lt;/p&gt;

&lt;p&gt;The unexpected bonus was that these same changes also helped with search engine rankings. Faster pages, smaller downloads, and aggressively cached assets all feed into &lt;a href="https://developers.google.com/search/docs/appearance/core-web-vitals" rel="noopener noreferrer"&gt;Core Web Vitals&lt;/a&gt;. So without targeting SEO explicitly, the site became easier to crawl, faster to index, and rank higher in search results simply by prioritizing user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making this easy to reuse
&lt;/h2&gt;

&lt;p&gt;If you've built your own blog, you've probably run into these performance bottlenecks just like I have, and that's okay. Serving assets out of S3 is a valid solution and has worked well for me for six years as the site grew.&lt;/p&gt;

&lt;p&gt;When my constraints changed, I wanted to extend the system without changing the deployment workflow. If you're interested in the same improvements I've described here, this setup is available in the &lt;a href="https://serverlessrepo.aws.amazon.com/applications/us-east-1/745159065988/image-optimizer-for-blogs" rel="noopener noreferrer"&gt;Serverless Application Repository&lt;/a&gt; as a drop-in addition to an existing system. The full source is also &lt;a href="https://github.com/allenheltondev/image-downscaler" rel="noopener noreferrer"&gt;available in GitHub&lt;/a&gt; if you want to dig into the details or adapt it further.&lt;/p&gt;

&lt;p&gt;Above all else, this was about taking an entire class of decisions around image formats, sizes, and caching, and keeping them out of the day-to-day workflow. With everything in place, performance simply happens by default.&lt;/p&gt;

&lt;p&gt;This was as fun as it was important for Ready, Set, Cloud. I wanted something easy to adopt, easy to remove, and something that quietly does the right thing as everything else moves around it.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>blogging</category>
    </item>
    <item>
      <title>The Real Cost of Swapping Infrastructure</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Mon, 15 Dec 2025 20:43:40 +0000</pubDate>
      <link>https://dev.to/allenheltondev/the-real-cost-of-swapping-infrastructure-3a73</link>
      <guid>https://dev.to/allenheltondev/the-real-cost-of-swapping-infrastructure-3a73</guid>
      <description>&lt;p&gt;I've gone through enough infrastructure evaluations as an architect to recognize the moment when the energy leaves the room. It's not when someone questions the performance numbers or the cost model. It's when someone pulls up the codebase and starts counting how many services need to change.&lt;/p&gt;

&lt;p&gt;The infrastructure might be more reliable, easier to operate, or have better economics, but it doesn't matter if getting there means touching stable production code across dozens of services. The conversation shifts from "should we do this?" to "can we afford to do this?" and the answer is usually no.&lt;/p&gt;

&lt;p&gt;That gap between "this is better" and "we can actually adopt this" is where many decisions stall or get turned down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cost of infrastructure change
&lt;/h2&gt;

&lt;p&gt;Architecture discussions tend to follow a familiar pattern. The whiteboard fills up with boxes and arrows, the tradeoffs look reasonable, everyone agrees that you'll come out the other end better. Then someone asks: how much code do we have to touch?&lt;/p&gt;

&lt;p&gt;That question isn't about features or benchmarks. &lt;em&gt;It's about risk&lt;/em&gt;. Architects evaluate the blast radius of change alongside performance and reliability. Every line of application code that needs to move, every client library that needs to be swapped, every behavior that needs to be re-learned increases the cost before you can even run a proof of concept.&lt;/p&gt;

&lt;p&gt;For systems already in production, touching stable code introduces uncertainty. It stretches review cycles, kicks off regression testing, and makes rollback complicated. Good ideas often don't make it past this point because weaving them into existing applications costs too much.&lt;/p&gt;

&lt;p&gt;This is especially relevant for infrastructure on the hot path. When caching misbehaves, it takes other systems down with it. Teams are rightfully cautious about changes here, even when the infrastructure side of the proposal is compelling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What teams actually trust
&lt;/h2&gt;

&lt;p&gt;Teams trust behavior they've observed in production, like how commands serialize, how errors surface, or how retries behave under load. That behavior has been exercised millions of times. It's hardened by real traffic, load testing, and years of incremental fixes. In practice, this behavior acts as a contract between the app code and infrastructure.&lt;/p&gt;

&lt;p&gt;This is why client changes feel expensive even when two libraries look similar on the surface. Timeouts, connection handling, pipelining behavior, and edge cases around failures all shape how systems respond when stressed. At scale, subtle differences show up as tail latency spikes or incident tickets that are hard to explain.&lt;/p&gt;

&lt;p&gt;For cache-heavy systems built on Redis or Valkey, this contract is often the wire protocol itself – &lt;a href="https://docs.momentohq.com/cache/resp" rel="noopener noreferrer"&gt;RESP&lt;/a&gt;, the wire format the client already speaks. The application doesn't depend on “a cache,” it depends on this specific way of talking to one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing what sits behind the contract
&lt;/h2&gt;

&lt;p&gt;When you hold the contract constant and change what sits behind it, the risk potential drops dramatically.&lt;/p&gt;

&lt;p&gt;Instead of rewriting cache layers or swapping SDKs across services, teams can &lt;a href="https://docs.momentohq.com/cache/resp#code-it-your-way" rel="noopener noreferrer"&gt;point existing Redis or Valkey clients at Momento&lt;/a&gt;, authenticate, and issue the same commands they already use. The infrastructure changes. The operational model changes. The application code largely does not.&lt;/p&gt;

&lt;p&gt;That distinction turns evaluation from a refactor into a configuration change. It lets teams observe real production behavior without committing to a rewrite upfront. More importantly, it makes rollbacks boring. Simply change an endpoint back and that's the extent of it.&lt;/p&gt;

&lt;p&gt;This doesn't eliminate &lt;em&gt;all&lt;/em&gt; risk. RESP compatibility has edges and limitations worth understanding. Not every Redis command is supported, but it shifts the evaluation risk from application code to infrastructure, where it's far easier to observe and reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lowering the cost of evaluation
&lt;/h2&gt;

&lt;p&gt;I've noticed the infrastructure platforms that gain real adoption share a common trait: they meet teams where they already are. They respect existing contracts, existing mental models, and the realities of systems that have been running in production for years.&lt;/p&gt;

&lt;p&gt;"Better" isn't enough if getting there requires destabilizing code no one wants to touch. The platforms that succeed make it easy to start small, observe real behavior, and back out safely when something doesn't line up. When evaluation feels reversible, teams engage honestly with the tradeoffs instead of inventing reasons to stay put.&lt;/p&gt;

&lt;p&gt;RESP compatibility fits this philosophy. It doesn't ask teams to abandon the clients or patterns they rely on. It allows them to keep the contract they trust while changing the parts that benefit most from being managed: scaling, availability, and operational complexity.&lt;/p&gt;

&lt;p&gt;In practice, that's often what separates interesting technology from technology that actually gets adopted.&lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>architecture</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>Breakthroughs are just boring improvements that pile up</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Mon, 24 Nov 2025 21:04:05 +0000</pubDate>
      <link>https://dev.to/allenheltondev/breakthroughs-are-just-boring-improvements-that-pile-up-299h</link>
      <guid>https://dev.to/allenheltondev/breakthroughs-are-just-boring-improvements-that-pile-up-299h</guid>
      <description>&lt;p&gt;We romanticize big engineering wins.&lt;/p&gt;

&lt;p&gt;The performance chart that hockey-sticks up. The keynote slide with the impossible number. The "we hit a billion requests per second" humble brag.&lt;/p&gt;

&lt;p&gt;The truth behind those moments is never one big breakthrough. It's dozens, possibly even hundreds, of small changes and enhancements. The kind that look ordinary when they land in a PR. The kind that your eyes casually skip over in release notes. The kind nobody celebrates… until they compound into something revolutionary.&lt;/p&gt;

&lt;p&gt;I was recently reminded of this while listening to the &lt;a href="https://youtu.be/c57lAopUh0k" rel="noopener noreferrer"&gt;&lt;em&gt;Cache It&lt;/em&gt; podcast&lt;/a&gt;. Valkey project maintainer &lt;a href="https://www.linkedin.com/in/harkrishn-patro/" rel="noopener noreferrer"&gt;Harkrishn Patro&lt;/a&gt; described how his team scaled their system to &lt;a href="https://valkey.io/blog/1-billion-rps/" rel="noopener noreferrer"&gt;handle a billion requests per second&lt;/a&gt;. He wasn't describing a feature launch, but rather the result of continuous refinement across every corner of the system.&lt;/p&gt;

&lt;p&gt;Engineers, myself included, often hone in on small pieces of a system and lose sight of the forest for the trees. But with bold headlines and sensational marketing, we forget the forest is made from trees. As a result, major innovations overshadow the hard work that was done in the years and months leading up to "the big reveal."&lt;/p&gt;

&lt;p&gt;So what happens if we take a stroll through that forest to see what the big announcement was &lt;em&gt;really&lt;/em&gt; about?&lt;/p&gt;

&lt;h2&gt;
  
  
  Innovation removes friction
&lt;/h2&gt;

&lt;p&gt;We tend to think innovation shows up wearing a cape. A disruptive new framework. A patented algorithm. A major architectural redesign. But most real innovation is far less glamorous – it's the process of shaving off tiny sources of drag until the entire system starts to glide. The "death by a thousand paper cuts" phenomenon is real.&lt;/p&gt;

&lt;p&gt;Distributed systems make this painfully clear. The moment you push scale beyond "normal," you reveal dozens of inefficiencies that rarely show up in happy-path benchmarks. And this doesn't just refer to hyper scale! Problems change at every tier of scale. From POC to startup. From startup to regional. From regional to global. Suddenly, small issues aren't small anymore, they're multiplied across thousands of functions, nodes, and containers processing millions of requests per second.&lt;/p&gt;

&lt;p&gt;That's exactly what the Valkey team discovered while pushing cluster sizes past limits that were never originally intended. Instead of rewriting the system from scratch, they focused on identifying and removing friction one piece at a time. For example, early pub/sub workloads broadcast messages to every node in the cluster, regardless of who needed the data. Reducing that via &lt;a href="https://valkey.io/topics/pubsub/#sharded-pub-sub" rel="noopener noreferrer"&gt;sharded messaging&lt;/a&gt; meant drastically less traffic flowing through the cluster bus,  which freed up capacity for the work that mattered.&lt;/p&gt;

&lt;p&gt;Or the size and frequency of &lt;a href="https://valkey.io/topics/cluster-spec/#heartbeat-packet-content" rel="noopener noreferrer"&gt;internal coordination messages&lt;/a&gt;. By making those packets smaller and more efficient, Valkey stopped wasting cycles on communication overhead, freeing up capacity for serving actual requests.&lt;/p&gt;

&lt;p&gt;Individually, neither of these changes would get a developer on stage at a conference. Nobody ships a "lighter gossip protocol!" feature release. Stack those improvements long enough, and the system starts performing at a level that once felt impossible.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Innovation isn't about adding more. It's about taking away.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you remove enough drag, the system suddenly feels like it's leaping forward… even though it got there one subtle change at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability is a force multiplier
&lt;/h2&gt;

&lt;p&gt;A fast system under perfect conditions is just a prototype. The moment you introduce chaos, like what happens regularly in production, performance either collapses or compounds. Reliability determines which way it goes.&lt;/p&gt;

&lt;p&gt;Just like removing friction creates capacity, improving reliability creates opportunity. In distributed systems, fragility multiplies just as quickly as performance. One node degrades, then another, and suddenly a perfectly healthy cluster wastes capacity on recovery instead of serving requests. Failover becomes a performance bottleneck waiting to happen if handled poorly.&lt;/p&gt;

&lt;p&gt;Back to the small wins with Valkey – when multiple primaries failed simultaneously (like during an Availability Zone outage), the system would struggle to elect new leaders because candidate requests flooded in all at once. Leadership battles caused downstream stalls that made the whole cluster feel unstable under pressure. The solution was introducing strategic delays to election behavior, allowing replicas to take over cleanly and confidently without overwhelming the system. &lt;/p&gt;

&lt;p&gt;But that's the pattern – resilience opens up throughput. A cluster that can route around failure without spiking CPU or tail latency stays fast when users need it most. In Valkey's case, the additional throughput in unfavorable situations kept cycles free to handle more requests per second. Another blip on the release notes that continued to add up to the breakthrough.&lt;/p&gt;

&lt;p&gt;Reliability amplifies every other improvement in the system. It creates breathing room. It enables aggressive scaling. It gives teams the confidence to grow beyond today's limits. When recovery becomes routine and uneventful, progress accelerates. &lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling is an operational behavior
&lt;/h2&gt;

&lt;p&gt;Contrary to what you might think, scaling isn't a hardware problem. It's a behavior problem. If changing scale feels risky, disruptive, or expensive, teams simply don't do it – even when they need to. They take the safe route: over-provisioning up front and praying they don't get surprised later.&lt;/p&gt;

&lt;p&gt;There's nothing innovative about survival mode.&lt;/p&gt;

&lt;p&gt;Real scalability emerges when the cost of adjusting capacity falls so low that it becomes a non-event (that's one of the reasons people like serverless so much). The faster and safer adding nodes, redistributing data, or shifting workload patterns feel, the more frequently teams perform them. And the more frequently teams perform them, the closer infrastructure can track real-world demand without waste, stress, and cost.&lt;/p&gt;

&lt;p&gt;Once again, the Valkey team learned this firsthand. Since Redis 1.0, data migration has been a scary operation. Moving slots between nodes meant risking performance hits, stalled operations, or complicated (and error prone) manual intervention. So people avoided it, and clusters either stayed small or stayed oversized. Innovation would stall because operating it at its potential felt like walking a tightrope.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gomomento.com/blog/cache-rebalancing-was-broken-heres-how-valkey-9-0-fixed-it/" rel="noopener noreferrer"&gt;Atomic slot migration&lt;/a&gt; changed the behavior. Now, moving data around a cluster is predictable, controlled, and boring – exactly what operators need when managing production systems. This feature opened up capabilities that already existed but weren't safe to reach for.&lt;/p&gt;

&lt;p&gt;When scaling becomes effortless, teams stop treating it like a last resort and start doing it as a reflex. Progress begins to compound. Capacity and confidence reinforce one another. And suddenly, what once felt like a milestone becomes just another Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breakthroughs happen gradually… then suddenly
&lt;/h2&gt;

&lt;p&gt;From the outside, breakthroughs look like a leap. One day the headline appears. The graph turns vertical. Everyone wonders how a system could possibly jump from "pretty good" to "borderline unbelievable" in such a short time.&lt;/p&gt;

&lt;p&gt;But from the inside, it looks like months, sometimes years, of sanding down rough edges. Fixing the tiny things that don't feel worthy of a celebration. Optimizing behavior that only shows up in profiling. Eliminating friction one pull request at a time.&lt;/p&gt;

&lt;p&gt;That's exactly what happened with Valkey. A billion RPS was never the goal, but it was the receipt of countless invisible improvements. The milestone only looks magical because we didn't see the work it took to make it boring.&lt;/p&gt;

&lt;p&gt;And this isn't unique to distributed systems or caching engines. It's universal. Every major engineering accomplishment is built from the moments where someone looked at a sharp corner and decided it didn't have to stay sharp.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Progress compounds. Confidence compounds. Capability compounds. Breakthroughs happen gradually… then suddenly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So if you're in the middle of the grind, whether you're shipping incremental changes, cleaning up edge cases, or improving reliability and operability in small ways, take a breath of solace. You're stacking. You're creating the conditions where something extraordinary is going to look effortless.&lt;/p&gt;

&lt;p&gt;The day is coming when the big announcement arrives, the system holds steady, and people assume it was easy. But you'll know better. You'll know it was earned one small improvement at a time.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>innovation</category>
    </item>
    <item>
      <title>Cache Rebalancing Was Broken. Here's How They Fixed It.</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Tue, 18 Nov 2025 22:16:54 +0000</pubDate>
      <link>https://dev.to/allenheltondev/cache-rebalancing-was-broken-heres-how-they-fixed-it-hh5</link>
      <guid>https://dev.to/allenheltondev/cache-rebalancing-was-broken-heres-how-they-fixed-it-hh5</guid>
      <description>&lt;p&gt;Few things make SREs more nervous than rebalancing a cache cluster.&lt;/p&gt;

&lt;p&gt;You know the feeling. You add a node, trigger a rebalance, and suddenly latency graphs start jumping. It's a familiar risk of the job, especially when your cache sits between your users and your database. A small configuration mistake here can suddenly unleash a storm of GET requests on your primary data store.&lt;/p&gt;

&lt;p&gt;I admit, I never really understood the concept of a slot or why it was needed. But after listening to a recent episode of the Cache It podcast on the new &lt;a href="https://www.youtube.com/watch?v=q5L2oW3YRZQ" rel="noopener noreferrer"&gt;atomic slot migration feature in Valkey 9.0&lt;/a&gt;, I finally decided to dig in. The deeper I went (and the more times I replayed the episode), the more it clicked.&lt;/p&gt;

&lt;p&gt;My learning adventure led me to ask, and finally answer, four important questions regarding slots and what happens to them when a cluster resizes. Let's dive in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are slots?
&lt;/h2&gt;

&lt;p&gt;Most caching systems rely on consistent hashing to decide where data lives. It keeps keys evenly balanced across nodes while allowing clusters to grow or shrink with minimal reshuffling.&lt;/p&gt;

&lt;p&gt;Valkey uses a fixed hash-slot model, specifically 16,384 slots, that together represent the entire keyspace. While 16,384 slots might seem random, it's actually 2¹⁴, a power of two chosen because it balances just the right amount of precision and efficiency. With that many slots, data can be distributed across large clusters without adding unnecessary overhead to routing or metadata.&lt;/p&gt;

&lt;p&gt;Every key hashes to one of those slots, and each node in a cluster owns a subset of them. Rather than mapping every key directly to a node, Valkey maps slots to nodes. Since keys are deterministically hashed to slots, this makes scaling predictable. When you add or remove a node, Valkey only has to move the slots it owns, not millions of individual keys.&lt;/p&gt;

&lt;p&gt;When the Valkey client library hashes a key, it already knows which node owns the corresponding slot. If the topology changes (aka slots being assigned to a different node due to a scaling event), Valkey issues a quick redirect so the client can retry against the correct node.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was broken about the old migration model?
&lt;/h2&gt;

&lt;p&gt;The old migration model moved one key at a time between nodes, triggering a flurry of redirects and topology changes. Clients were constantly told, "&lt;em&gt;Sorry, this key moved, try over there.&lt;/em&gt;"&lt;/p&gt;

&lt;p&gt;It was essentially a brute-force way of moving slots from one node to another. It worked, but it wasn't elegant – and it definitely wasn't fast.&lt;/p&gt;

&lt;p&gt;Each key transfer required multiple round trips, and every slot migration forced clients to refresh cluster topology. Large values could even block threads during serialization.&lt;/p&gt;

&lt;p&gt;When you're working with millions of keys across your cluster, that adds up to a resource-intensive process that can take minutes to complete, all while the cluster remains live and serving traffic.&lt;/p&gt;

&lt;p&gt;The result was instability and slower tail latencies during migration. Which means it was something you'd postpone unless you absolutely had to run it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does atomic slot migration fix it?
&lt;/h2&gt;

&lt;p&gt;Valkey 9.0 redesigned the slot migration process using the same principles that power replication. Instead of moving keys one by one, atomic slot migration runs in three distinct phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot&lt;/strong&gt; – The source node forks a background process and captures a point-in-time snapshot of the slots being migrated while continuing to serve live traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming&lt;/strong&gt; – Any writes that happen during the snapshot are captured in a buffer and streamed incrementally to the target node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finalization&lt;/strong&gt; – Once all data is synchronized, Valkey briefly pauses new writes, sends a final marker, and performs a single, atomic handover.&lt;/p&gt;

&lt;p&gt;This three-phase approach eliminates the multiple round trips, client redirects, and fragile user experience that used to come with slot migration. Because the process runs in the background and atomically switches ownership when complete, there is no risky in-between state. Once it finishes, your slots are already assigned to the target nodes without interruption or confusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is this better?
&lt;/h2&gt;

&lt;p&gt;For operators, this is a big deal.&lt;/p&gt;

&lt;p&gt;Fewer round trips, fewer topology changes, and no split-brain state during migration. You can rebalance entire clusters without disturbing workloads or waking up the on-call engineer (hooray!).&lt;/p&gt;

&lt;p&gt;Both models still exist today, but atomic slot migration represents a new standard for reliability. It shows how thoughtful engineering can make the hardest operational tasks feel invisible.&lt;/p&gt;

&lt;p&gt;As &lt;a href="https://www.linkedin.com/in/kshams/" rel="noopener noreferrer"&gt;Khawaja&lt;/a&gt; said in the latest Cache It podcast episode with &lt;a href="https://www.linkedin.com/in/jacob-murphy-801078127/" rel="noopener noreferrer"&gt;Jacob Murphy&lt;/a&gt;, "&lt;em&gt;This moves us closer to a world where scaling a cache never means taking it offline.&lt;/em&gt;"&lt;/p&gt;

&lt;p&gt;And that's a world every SRE wants to live in.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>caching</category>
      <category>distributedsystems</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
