Dead Letter Policy
Configure dead-letter queue policy via upstream streams
Overview
A dead-letter policy protects a queue from poison messages — a record that fails processing over and over because of a malformed payload, a consumer bug, or a downstream dependency that is down. Without one, that message is redelivered forever: it blocks head-of-line delivery, burns your consumers' retry budget, and can stall an entire queue behind a single bad record.
With a policy attached, KubeMQ counts each failed delivery and, once the message crosses SetMaxReceiveCount, automatically moves it to the dead-letter channel you name with SetMaxReceiveQueue on the QueueMessage::Builder. The main queue keeps flowing while the failure is quarantined for inspection or replay.
Gotchas: the receive count increments on every failed delivery — an explicit nack, an expired transaction, or a visibility timeout — not just deliberate rejections, so set the ceiling above your normal retry budget. The dead-letter channel is an ordinary queue with no special behavior: nothing drains it for you, so monitor it and build a reprocessing path or failures pile up silently. The policy is set at send time and travels with the message, so the producer, not the consumer, decides the retry ceiling.
Prerequisites
- KubeMQ server running on
localhost:50000 - C++ SDK installed (vcpkg or CMake FetchContent)
- C++17 compiler (GCC 9+, Clang 9+, MSVC 2019+)
Code
// Example: queues_stream/dead_letter_policy
//
// Demonstrates sending queue messages with a dead-letter queue policy
// via the upstream stream. After exceeding the max receive count,
// messages are moved to the specified dead-letter queue.
//
// Channel: cpp-queues.dead-letter-policy
// Client ID: cpp-queues-dead-letter-policy-client
//
// Run with a KubeMQ server on localhost:50000
// (see https://docs.kubemq.io/deploy).
#include <kubemq/kubemq.h>
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::cout << "[1] Connecting to localhost:50000" << std::endl;
kubemq::ClientOptions options;
options.set_address("localhost", 50000);
options.set_client_id("cpp-queues-dead-letter-policy-client");
auto client_result = kubemq::Client::Create(options);
if (!client_result.ok()) {
std::cerr << "[ERROR] Failed to create client: " << client_result.status().message()
<< std::endl;
return 1;
}
auto& client = *client_result;
std::string channel = "cpp-queues.dead-letter-policy";
std::string dlq_channel = "cpp-queues.dead-letter-policy.dlq";
std::atomic<bool> result_received{false};
// Open an upstream stream for sending messages with DLQ policy.
std::cout << "[2] Opening upstream stream" << std::endl;
auto upstream_result = client->QueueUpstream(
[&result_received, &dlq_channel](const kubemq::QueueUpstreamResult& res) {
if (res.is_error) {
std::cerr << "[ERROR] Upstream result: " << res.error << std::endl;
} else {
std::cout << "[4] Sent message with DLQ policy (max 3 receives, dlq=" << dlq_channel
<< ")" << std::endl;
}
result_received.store(true);
},
[](const kubemq::Status& err) {
std::cerr << "[ERROR] Upstream error: " << err.message() << std::endl;
});
if (!upstream_result.ok()) {
std::cerr << "[ERROR] QueueUpstream: " << upstream_result.status().message() << std::endl;
return 1;
}
auto& upstream = *upstream_result;
// Build a message with dead-letter queue policy.
auto msg_result = kubemq::QueueMessage::Builder()
.SetChannel(channel)
.SetBody("max 3 receives, then DLQ")
.SetMaxReceiveCount(3)
.SetMaxReceiveQueue(dlq_channel)
.Build();
if (!msg_result.ok()) {
std::cerr << "[ERROR] Build message: " << msg_result.status().message() << std::endl;
return 1;
}
std::vector<kubemq::QueueMessage> messages;
messages.push_back(std::move(*msg_result));
std::cout << "[3] Sending message with max_receive_count=3, dlq=" << dlq_channel << std::endl;
auto send_status = upstream->Send("req-dlq", messages);
if (!send_status.ok()) {
std::cerr << "[ERROR] Send: " << send_status.message() << std::endl;
return 1;
}
// Wait for the result callback.
std::this_thread::sleep_for(std::chrono::seconds(3));
if (!result_received.load()) {
std::cout << "[4] Sent message (no result confirmation within timeout)" << std::endl;
}
// Close the upstream stream and client explicitly.
// Note: Destructors also handle cleanup, but explicit calls are shown
// for clarity and to match Go's defer pattern.
upstream->Close();
std::cout << "[5] Upstream stream closed" << std::endl;
auto close_status = client->Close();
if (!close_status.ok()) {
std::cerr << "[ERROR] Close failed: " << close_status.message() << std::endl;
return 1;
}
std::cout << "[6] Client closed" << std::endl;
return 0;
}How It Works
- Sends messages via upstream stream with DLQ policy using
SetMaxReceiveCount()andSetMaxReceiveQueue(). - After exceeding the max receive count, messages are moved to the dead-letter queue.
- The DLQ channel is specified per-message in the builder.
- Useful for isolating poison messages that repeatedly fail processing.
Related
Was this page helpful?