KubeMQ
Client SDKsGoHow-to guidesQueues

Dead Letter Queue

Route failed KubeMQ queue messages to a dead-letter queue using the Go SDK to isolate poison messages.

Which to use

This is the task-oriented guide for routing failed messages to a dead-letter queue via the single-send API. For the stream-based SetMaxReceiveCount/SetMaxReceiveQueue policy option and its edge cases, see Dead Letter Policy.

Overview

A dead-letter queue (DLQ) gives a poison message somewhere to go instead of looping through consumers forever. When a message keeps failing — a malformed payload, a downstream outage, a handler bug — retrying it forever wastes consumer cycles and blocks everything behind it. A DLQ takes that decision out of your hands: past a set number of failed attempts, the broker diverts the message to a separate channel instead of retrying it again.

Routing runs on two settings attached to the message: SetMaxReceiveCount() and SetMaxReceiveQueue(). Every failed delivery — a nack, a reject, or an expired visibility window — increments the receive count; past the threshold, the broker reroutes the message to the DLQ instead of redelivering it. The DLQ itself is an ordinary queue, consumed like any other channel.

Gotchas: the DLQ doesn't drain itself — a dedicated consumer must watch it. The count increments on any failed delivery, not just deliberate rejections — a slow consumer that lets the visibility window lapse counts the same as an explicit nack. A typo in the DLQ channel name quietly creates an unrelated channel instead of failing loudly.

Prerequisites

  • KubeMQ server running on localhost:50000
  • Go SDK installed (go get github.com/kubemq-io/kubemq-go/v2)

Code

main.go
// Example: queues/dead-letter-queue
//
// Demonstrates configuring a dead-letter queue (DLQ) for messages that
// exceed the maximum receive count. After the max attempts, messages
// are moved to the specified DLQ channel.
//
// Channel: go-queues.dead-letter-queue
// Client ID: go-queues-dead-letter-queue-client
//
// Run with a KubeMQ server on localhost:50000
// (see https://docs.kubemq.io/deploy).
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/kubemq-io/kubemq-go/v2"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	client, err := kubemq.NewClient(ctx,
		kubemq.WithAddress("localhost", 50000),
		kubemq.WithClientId("go-queues-dead-letter-queue-client"),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	channel := "go-queues.dead-letter-queue"
	dlqChannel := channel + ".dlq"

	// Send a message with dead-letter queue configuration.
	// After 3 failed receive attempts, the message is moved to the DLQ.
	dlqMsg := kubemq.NewQueueMessage().
		SetChannel(channel).
		SetBody([]byte("message with DLQ")).
		SetMaxReceiveCount(3).
		SetMaxReceiveQueue(dlqChannel)

	result, err := client.SendQueueMessage(ctx, dlqMsg)
	if err != nil {
		log.Fatal(err)
	}
	if result.IsError {
		log.Fatalf("Send failed: %s", result.Error)
	}
	fmt.Printf("DLQ message sent: id=%s (max receives=3, dlq=%s)\n",
		result.MessageID, dlqChannel)
}

How It Works

  1. kubemq.NewQueueMessage().SetMaxReceiveCount(3).SetMaxReceiveQueue(dlqChannel) configures a dead-letter policy inline on the message at send time; no separate channel-level configuration is required.
  2. When a consumer polls and nacks the message (or lets the transaction expire) more than 3 times, the broker automatically moves it to dlqChannel instead of redelivering it.
  3. The DLQ channel name is go-queues.dead-letter-queue.dlq — it is a regular queue that can be polled for investigation or reprocessing.
  4. result.IsError on the send response confirms the message was accepted with its DLQ policy; the policy is stored with the message in the broker.

Was this page helpful?

On this page