KubeMQ
Client SDKsC++How-to guidesQueues

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

main.cc
// 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() and SetMaxReceiveQueue().
  • 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.

Was this page helpful?

On this page