<?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: Stephen Wanjohi</title>
    <description>The latest articles on DEV Community by Stephen Wanjohi (@nehigna).</description>
    <link>https://dev.to/nehigna</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%2F4043215%2F8cb7ca54-fa59-43c5-a643-1fe097a28663.png</url>
      <title>DEV Community: Stephen Wanjohi</title>
      <link>https://dev.to/nehigna</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nehigna"/>
    <language>en</language>
    <item>
      <title>A2A push notifications: what the spec guarantees, and what it leaves to you</title>
      <dc:creator>Stephen Wanjohi</dc:creator>
      <pubDate>Tue, 04 Aug 2026 09:15:01 +0000</pubDate>
      <link>https://dev.to/nehigna/a2a-push-notifications-what-the-spec-guarantees-and-what-it-leaves-to-you-55i9</link>
      <guid>https://dev.to/nehigna/a2a-push-notifications-what-the-spec-guarantees-and-what-it-leaves-to-you-55i9</guid>
      <description>&lt;p&gt;The Agent2Agent protocol solved a real problem. Long-running agent tasks do not fit inside a request and response, and holding an SSE stream open for forty minutes is not something you want to depend on. So A2A lets the client register a webhook and disconnect, and the remote agent posts back when something worth knowing about happens.&lt;/p&gt;

&lt;p&gt;The specification is careful about the parts it covers. It is also explicit, by omission, about a part it does not cover, and that part is the one that decides whether your integration works at three in the morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the spec actually gives you
&lt;/h2&gt;

&lt;p&gt;A push notification is configured with a &lt;code&gt;PushNotificationConfig&lt;/code&gt;. The fields are small and sensible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;url&lt;/code&gt;, the HTTPS endpoint that will receive the POST&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;token&lt;/code&gt;, an optional opaque value the receiver can check to confirm the notification belongs to a task it knows about&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;authentication&lt;/code&gt;, optional details describing how the sending agent will authenticate itself to your webhook
You can supply it inline on the initial &lt;code&gt;SendMessage&lt;/code&gt; or &lt;code&gt;SendStreamingMessage&lt;/code&gt; call, or manage it separately for an existing task. The current specification exposes create, get, list, and delete operations for these configs, so a client can rotate an endpoint without restarting the task.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the wire, the notification body uses the same &lt;code&gt;StreamResponse&lt;/code&gt; shape as streaming, carrying exactly one of a &lt;code&gt;task&lt;/code&gt;, a &lt;code&gt;message&lt;/code&gt;, a &lt;code&gt;statusUpdate&lt;/code&gt;, or an &lt;code&gt;artifactUpdate&lt;/code&gt;. If you have already written a streaming consumer, you have most of a webhook consumer.&lt;/p&gt;

&lt;p&gt;The spec also tells you when to expect one. The server decides, but the guidance is that notifications fire on significant state changes. A task is in one of eight states. Four are terminal: &lt;code&gt;COMPLETED&lt;/code&gt;, &lt;code&gt;FAILED&lt;/code&gt;, &lt;code&gt;CANCELED&lt;/code&gt;, &lt;code&gt;REJECTED&lt;/code&gt;. Four are interim: &lt;code&gt;SUBMITTED&lt;/code&gt;, &lt;code&gt;WORKING&lt;/code&gt;, &lt;code&gt;INPUT_REQUIRED&lt;/code&gt;, &lt;code&gt;AUTH_REQUIRED&lt;/code&gt;. In practice you will be notified on the terminal ones, and on &lt;code&gt;INPUT_REQUIRED&lt;/code&gt; and &lt;code&gt;AUTH_REQUIRED&lt;/code&gt;, because those are the two interim states where the task has stopped and is waiting on you.&lt;/p&gt;

&lt;p&gt;On authentication it is genuinely helpful. Bearer tokens, API keys, HMAC signatures, and mTLS are all named, with a JWT and JWKS example for key distribution. The security guidance is good in both directions. The sending agent should treat a client-supplied URL as hostile and defend against SSRF with domain allowlisting, ownership verification, and egress controls. The receiving webhook should verify signatures against trusted keys, validate the &lt;code&gt;token&lt;/code&gt; if one was set, and use timestamps and nonces or &lt;code&gt;jti&lt;/code&gt; claims to reject replays.&lt;/p&gt;

&lt;p&gt;That is a well-specified protocol. Read it and you know exactly what a notification looks like and how to prove it came from who it claims to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sentence that is not in the specification
&lt;/h2&gt;

&lt;p&gt;Nowhere does A2A say what happens if your webhook is down.&lt;/p&gt;

&lt;p&gt;There is no retry policy. No backoff guidance. No timeout threshold. No statement about how many attempts a sending agent should make, or whether it should make any. No definition of at-least-once or at-most-once. The spec says notifications are delivered by HTTP POST and leaves everything after that word to the implementation.&lt;/p&gt;

&lt;p&gt;This is a defensible choice. Protocols that try to mandate delivery semantics tend to age badly. But it means something specific for anyone building on A2A: &lt;strong&gt;the reliability of your agent notifications is not a property of the protocol. It is a property of whichever implementation happens to be on the other end, and you probably have not read its source.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two A2A-compliant agents can behave completely differently here. One retries ten times over six hours with exponential backoff. One tries once, catches the exception, logs it, and moves on. Both are conformant. Your integration passes its tests against either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this bites harder for agents than for ordinary webhooks
&lt;/h2&gt;

&lt;p&gt;A dropped &lt;code&gt;order.paid&lt;/code&gt; webhook is bad. A dropped A2A terminal notification is worse, and the reason is structural.&lt;/p&gt;

&lt;p&gt;Push notifications exist precisely for the long-running case. The client disconnected on purpose. That is the feature. So when the agent finishes a task that took forty minutes of real compute, there is often exactly one POST that says so, and nobody is watching the connection anymore.&lt;/p&gt;

&lt;p&gt;If that POST fails and is not retried, three things are true at once. The work happened. The result exists on the remote agent. And your side of the system believes the task is still &lt;code&gt;WORKING&lt;/code&gt;, forever, because the notification that would have moved it was the notification that got lost.&lt;/p&gt;

&lt;p&gt;With streaming you would have noticed the disconnect. That is the trade you made when you chose push. It is a good trade, but it moves the burden of noticing onto the delivery layer.&lt;/p&gt;

&lt;p&gt;There is a second-order version of this that is nastier. Because the sender chooses when to notify, an agent can legitimately send you the same state transition twice, and a retrying sender certainly will. If your webhook handler is not idempotent, a duplicate &lt;code&gt;COMPLETED&lt;/code&gt; notification does not just log twice. It runs whatever your completion handler runs, twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the receiving side owes
&lt;/h2&gt;

&lt;p&gt;Your webhook is a public HTTPS endpoint that a third party posts to. Treat it that way.&lt;/p&gt;

&lt;p&gt;Verify before you trust. Check the signature or bearer token against the key you expect, compare the &lt;code&gt;token&lt;/code&gt; field against the value you registered, and do both with constant-time comparison so you are not leaking the credential a byte at a time through timing. Return &lt;code&gt;401&lt;/code&gt; on a mismatch and do not explain which part failed.&lt;/p&gt;

&lt;p&gt;Reject replays. A timestamp window plus a seen-nonce set is enough. The &lt;code&gt;jti&lt;/code&gt; claim exists for this if you are on JWTs.&lt;/p&gt;

&lt;p&gt;Be idempotent, and key it correctly. The natural key for an A2A notification is the task ID combined with the state being reported, not a random message ID, because the thing you want to happen only once is "this task became &lt;code&gt;COMPLETED&lt;/code&gt;", regardless of how many POSTs carry that news.&lt;/p&gt;

&lt;p&gt;Return quickly. Acknowledge with a &lt;code&gt;2xx&lt;/code&gt; as soon as the notification is durably written on your side, then do the actual work asynchronously. A handler that takes eleven seconds because it calls three internal services will eventually take longer than the sender's timeout, and then you are being retried for work you already completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the sending side owes
&lt;/h2&gt;

&lt;p&gt;If you are the one running the agent, the notification is your outbound delivery problem, and it looks exactly like every other outbound delivery problem.&lt;/p&gt;

&lt;p&gt;Write it down before you send it. The state transition should be durable in your own database before the POST is attempted, because a process that crashes between "task completed" and "notification sent" otherwise loses the only record that a notification was owed.&lt;/p&gt;

&lt;p&gt;Retry, but retry with judgement. A connection timeout or a &lt;code&gt;503&lt;/code&gt; deserves another attempt on a backoff schedule with jitter, because the endpoint is probably coming back. A &lt;code&gt;422&lt;/code&gt; or a &lt;code&gt;400&lt;/code&gt; does not, because the payload will be equally wrong in ten minutes and all you are doing is delaying the moment somebody discovers the integration has been broken since it shipped. A &lt;code&gt;410 Gone&lt;/code&gt; is the receiver telling you to stop, and the right response is to disable the config rather than to keep trying.&lt;/p&gt;

&lt;p&gt;Fail somewhere visible. When the retry budget is exhausted, the notification should land in a dead letter queue that a person actually reads, stored whole rather than as a log line, so that when the endpoint comes back somebody can replay it. Alert on the endpoint rather than on the individual notification, or you will teach everyone to ignore the alert.&lt;/p&gt;

&lt;p&gt;Defend the URL. The client hands you an arbitrary HTTPS address and asks you to POST to it from inside your network. That is a textbook SSRF vector, which is why the spec calls it out. Allowlist, verify ownership, and keep egress controlled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build or buy, honestly
&lt;/h2&gt;

&lt;p&gt;None of the above is exotic. A competent backend team can build a persisted queue, a retry worker with jitter, a dead letter table, and a replay endpoint. Teams do it every quarter.&lt;/p&gt;

&lt;p&gt;The cost is not the build. It is that you have now taken ownership of a piece of infrastructure whose failure mode is silence, and it has to keep working correctly while you build the agent your users are actually paying for. Delivery infrastructure does not page you when it breaks. It just stops mentioning things.&lt;/p&gt;

&lt;p&gt;If you are running one agent, notifying one endpoint your own team operates, write the retry loop. That is the correct amount of engineering for that problem. The line worth watching is not volume. It is who owns the other end. Once you are posting to endpoints operated by other people, on their uptime and their deploy schedule, the failure modes stop being yours and you find out about them from a customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;A2A tells you what a push notification looks like and how to prove where it came from. It does not tell you what happens when the POST fails, and that gap is yours to fill on both sides of the connection.&lt;/p&gt;

&lt;p&gt;Persist the state transition before you send it. Retry the retryable and stop fast on everything else. Make the receiver idempotent on task ID plus state, because duplicates are not a bug in the sender, they are the sender doing its job. Verify credentials in constant time and reject replays. And put the notifications that never made it somewhere a person will look, because a task that silently stays &lt;code&gt;WORKING&lt;/code&gt; forever is the one failure mode nobody has a dashboard for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Mittr is the reliable action layer for webhooks and AI agent actions, including A2A push notifications. It handles both auth presets from the spec, bearer token and OIDC or OAuth2 JWT, with constant-time credential checks and a &lt;code&gt;401&lt;/code&gt; on mismatch. Every receipt is recorded in a request inspector, idempotency keys are derived from task plus state, and notifications are correlated by task ID. Outbound, every action is written to Postgres before delivery, retried on a front-loaded schedule with jitter behind a per-endpoint circuit breaker, and dead-lettered rather than dropped if it exhausts its budget. The free tier is 3,000 messages a month, no card. More at &lt;a href="https://mittr.io" rel="noopener noreferrer"&gt;mittr.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webhooks</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>Dead letter queues for webhooks: after the last retry</title>
      <dc:creator>Stephen Wanjohi</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:18:26 +0000</pubDate>
      <link>https://dev.to/nehigna/dead-letter-queues-for-webhooks-after-the-last-retry-1mpf</link>
      <guid>https://dev.to/nehigna/dead-letter-queues-for-webhooks-after-the-last-retry-1mpf</guid>
      <description>&lt;p&gt;Almost every webhook system has a retry policy. Far fewer have a good answer to the question that comes immediately after it: what happens when the retries run out?&lt;/p&gt;

&lt;p&gt;That moment is where most delivery systems quietly lose data. Not in a dramatic outage, but in a &lt;code&gt;catch&lt;/code&gt; block that logs an error nobody reads, on an endpoint that was down for eleven minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three endings, and most systems ship two
&lt;/h2&gt;

&lt;p&gt;An outbound event has exactly three terminal states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Delivered.&lt;/li&gt;
&lt;li&gt;Still trying.&lt;/li&gt;
&lt;li&gt;Gave up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In-house delivery code usually models the first two with real care. There is a retry loop, exponential backoff, maybe some jitter. State three is where the design thins out. The loop exits, an error is logged, and the event stops existing in any form the team can act on.&lt;/p&gt;

&lt;p&gt;A dead letter queue is what you build so that state three is a place rather than an ending.&lt;/p&gt;

&lt;h2&gt;
  
  
  A dead letter queue is not a queue of failures
&lt;/h2&gt;

&lt;p&gt;It is a queue of decisions nobody has made yet.&lt;/p&gt;

&lt;p&gt;That distinction sounds like semantics and it is not. Every entry in a well built dead letter queue is an event where the system has already exhausted everything it can do automatically, and a human now has to choose one of three things: replay it as is, fix the receiving endpoint and then replay it, or discard it deliberately.&lt;/p&gt;

&lt;p&gt;Once you frame it that way, the design follows. A queue of decisions needs enough context for someone to decide, it needs to reach a person who is capable of deciding, and it needs an obvious action to take once they have.&lt;/p&gt;

&lt;p&gt;Most dead letter implementations fail on at least one of those three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding when to stop
&lt;/h2&gt;

&lt;p&gt;Before anything lands in a dead letter queue, you have to decide what "give up" means. Three rules cover most cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate retryable from non-retryable.&lt;/strong&gt; A 500, a 502, a 503, a connection reset, a timeout: retry all of those, because the endpoint may well be fine in thirty seconds. A 400, 401, 403 or 422 will not fix itself on the next attempt. Something about the request is wrong, or the credentials are wrong, and hammering it twelve more times over six hours just adds noise. Fail those fast and dead letter them immediately, because the sooner a human sees a 401 the better.&lt;/p&gt;

&lt;p&gt;A 429 sits in the middle. Retry it, but honour the &lt;code&gt;Retry-After&lt;/code&gt; header rather than your own backoff curve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cap by attempts and by wall clock, not just attempts.&lt;/strong&gt; Exponential backoff compounds quickly. If you retry twelve times with doubling delays, the last attempt lands days after the first. By then the event may be meaningless and the customer has long since noticed.&lt;/p&gt;

&lt;p&gt;A front-loaded schedule handles this better than a pure doubling curve. Something like 1s, 5s, 30s, 2m, 10m, 30m, 1h, 6h, 24h catches the common case (an endpoint that blips for a few seconds) almost immediately, while still giving a genuinely down endpoint most of a day to come back. Add roughly ten percent random jitter to every delay, or a thousand retries queued during the same outage will stampede the endpoint the moment it recovers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a circuit breaker so one dead endpoint does not fill your queue.&lt;/strong&gt; If every delivery to a given endpoint has failed for the last ten minutes, stop trying and start parking. Otherwise a single customer whose server is down for an afternoon generates tens of thousands of doomed attempts, saturates your workers, and slows delivery for everyone else.&lt;/p&gt;

&lt;p&gt;There is a fourth case worth handling separately. If an endpoint returns 410 Gone, or 404s consistently for days, the subscription itself is dead. Disable it and notify the account owner rather than dead lettering thousands of events nobody will ever replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to store
&lt;/h2&gt;

&lt;p&gt;The most common mistake in a homegrown dead letter queue is storing the error instead of the event.&lt;/p&gt;

&lt;p&gt;A row that says &lt;code&gt;POST failed: 503&lt;/code&gt; is a log line, not a recovery mechanism. You cannot replay it. You have preserved the evidence of the problem and thrown away the thing you actually needed.&lt;/p&gt;

&lt;p&gt;A dead letter entry needs the full request as it would be sent again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the exact payload body&lt;/li&gt;
&lt;li&gt;the headers, including the signature and its timestamp&lt;/li&gt;
&lt;li&gt;the destination URL and the subscription or endpoint id&lt;/li&gt;
&lt;li&gt;the event id and the idempotency key&lt;/li&gt;
&lt;li&gt;the original event creation time, separate from the time it was dead lettered&lt;/li&gt;
&lt;li&gt;the complete attempt history: timestamp, status code, latency, and response body for every single try&lt;/li&gt;
&lt;li&gt;the reason delivery stopped: attempts exhausted, time budget exceeded, circuit open, non retryable status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response body is the field teams most often skip, usually for storage reasons, and it is the field that saves the most time. A bare &lt;code&gt;500&lt;/code&gt; tells you nothing about who owns the problem. A &lt;code&gt;500&lt;/code&gt; with &lt;code&gt;{"error":"unknown_currency: KES"}&lt;/code&gt; in the body tells you this is a broken integration rather than a flaky network, and it tells you in one glance instead of one afternoon.&lt;/p&gt;

&lt;p&gt;Truncate the body if you need to. Two kilobytes is usually plenty. Do not drop it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dead letter queue nobody looks at
&lt;/h2&gt;

&lt;p&gt;A dead letter queue without alerting is &lt;code&gt;/dev/null&lt;/code&gt; with extra steps and a storage bill.&lt;/p&gt;

&lt;p&gt;This is the failure mode I would watch for most closely, because it feels like success. The table exists. Rows are being written. Delivery has technically been made recoverable. But nobody is subscribed to it, so events accumulate for weeks and the first person to look is an engineer investigating a customer complaint that has already escalated.&lt;/p&gt;

&lt;p&gt;Two rules make it real:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert on rate, not on individual entries.&lt;/strong&gt; One dead letter is noise, and paging on every single one trains people to ignore the channel within a week. Forty from the same endpoint inside ten minutes is an incident. Alert on the second thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert on age.&lt;/strong&gt; An entry that has been sitting untouched for seven days is a decision nobody made. That is a different alert with a different urgency, and it should go to whoever owns the integration rather than to whoever is on call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replay is harder than it looks
&lt;/h2&gt;

&lt;p&gt;The replay button is the payoff for all of this, and it is also where you can do real damage. Three questions are worth asking before you press it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the event still true?&lt;/strong&gt; Some events are facts and stay valid forever. &lt;code&gt;payment.succeeded&lt;/code&gt; was true when it happened and it is still true four days later. Other events are snapshots of state. &lt;code&gt;order.status.updated&lt;/code&gt; from Tuesday may now describe a status that has been superseded twice, and replaying it can walk the receiver's state backwards. Facts are safe to replay indefinitely. Snapshots should either expire or be replaced by a fresh read of current state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will the receiver double process it?&lt;/strong&gt; At least once delivery means duplicates are inevitable and replay makes them likely. The idempotency key you generated when the event was first created has to survive into the dead letter entry and go out with the replay, unchanged. If replaying charges a card twice, the dead letter queue has made things worse rather than better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does order matter?&lt;/strong&gt; This one gets skipped and it is the one that corrupts data. If you replay event 40 after events 41 through 60 have already been delivered, the receiver processes an outdated change on top of newer ones. Where ordering matters for a given entity, replay needs to pause that entity's stream, replay in sequence, then resume. Where it does not matter, say so explicitly in your docs so consumers know not to rely on it.&lt;/p&gt;

&lt;p&gt;There is a fourth option worth building, which is to edit before you replay. A surprising share of dead letters are not transport failures at all. The endpoint URL changed, or the payload carries a field the receiver rejects. If the only action available is replay as is, those events are stuck forever and someone ends up writing a one off script. Being able to correct the destination or the body and then resend turns a dead end into a two minute fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poison messages
&lt;/h2&gt;

&lt;p&gt;There is one specific pattern worth designing against.&lt;/p&gt;

&lt;p&gt;An event that reliably crashes the receiver will fail every attempt, land in the dead letter queue, get replayed by a well meaning engineer, fail again, and come straight back. Left alone it can loop indefinitely, and if you replay in bulk it can take the receiving service down each time it comes around.&lt;/p&gt;

&lt;p&gt;Count replays per event. After two or three, quarantine it and require an explicit override to try again. The loop is almost always a malformed payload or a schema mismatch, and no number of retries will fix either.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building this actually costs
&lt;/h2&gt;

&lt;p&gt;None of the individual pieces here are hard, and that is exactly why teams underestimate the whole.&lt;/p&gt;

&lt;p&gt;The rough shape is an events table with a real state machine, a separate attempts table so history survives, a scheduler that wakes up delivery at the right time, a worker pool that does not starve under load, backoff with jitter, circuit breaker state tracked per endpoint, signature generation and zero downtime secret rotation, SSRF protection on every destination URL your customers can set, an admin interface where a human can actually see and act on dead letters, authentication and an audit trail on the replay action because replay is a write operation with real consequences, and a retention policy so the tables do not grow without bound.&lt;/p&gt;

&lt;p&gt;Any competent backend team can build that. The honest cost is not the initial build, it is that you now own a piece of infrastructure that has to keep working correctly while you build the product your customers are actually paying for, and its failure modes are silent by nature. Delivery infrastructure does not page you when it breaks. It just stops mentioning things.&lt;/p&gt;

&lt;p&gt;If your event volume is low and your endpoints are internal, building it is a perfectly reasonable call. If you are delivering to endpoints you do not control, the surface area grows faster than most teams expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Retries buy you time. The dead letter queue is what turns the events that time did not save into something recoverable.&lt;/p&gt;

&lt;p&gt;Store the whole event, not the error. Alert on rate and on age. Keep idempotency keys intact through replay. Decide up front which of your events are facts and which are snapshots. And make sure a real person receives the queue, because a dead letter queue nobody reads is just a slower way to lose data.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Mittr is the reliable action layer for webhooks and AI agent actions. Every action is written to Postgres before delivery, retried on a front-loaded schedule with jitter behind a per-endpoint circuit breaker, and logged on every attempt with status code, latency, and response. Anything that exhausts its retry budget is dead-lettered rather than dropped, where it can be inspected, edited, and replayed, with alerting on Slack, PagerDuty, or email. More at &lt;a href="https://mittr.io" rel="noopener noreferrer"&gt;mittr.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>architecture</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why retries matter more than prompts</title>
      <dc:creator>Stephen Wanjohi</dc:creator>
      <pubDate>Thu, 23 Jul 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/nehigna/why-retries-matter-more-than-prompts-2ll0</link>
      <guid>https://dev.to/nehigna/why-retries-matter-more-than-prompts-2ll0</guid>
      <description>&lt;p&gt;Somewhere right now, an engineering team is holding a retro about their AI agent. It worked in every demo. In production, a customer says it "just didn't do anything." The team is debating a prompt rewrite.&lt;/p&gt;

&lt;p&gt;The logs tell a different story. The agent reasoned correctly. It chose the right tool. It made the call. The call timed out. Nothing retried it. The action never happened, no one was notified, and the model took the blame for a networking problem.&lt;/p&gt;

&lt;p&gt;This is the most common agent failure we see, and no prompt will fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents are event-driven systems wearing a trench coat
&lt;/h2&gt;

&lt;p&gt;Strip away the novelty and an agent action is three steps: decide, act, confirm. The deciding gets all the attention. The acting is usually a bare HTTP call: &lt;code&gt;fetch(url)&lt;/code&gt; and hope.&lt;/p&gt;

&lt;p&gt;Backend engineers already know how this movie ends, because webhooks taught us. Networks fail. Endpoints rate-limit. Processes restart mid-request. The industry's answer wasn't smarter senders. It was delivery infrastructure: persistence, retries, idempotency, observability. Payments companies have run on it for a decade.&lt;/p&gt;

&lt;p&gt;Agents have quietly inherited the exact same problem. Every tool call, callback, and notification an agent produces is an event that needs to arrive. And most agent stacks today deliver those events with less care than a 2015 webhook integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a lost action costs
&lt;/h2&gt;

&lt;p&gt;When a prompt is mediocre, the output is mediocre: visibly, immediately, fixably.&lt;/p&gt;

&lt;p&gt;When a delivery fails silently, the cost is deferred and compounding. The customer wasn't notified. The workflow half-completed. And because fire-and-forget leaves no trail, there's nothing to debug: no status code, no attempt history, no way to distinguish "the agent decided wrong" from "the agent decided right and the network ate it."&lt;/p&gt;

&lt;p&gt;That last distinction is everything. Without it, every infrastructure failure becomes an indictment of your model, and you'll spend your time tuning prompts that were never the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reliability checklist that actually moves the needle
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. It's the same discipline reliable webhook systems use, applied to agent actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persist before you attempt.&lt;/strong&gt; Write the event down before trying to deliver it. If the process dies or the deploy restarts, the intent survives and delivery resumes. Anything held only in memory is a coin flip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry with backoff, and know when to stop.&lt;/strong&gt; Most delivery failures are transient: a timeout, a 502, a rate limit. Exponential backoff resolves them without hammering the endpoint. A circuit breaker stops you from making a struggling endpoint worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make retries safe.&lt;/strong&gt; At-least-once delivery means occasional duplicates, so every send needs an idempotency key. In Mittr's MCP tools, &lt;code&gt;mittr_send_event&lt;/code&gt; takes an &lt;code&gt;idempotencyKey&lt;/code&gt; parameter for exactly this reason, so the model can retry a tool call without double-charging anyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log every attempt.&lt;/strong&gt; Status code, latency, response body, timestamp. "The webhook never arrived" should be a 30-second lookup, not a day of grepping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep a replay button.&lt;/strong&gt; Some events fail past any reasonable retry budget, like when an endpoint is down for a full hour. That should be recoverable, not fatal: dead-letter it, alert on it, replay it when the endpoint is back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correlate by run.&lt;/strong&gt; Agent debugging has a dimension webhooks never had: one reasoning loop produces many actions. Tagging every event with a run identifier (&lt;code&gt;agentRunId&lt;/code&gt;, in Mittr's case) means you can pull up everything an agent did in a single run and see precisely which action failed, when, and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the leverage is
&lt;/h2&gt;

&lt;p&gt;Here's the argument in one comparison.&lt;/p&gt;

&lt;p&gt;Improving your prompt makes the agent decide a bit better, some of the time. Improving your delivery layer makes every action the agent takes (every tool call, every callback, every notification) either arrive or leave a recoverable, visible trail. One is a marginal gain on the intelligent part. The other removes an entire failure class from the system.&lt;/p&gt;

&lt;p&gt;Prompts are a craft. Delivery is a guarantee. Production systems are built on guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring ending
&lt;/h2&gt;

&lt;p&gt;The uncomfortable truth about production agents is that the differentiating work (your product, your reasoning, your UX) sits on top of a pile of undifferentiated plumbing: queues, retry engines, dead-letter queues, audit logs, replay systems.&lt;/p&gt;

&lt;p&gt;You can build that pile. Teams do, and it takes weeks to build and years to babysit. Or you can treat delivery as infrastructure, point your agent at it, and spend those weeks on the part of the system your customers actually see.&lt;/p&gt;

&lt;p&gt;That second option is why Mittr exists. Your agent sends the event. We do the rest.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Mittr is delivery infrastructure for events, webhooks, and agent actions: persisted first, retried with backoff, logged on every attempt, replayable any time. Agents connect over MCP in one config block: &lt;a href="https://docs.mittr.io/guides/ai-agents" rel="noopener noreferrer"&gt;docs.mittr.io/guides/ai-agents&lt;/a&gt;. The free tier is 3,000 messages a month, no credit card.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webhooks</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
