PageSourceSearch

https://www.rabbitmq.com/assets/js/a20a4e5d.4972d6dd.js

js rabbitmq.com collected 2026-10-01 06:27:26 UTC 15,690 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkrabbitmq_website=self.webpackChunkrabbitmq_website||[]).push([["10250"],{19197(e,s,t){t.r(s),t.d(s,{assets:()=>d,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>n,toc:()=>l});var n=t(4033),i=t(74848),a=t(28453);let o={title:"AMQP 1.0 Modified Outcome",tags:["AMQP 1.0","RabbitMQ 4.0","New Features"],authors:["dansari","nkarl","acogoluegnes"],image:"./modified-outcome.png"},r,d={image:t(10405).A,authorsImageUrls:[void 0,void 0,void 0]},l=[{value:"Requeue",id:"requeue",level:2},{value:"Dead Letter",id:"dead-letter",level:2},{value:"Dead Letter vs. Re-publish",id:"dead-letter-vs-re-publish",level:2},{value:"Wrapping Up",id:"wrapping-up",level:2}];function c(e){let s={a:"a",code:"code",figcaption:"figcaption",figure:"figure",h2:"h2",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components},{Details:n}=s;return n||function(e,s){throw Error("Expected "+(s?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(s.p,{children:["This blog post explores use cases of the AMQP 1.0 ",(0,i.jsx)(s.a,{href:"https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified",children:"modified outcome"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:["The modified outcome is a ",(0,i.jsx)(s.a,{href:"/docs/amqp#outcomes",children:"feature"})," exclusive to AMQP 1.0 and not available in AMQP 0.9.1\nIt is supported in ",(0,i.jsx)(s.a,{href:"/docs/quorum-queues",children:"quorum queues"}),", but not in ",(0,i.jsx)(s.a,{href:"/docs/classic-queues",children:"classic queues"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:["This feature enables consumers to add or update ",(0,i.jsx)(s.a,{href:"https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-message-annotations",children:"message annotations"})," before requeueing or ",(0,i.jsx)(s.a,{href:"/docs/dlx",children:"dead lettering"})," a message."]}),"\n",(0,i.jsx)(s.h2,{id:"requeue",children:"Requeue"}),"\n",(0,i.jsx)(s.p,{children:"Including additional metadata when requeuing a message can be valuable for improving traceability and debugging during message processing."}),"\n",(0,i.jsxs)(s.p,{children:["For example, an application using the ",(0,i.jsx)(s.a,{href:"https://github.com/rabbitmq/rabbitmq-amqp-java-client",children:"RabbitMQ AMQP 1.0 Java Client"})," can set specific message annotations before requeuing the message at the head of a quorum queue, as shown below:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-java",children:'Consumer consumer = connection.consumerBuilder()\n    .queue(ordersQueue)\n    .messageHandler((context, message) -> {\n        Map<String, Object> annotations = new HashMap<>();\n        annotations.put("x-opt-requeue-reason", "external_service_unavailable");\n        annotations.put("x-opt-requeue-time", System.currentTimeMillis());\n        annotations.put("x-opt-requeued-by", "consumer_1");\n        context.requeue(annotations);\n    }).build();\n'})}),"\n",(0,i.jsxs)(s.p,{children:["These annotations could use different types including ",(0,i.jsx)(s.a,{href:"https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html#type-map",children:"map"}),", ",(0,i.jsx)(s.a,{href:"https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html#type-list",children:"list"}),", or ",(0,i.jsx)(s.a,{href:"https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-types-v1.0-os.html#type-array",children:"array"}),".\nThis flexibility allows not only setting details like the last requeue reason, time, and consumer, but also tracking a history of requeue events.\nMaintaining such a history can reveal patterns, such as identifying consumers that requeue messages more frequently or discovering common requeue reasons across the system.\nHowever, keep in mind that quorum queues retain modified message annotations in memory, which increases the memory overhead per requeued message."]}),"\n",(0,i.jsx)(s.p,{children:"Setting custom headers before requeueing a message at the head of the queue is not supported in AMQP 0.9.1."}),"\n",(0,i.jsxs)(s.p,{children:["Whether requeuing a message to a quorum queue via AMQP 1.0 or AMQP 0.9.1, the ",(0,i.jsx)(s.a,{href:"https://www.rabbitmq.com/docs/quorum-queues#poison-message-handling",children:"x-delivery-count"})," annotation will always be incremented."]}),"\n",(0,i.jsx)(s.h2,{id:"dead-letter",children:"Dead Letter"}),"\n",(0,i.jsx)(s.p,{children:"When dead lettering a message, the consumer can include a custom reason for the dead lettering in the message annotations:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-java",children:'Consumer consumer = connection.consumerBuilder()\n    .queue(ordersQueue)\n    .messageHandler((context, message) -> {\n        Map<String, Object> annotations = new HashMap<>();\n        annotations.put("x-opt-dead-letter-reason", "Incompatible Message Format");\n        context.discard(annotations);\n    }).build();\n'})}),"\n",(0,i.jsxs)(s.p,{children:["When dead lettering to a ",(0,i.jsx)(s.a,{href:"/tutorials/amqp-concepts#exchange-headers",children:"headers exchange"}),", the consumer can even decide which target queue the message will be routed to:"]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsxs)(s.figure,{children:[(0,i.jsx)(s.img,{alt:"An AMQP 1.0 consumer can use the modified outcome to decide which dead letter queue a message is routed to.",src:t(53221).A+"",width:"960",height:"540"}),(0,i.jsx)(s.figcaption,{children:"An AMQP 1.0 consumer can use the modified outcome to decide which dead letter queue a message is routed to."})]})}),"\n",(0,i.jsx)(s.p,{children:"In this example, two dead letter quorum queues are bound to the dead letter headers exchange:"}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"transient-failures-dlq"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"business-logic-failures-dlq"})}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["Different dead letter queues can be processed by different apps or teams, with varying actions taken depen
1ding on the nature of the failure.\nFor instance, all messages in the ",(0,i.jsx)(s.code,{children:"transient-failures-dlq"})," could be re-published to the original ",(0,i.jsx)(s.code,{children:"orders"})," queue, while messages in the ",(0,i.jsx)(s.code,{children:"business-logic-failures-dlq"})," might require human intervention."]}),"\n",(0,i.jsx)(s.p,{children:"More dead letter queues could be added, such as:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"data-integrity-dlq"})," for messages with unknown schema"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"resource-limit-dlq"})," for cases where rate limits were exceeded"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"critical-errors-dlq"})," for situations that require administrator attention."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["It\u2019s crucial that all messages dead lettered from the ",(0,i.jsx)(s.code,{children:"orders"})," queue are routable.\nThe ",(0,i.jsx)(s.a,{href:"/docs/ae",children:"alternate exchange"}),' in the above diagram provides "or else" routing semantics, ensuring messages end up in the ',(0,i.jsx)(s.code,{children:"uncategorised-dlq"})," if no ",(0,i.jsx)(s.code,{children:"x-opt-dead-letter-category"})," annotation is set.\nThis might occur, for example, if the publisher sets a ",(0,i.jsx)(s.code,{children:"ttl"})," ",(0,i.jsx)(s.a,{href:"https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-header",children:"header"})," but no consumer grants ",(0,i.jsx)(s.a,{href:"/blog/2024/09/02/amqp-flow-control#link-credit",children:"link credit"}),", causing the message to expire and be dead lettered."]}),"\n",(0,i.jsxs)(s.p,{children:["The scenario depicted above is demonstrated in the ",(0,i.jsx)(s.a,{href:"https://github.com/ansd/modified-outcome/blob/v0.1.0/src/main/java/com/github/ansd/App.java",children:"modified-outcome sample application"}),"."]}),"\n",(0,i.jsxs)(n,{children:[(0,i.jsx)("summary",{children:"modified-outcome sample application"}),(0,i.jsxs)(s.p,{children:["The sample app uses the ",(0,i.jsx)(s.a,{href:"https://github.com/rabbitmq/rabbitmq-amqp-java-client",children:"RabbitMQ AMQP 1.0 Java Client"}),"."]}),(0,i.jsx)(s.p,{children:"You can run this sample application as follows:"}),(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:["Start RabbitMQ server via ",(0,i.jsx)(s.code,{children:"docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4.0-management"})]}),"\n",(0,i.jsxs)(s.li,{children:["In the root directory of ",(0,i.jsx)(s.a,{href:"https://github.com/ansd/modified-outcome/tree/v0.1.0",children:"the sample app"}),", start the client via ",(0,i.jsx)(s.code,{children:"mvn clean compile exec:java"}),"."]}),"\n"]}),(0,i.jsxs)(s.p,{children:["After publishing a message to the ",(0,i.jsx)(s.code,{children:"orders"})," queue, the client app consumes the message and outputs the following on the console:"]}),(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{children:"publisher: received ACCEPTED outcome\nconsumer: setting annotations {x-opt-dead-letter-reason=Customer Not Eligible for Discount, x-opt-dead-letter-category=business-logic} and dead lettering...\n"})}),(0,i.jsxs)(s.p,{children:["The message will be dead lettered to the ",(0,i.jsx)(s.code,{children:"business-logic-failures-dlq"}),"."]}),(0,i.jsxs)(s.p,{children:["To prevent message loss during dead lettering, the sample app uses ",(0,i.jsx)(s.a,{href:"/blog/2022/03/29/at-least-once-dead-lettering",children:"at-least-once dead lettering"}),"."]})]}),"\n",(0,i.jsx)(s.h2,{id:"dead-letter-vs-re-publish",children:"Dead Letter vs. Re-publish"}),"\n",(0,i.jsxs)(s.p,{children:["An AMQP 0.9.1 consumer cannot set custom headers before dead lettering a message.\nHowever, instead of using ",(0,i.jsx)(s.code,{children:"basic.nack"})," or ",(0,i.jsx)(s.code,{children:"basic.reject"})," with ",(0,i.jsx)(s.code,{children:"requeue=false"})," to dead letter a message, an AMQP 0.9.1 client could follow this approach:"]}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:'Re-publish the message directly to a specific "dead letter" queue with new custom headers.'}),"\n",(0,i.jsx)(s.li,{children:"Wait for RabbitMQ to confirm the re-published message."}),"\n",(0,i.jsxs)(s.li,{children:["Acknowledge the original message via ",(0,i.jsx)(s.code,{children:"basic.ack"}),"."]}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"An AMQP 1.0 client can choose between dead lettering with custom message annotations or re-publishing the message.\nBoth approaches have their advantages and trade-offs:"}),"\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Criteria"}),(0,i.jsx)(s.th,{children:"Dead Letter with Custom Reason"}),(0,i.jsx)(s.th,{children:"Re-publish with Custom Reason"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Simplicity"}),(0,i.jsx)(s.td,{children:"Easier for consumers."}),(0,i.jsx)(s.td,{children:"More complex, as the consumer must handle the republishing process."})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Overhead"}),(0,i.jsx)(s.td,{children:"Low overhead."}),(0,i.jsx)(s.td,{children:"Higher overhead for the client: the message payload must be re-published from the client to RabbitMQ, with additional latency due to the extra publish and confirm steps."})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Network Failure between client and RabbitMQ before settling the consumed message."}),(0,i.jsx)(s.td,{children:"Message gets requeued."}),(0,i.jsx)(s.td,{children:'The message might have been both re-published and requeued, resulting in one copy ending up in the "dead letter" queue and another in the original queue.'})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"Flexibility"}),(0,i.jsx)(s.td,{children:"Can modify only message annotations and route based on dead letter headers exchange."}),(0,i.jsx)(s.td,{children:"Allows modification of any part of the message and re-publishing to any exchange."})]})]})]}),"\n",(0,i.jsx)(s.h2,{id:"wrapping-up",children:"Wrapping Up"}),"\n",(0,i.jsx)(s.p,{children:"AMQP 1.0's modified outcome feature allows consumers to modify message annotations before requeueing or dead lettering."}),"\n",(0,i.jsxs)(s.p,{children:["Rather than relying solely on RabbitMQ's built-in dead lettering tracking via ",(0,i.jsx)(s.a,{href:"/docs/dlx#effects",children:"x-opt-deaths"}),", consumers can customise dead lettering event tracking and even choose which dead letter queue a message is sent to."]})]})}function h(e={}){let{wrapper:s}={...(0,a.R)(),...e.components};return s?(0,i.jsx)(s,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},10405(e,s,t){t.d(s,{A:()=>n});let n=t.p+"assets/images/modified-outcome-8c40a1d461b37bbde0a1f3f5f849117b.png"},53221(e,s,t){t.d(s,{A:()=>n});let n=t.p+"assets/images/modified-outcome-1f1cee13e3c2f3aec685976bbfc338c3.svg"},28453(e,s,t){t.d(s,{R:()=>o,x:()=>r});var n=t(96540);let i={},a=n.createContext(i);function o(e){let s=n.useContext(a);return n.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function r(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),n.createElement(a.Provider,{value:s},e.children)}},4033(e){e.exports=JSON.parse('{"permalink":"/blog/2024/10/11/modified-outcome","editUrl":"https://github.com/rabbitmq/rabbitmq-website/tree/main/blog/2024-10-11-modified-outcome/index.md","source":"@site/blog/2024-10-11-modified-outcome/index.md","title":"AMQP 1.0 Modified Outcome","description":"This blog post explores use cases of the AMQP 1.0 modified outcome.","date":"2024-10-11T00:00:00.000Z","tags":[{"inline":true,"label":"AMQP 1.0","permalink":"/blog/tags/amqp-1-0"},{"inline":true,"label":"RabbitMQ 4.0","permalink":"/blog/tags/rabbit-mq-4-0"},{"inline":true,"label":"New Features","permalink":"/blog/tags/new-features"}],"readingTime":5.68,"hasTruncateMarker":true,"authors":[{"name":"David Ansari","url":"https://github.com/ansd","socials":{"github":"https://github.com/ansd","linkedin":"https://www.linkedin.com/in/ansd/","mastodon":"https://m.ansd.xyz/@ansd","bluesky":"https://bsky.app/profile/ansd.xyz"},"imageURL":"https://github.com/ansd.png","key":"dansari","page":null},{"name":"Karl Nilsson","url":"https://github.com/kjnilsson","socials":{"github":"https://github.com/kjnilsson","linkedin":"https://www.linkedin.com/in/kjnils/","bluesky":"https://bsky.app/profile/kjnilsson.bsky.social"},"imageURL":"https://github.com/kjnilsson.png","key":"nkarl","page":null},{"name":"Arnaud Cogolu\xe8gnes","url":"https://github.com/acogoluegnes","socials":{"github":"https://github.com/acogoluegnes","linkedin":"https://www.linkedin.com/in/arnaudcogoluegnes/","bluesky":"https://bsky.app/profile/acogoluegnes.bsky.social"},"imageURL":"https://github.com/acogoluegnes.png","key":"acogoluegnes","page":null}],"frontMatter":{"title":"AMQP 1.0 Modified Outcome","tags":["AMQP 1.0","RabbitMQ 4.0","New Features"],"authors":["dansari","nkarl","acogoluegnes"],"image":"./modified-outcome.png"},"unlisted":false,"prevItem":{"title":"AMQP 1.0 Filter Expressions","permalink":"/blog/2024/12/13/amqp-filter-expressions"},"nextItem":{"title":"Ten Benefits of AMQP 1.0 Flow Control","permalink":"/blog/2024/09/02/amqp-flow-control"}}')}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.