# Retry with Backoff (/learn/queues/how-to/retry-with-backoff)



## Overview [#overview]

When message processing fails, you can implement retry strategies ranging from simple immediate retry to exponential backoff with DLQ fallback. KubeMQ's nack mechanism and DLQ policy provide the building blocks.

The diagram below shows the lifecycle of a message that flows through a backoff retry strategy — each failure either schedules a delayed retry or, once the attempt budget is exhausted, routes the message to a Dead Letter Queue (DLQ).

<Mermaid
  chart="stateDiagram-v2
    [*] --> Queued: send
    Queued --> Delivered: receive
    Delivered --> Acked: process OK
    Delivered --> Retrying: process fails<br/>(attempt < max)
    Retrying --> Queued: re-send with<br/>delaySeconds = 2^attempt
    Delivered --> DeadLetter: attempt >= max<br/>requeue to orders.dlq
    Acked --> [*]
    DeadLetter --> [*]"
/>

*Message lifecycle: each failed delivery schedules a delayed retry until the attempt budget is exhausted, then routes to the DLQ.*

## Simple Retry (Nack) [#simple-retry-nack]

The simplest retry — nack the message so it becomes available again immediately.

<Tabs groupId="language" items="['Go', 'Python', 'Node.js', 'Java', 'C#', 'Kotlin', 'C++', 'Rust', 'Ruby', 'Elixir']">
  <Tab value="Go">
    ```go title="simple_retry.go"
    resp, err := client.PollQueue(ctx, &kubemq.PollRequest{
        Channel:            "orders",
        MaxItems:           1,
        WaitTimeoutSeconds: 5,
    })
    if err != nil {
        log.Fatal(err)
    }
    for _, m := range resp.Messages {
        err := processOrder(m.Message.Body)
        if err != nil {
            log.Printf("Processing failed (attempt %d): %v", m.Message.Attributes.ReceiveCount, err)
            resp.NAckAll()
            return
        }
    }
    resp.AckAll()
    ```
  </Tab>

  <Tab value="Python">
    ```python title="simple_retry.py"
    response = client.receive_queue_messages(
        channel="orders",
        max_messages=1,
        wait_timeout_in_seconds=5,
    )
    for msg in response.messages:
        try:
            process_order(msg.body)
            msg.ack()
        except Exception as e:
            print(f"Processing failed (attempt {msg.receive_count}): {e}")
            msg.nack()
    ```
  </Tab>

  <Tab value="Node.js">
    ```typescript title="simple_retry.ts"
    const messages = await client.receiveQueueMessages({
      channel: 'orders',
      maxMessages: 1,
      waitTimeoutSeconds: 5,
    });
    for (const msg of messages) {
      try {
        await processOrder(msg.body);
        await msg.ack();
      } catch (err) {
        console.log(`Processing failed (attempt ${msg.receiveCount}):`, err);
        await msg.nack();
      }
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java title="SimpleRetry.java"
    ReceiveQueueMessagesResponse response = client.receiveQueueMessages(
        ReceiveQueueMessagesRequest.builder()
            .channel("orders").maxMessages(1).waitTimeoutSeconds(5).build());

    for (QueueMessageReceived msg : response.getMessages()) {
        try {
            processOrder(msg.getBody());
            msg.ack();
        } catch (Exception e) {
            System.out.printf("Processing failed (attempt %d): %s%n", msg.getReceiveCount(), e);
            msg.nack();
        }
    }
    ```
  </Tab>

  <Tab value="C#">
    ```csharp title="SimpleRetry.cs"
    var response = await client.ReceiveQueueMessagesAsync(new ReceiveQueueMessagesRequest
    {
        Channel = "orders", MaxMessages = 1, WaitTimeoutSeconds = 5,
    });
    foreach (var msg in response.Messages)
    {
        try
        {
            ProcessOrder(msg.Body);
            await msg.AckAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Processing failed (attempt {msg.ReceiveCount}): {ex.Message}");
            await msg.NAckAsync();
        }
    }
    ```
  </Tab>

  <Tab value="Kotlin">
    ```kotlin title="SimpleRetry.kt"
    val response = client.receiveQueueMessages(
        channel = "orders", maxMessages = 1, waitTimeoutSeconds = 5)

    for (msg in response.messages) {
        try {
            processOrder(msg.body)
            msg.ack()
        } catch (e: Exception) {
            println("Processing failed (attempt ${msg.receiveCount}): ${e.message}")
            msg.nack()
        }
    }
    ```
  </Tab>

  <Tab value="C++">
    ```cpp title="simple_retry.cpp"
    auto response = client.receiveQueueMessages("orders", 1, 5);
    for (const auto& msg : response.messages) {
        try {
            processOrder(msg.body);
            msg.ack();
        } catch (const std::exception& e) {
            std::cerr << "Processing failed (attempt " << msg.receiveCount
                      << "): " << e.what() << std::endl;
            msg.nack();
        }
    }
    ```
  </Tab>

  <Tab value="Rust">
    ```rust title="simple_retry.rs"
    let mut receiver = client.new_queue_downstream_receiver().await?;

    let response = receiver
        .poll(PollRequest {
            channel: "orders".to_string(),
            max_items: 1,
            wait_timeout_seconds: 5,
            auto_ack: false,
        })
        .await?;

    for msg in &response.messages {
        let attempt = msg.message.attributes.as_ref().map_or(0, |a| a.receive_count);
        match process_order(&msg.message.body) {
            Ok(()) => msg.ack().await?,
            Err(e) => {
                eprintln!("Processing failed (attempt {}): {}", attempt, e);
                // nack returns the message to the queue for immediate redelivery
                msg.nack().await?;
            }
        }
    }
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby title="simple_retry.rb"
    receiver = client.create_downstream_receiver
    request = KubeMQ::Queues::QueuePollRequest.new(
      channel: 'orders', max_items: 1, wait_timeout: 5)
    response = receiver.poll(request)

    response.messages.each do |msg|
      begin
        process_order(msg.body)
        msg.ack
      rescue StandardError => e
        attempt = msg.attributes&.receive_count
        puts "Processing failed (attempt #{attempt}): #{e.message}"
        # nack returns the message to the queue for immediate redelivery
        msg.nack
      end
    end
    ```
  </Tab>

  <Tab value="Elixir">
    ```elixir title="simple_retry.exs"
    {:ok, poll} =
      KubeMQ.Client.poll_queue(client,
        channel: "orders",
        max_items: 1,
        wait_timeout: 5_000
      )

    Enum.each(poll.messages, fn msg ->
      attempt = if msg.attributes, do: msg.attributes.receive_count, else: 0

      case process_order(msg.body) do
        :ok ->
          KubeMQ.PollResponse.ack_all(poll)

        {:error, reason} ->
          IO.puts("Processing failed (attempt #{attempt}): #{inspect(reason)}")
          # nack returns the message to the queue for immediate redelivery
          KubeMQ.PollResponse.nack_all(poll)
      end
    end)
    ```
  </Tab>
</Tabs>

## Exponential Backoff with DLQ Fallback [#exponential-backoff-with-dlq-fallback]

Combine nack-based retry with a delay requeue for exponential backoff, and a DLQ for final failure.

<Tabs groupId="language" items="['Go', 'Python', 'Node.js', 'Java', 'C#', 'Kotlin', 'C++', 'Rust', 'Ruby', 'Elixir']">
  <Tab value="Go">
    ```go title="backoff_retry.go"
    resp, err := client.PollQueue(ctx, &kubemq.PollRequest{
        Channel:            "orders",
        MaxItems:           1,
        WaitTimeoutSeconds: 5,
    })
    if err != nil {
        log.Fatal(err)
    }

    for _, m := range resp.Messages {
        err := processOrder(m.Message.Body)
        if err == nil {
            resp.AckAll()
            return
        }

        attempt := int(m.Message.Attributes.ReceiveCount)
        if attempt >= 5 {
            log.Printf("Max retries exceeded, sending to DLQ")
            resp.ReQueueAll("orders.dlq")
            return
        }

        delay := 1 << attempt // 2, 4, 8, 16 seconds
        log.Printf("Retry %d in %ds", attempt, delay)

        retryMsg := kubemq.NewQueueMessage().
            SetChannel("orders").
            SetBody(m.Message.Body).
            SetMetadata(m.Message.Metadata).
            SetTags(m.Message.Tags).
            SetDelaySeconds(delay)

        client.SendQueueMessage(ctx, retryMsg)
        resp.AckAll()
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python title="backoff_retry.py"
    response = client.receive_queue_messages(
        channel="orders", max_messages=1, wait_timeout_in_seconds=5)

    for msg in response.messages:
        try:
            process_order(msg.body)
            msg.ack()
        except Exception as e:
            attempt = msg.receive_count

            if attempt >= 5:
                print("Max retries exceeded, sending to DLQ")
                msg.requeue("orders.dlq")
                continue

            delay = 2 ** attempt
            print(f"Retry {attempt} in {delay}s")

            client.send_queue_message(QueueMessage(
                channel="orders",
                body=msg.body,
                metadata=msg.metadata,
                delay_in_seconds=delay,
            ))
            msg.ack()
    ```
  </Tab>

  <Tab value="Node.js">
    ```typescript title="backoff_retry.ts"
    const messages = await client.receiveQueueMessages({
      channel: 'orders',
      maxMessages: 1,
      waitTimeoutSeconds: 5,
    });

    for (const msg of messages) {
      try {
        await processOrder(msg.body);
        await msg.ack();
      } catch (err) {
        const attempt = msg.receiveCount;

        if (attempt >= 5) {
          console.log('Max retries exceeded, sending to DLQ');
          await msg.requeue('orders.dlq');
          continue;
        }

        const delay = Math.pow(2, attempt);
        console.log(`Retry ${attempt} in ${delay}s`);

        await client.sendQueueMessage(
          createQueueMessage({
            channel: 'orders',
            body: msg.body,
            metadata: msg.metadata,
            policy: { delaySeconds: delay },
          }),
        );
        await msg.ack();
      }
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java title="BackoffRetry.java"
    ReceiveQueueMessagesResponse response = client.receiveQueueMessages(
        ReceiveQueueMessagesRequest.builder()
            .channel("orders").maxMessages(1).waitTimeoutSeconds(5).build());

    for (QueueMessageReceived msg : response.getMessages()) {
        try {
            processOrder(msg.getBody());
            msg.ack();
        } catch (Exception e) {
            int attempt = msg.getReceiveCount();

            if (attempt >= 5) {
                System.out.println("Max retries exceeded, sending to DLQ");
                msg.requeue("orders.dlq");
                continue;
            }

            int delay = (int) Math.pow(2, attempt);
            System.out.printf("Retry %d in %ds%n", attempt, delay);

            client.sendQueueMessage(QueueMessage.builder()
                .channel("orders").body(msg.getBody())
                .delaySeconds(delay).build());
            msg.ack();
        }
    }
    ```
  </Tab>

  <Tab value="C#">
    ```csharp title="BackoffRetry.cs"
    var response = await client.ReceiveQueueMessagesAsync(new ReceiveQueueMessagesRequest
    {
        Channel = "orders", MaxMessages = 1, WaitTimeoutSeconds = 5,
    });

    foreach (var msg in response.Messages)
    {
        try
        {
            ProcessOrder(msg.Body);
            await msg.AckAsync();
        }
        catch (Exception)
        {
            var attempt = msg.ReceiveCount;

            if (attempt >= 5)
            {
                Console.WriteLine("Max retries exceeded, sending to DLQ");
                await msg.ReQueueAsync("orders.dlq");
                continue;
            }

            var delay = (int)Math.Pow(2, attempt);
            Console.WriteLine($"Retry {attempt} in {delay}s");

            await client.SendQueueMessageAsync(new QueueMessage
            {
                Channel = "orders",
                Body = msg.Body.ToArray(),
                DelaySeconds = delay
            });
            await msg.AckAsync();
        }
    }
    ```
  </Tab>

  <Tab value="Kotlin">
    ```kotlin title="BackoffRetry.kt"
    val response = client.receiveQueueMessages(
        channel = "orders", maxMessages = 1, waitTimeoutSeconds = 5)

    for (msg in response.messages) {
        try {
            processOrder(msg.body)
            msg.ack()
        } catch (e: Exception) {
            val attempt = msg.receiveCount

            if (attempt >= 5) {
                println("Max retries exceeded, sending to DLQ")
                msg.requeue("orders.dlq")
                continue
            }

            val delay = 2.0.pow(attempt.toDouble()).toInt()
            println("Retry $attempt in ${delay}s")

            client.sendQueueMessage(QueueMessage(
                channel = "orders", body = msg.body, delaySeconds = delay))
            msg.ack()
        }
    }
    ```
  </Tab>

  <Tab value="C++">
    ```cpp title="backoff_retry.cpp"
    auto response = client.receiveQueueMessages("orders", 1, 5);
    for (const auto& msg : response.messages) {
        try {
            processOrder(msg.body);
            msg.ack();
        } catch (const std::exception& e) {
            int attempt = msg.receiveCount;

            if (attempt >= 5) {
                std::cout << "Max retries exceeded, sending to DLQ" << std::endl;
                msg.requeue("orders.dlq");
                continue;
            }

            int delay = static_cast<int>(std::pow(2, attempt));
            std::cout << "Retry " << attempt << " in " << delay << "s" << std::endl;

            kubemq::QueueMessage retryMsg;
            retryMsg.channel = "orders";
            retryMsg.body = msg.body;
            retryMsg.delaySeconds = delay;
            client.sendQueueMessage(retryMsg);
            msg.ack();
        }
    }
    ```
  </Tab>

  <Tab value="Rust">
    ```rust title="backoff_retry.rs"
    let mut receiver = client.new_queue_downstream_receiver().await?;

    let response = receiver
        .poll(PollRequest {
            channel: "orders".to_string(),
            max_items: 1,
            wait_timeout_seconds: 5,
            auto_ack: false,
        })
        .await?;

    for msg in &response.messages {
        if process_order(&msg.message.body).is_ok() {
            msg.ack().await?;
            continue;
        }

        let attempt = msg.message.attributes.as_ref().map_or(0, |a| a.receive_count);
        if attempt >= 5 {
            println!("Max retries exceeded, sending to DLQ");
            msg.re_queue("orders.dlq").await?;
            continue;
        }

        let delay = 1 << attempt; // 2, 4, 8, 16 seconds
        println!("Retry {} in {}s", attempt, delay);

        let retry_msg = QueueMessageBuilder::new()
            .channel("orders")
            .body(msg.message.body.clone())
            .metadata(&msg.message.metadata)
            .delay_seconds(delay)
            .build();

        client.send_queue_message(retry_msg).await?;
        msg.ack().await?;
    }
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby title="backoff_retry.rb"
    receiver = client.create_downstream_receiver
    request = KubeMQ::Queues::QueuePollRequest.new(
      channel: 'orders', max_items: 1, wait_timeout: 5)
    response = receiver.poll(request)

    response.messages.each do |msg|
      begin
        process_order(msg.body)
        msg.ack
      rescue StandardError
        attempt = msg.attributes&.receive_count || 0

        if attempt >= 5
          puts 'Max retries exceeded, sending to DLQ'
          # Re-send the message to the DLQ channel, then ack the original.
          client.send_queue_message(KubeMQ::Queues::QueueMessage.new(
            channel: 'orders.dlq', metadata: msg.metadata, body: msg.body))
          msg.ack
          next
        end

        delay = 2**attempt
        puts "Retry #{attempt} in #{delay}s"

        policy = KubeMQ::Queues::QueueMessagePolicy.new(delay_seconds: delay)
        client.send_queue_message(KubeMQ::Queues::QueueMessage.new(
          channel: 'orders', metadata: msg.metadata, body: msg.body, policy: policy))
        msg.ack
      end
    end
    ```
  </Tab>

  <Tab value="Elixir">
    ```elixir title="backoff_retry.exs"
    {:ok, poll} =
      KubeMQ.Client.poll_queue(client,
        channel: "orders",
        max_items: 1,
        wait_timeout: 5_000
      )

    Enum.each(poll.messages, fn msg ->
      case process_order(msg.body) do
        :ok ->
          KubeMQ.PollResponse.ack_all(poll)

        {:error, _reason} ->
          attempt = if msg.attributes, do: msg.attributes.receive_count, else: 0

          if attempt >= 5 do
            IO.puts("Max retries exceeded, sending to DLQ")
            KubeMQ.PollResponse.requeue_all(poll, "orders.dlq")
          else
            delay = Bitwise.bsl(1, attempt) # 2, 4, 8, 16 seconds
            IO.puts("Retry #{attempt} in #{delay}s")

            retry_msg =
              KubeMQ.QueueMessage.new(
                channel: "orders",
                metadata: msg.metadata,
                body: msg.body,
                policy: KubeMQ.QueuePolicy.new(delay_seconds: delay)
              )

            KubeMQ.Client.send_queue_message(client, retry_msg)
            KubeMQ.PollResponse.ack_all(poll)
          end
      end
    end)
    ```
  </Tab>
</Tabs>

## Retry Strategy Comparison [#retry-strategy-comparison]

| Strategy            | Latency                 | Load              | Complexity | Best For               |
| ------------------- | ----------------------- | ----------------- | ---------- | ---------------------- |
| Immediate nack      | Instant retry           | High (tight loop) | Low        | Transient errors       |
| Fixed delay requeue | Constant wait           | Moderate          | Medium     | Rate-limited APIs      |
| Exponential backoff | Increasing wait         | Low               | Medium     | External service calls |
| Backoff + DLQ       | Increasing + final stop | Lowest            | Higher     | Production workloads   |

## Next Steps [#next-steps]

<Cards>
  <Card title="Dead Letter Queue" href="/learn/queues/tutorials/dead-letter-queue" description="Automatic DLQ routing with maxReceiveCount." />

  <Card title="Webhook Delivery" href="/learn/queues/scenarios/webhook-delivery" description="Retry pattern applied to webhook delivery." />
</Cards>
