Queue change in requeue_message: delay issue

Hello!

I am using KumoMTA latest version 2025.12.02-67ee9e96

I’m experimenting with the requeue_message hook using a simple setup.

I have this configuration:

kumo.on(
  'requeue_message',
  function(msg, smtp_response, insert_context, increment_attempts, delay)
    msg:set_meta('queue', 'test-queue')
  end
)

kumo.on('get_queue_config', function(domain, tenant, campaign_or_routing_domain)
  if domain == 'test-queue' then
    return kumo.make_queue_config { retry_interval = "2 minutes" }
  end
  if domain == 'base-queue' then
    return kumo.make_queue_config { retry_interval = "5 minutes" }
  end
end)

I also have another queue called base-queue.

With this setup, a message is first sent through base-queue.
When a transient failure occurs, it enters the requeue_message hook, the queue is changed to test-queue, and Kumo immediately retries sending the message. This matches what I expected from reading the documentation/code.

However, I want the message to be moved to the new queue and have its delay computed according to the new queue’s retry policy.

So I tried this:

kumo.on(
  'requeue_message',
  function(msg, smtp_response, insert_context, increment_attempts, delay)
    msg:set_meta('queue', 'test-queue')
    msg:set_due(kumo.time.now() + kumo.time.parse_duration('1m'))
  end
)

But what happens is:

  • The message is received in base-queue
  • A transient failure occurs
  • requeue_message runs and changes the queue to test-queue
  • The message remains in base-queue for the first retry, using base-queue’s delay policy
  • Only subsequent retries are sent from test-queue

Why does the first retry still happen from base-queue (when adding msg:set_due(kumo.time.now() + kumo.time.parse_duration('1m')) ?
And more generally, how can I prevent an immediate retry when changing the queue in requeue_message?

Thank you for your help :slightly_smiling_face:

When requeue changes the queue, the message is always considered to be immediately eligible for delivery, as it has been “re-bound” to a new queue and must therefore needs to be re-assessed.

rebinding the queue doesn’t change the number of attempts, which is the thing that we use to decide where in the retry schedule we are, so in your original example:

  1. Message received, put into base-queue and is immediately due
  2. Message transfails, requeue-message event puts it into test-queue. Since it was rebound, it is immediately due again, but num-attempts is incremented (0->1)
  3. Message transfails, requeue-message doesn’t change the queue, num-attempts is incremented (1->2). Its next due time is 2 + (2*2) + (2*2*2) minutes (jittered) because of exponential backoff for num_attempts=2

If you want to ensure that a message that you rebind in requeue-message is not sent before you want it, then you can use set_scheduling - KumoMTA Docs to set a constraint to prevent delivering before you want it. You can use the kumo.time functions to compute that time.

Thank you for your answer, so i did try using set_scheduling :

kumo.on(
  'requeue_message',
  function(msg, smtp_response, insert_context, increment_attempts, delay)
    msg:set_meta('queue', 'test-queue')
    msg:set_scheduling { first_attempt = kumo.time.now() + kumo.time.parse_duration('1m') }
  end
)

kumo.on('get_queue_config', function(domain, tenant, campaign_or_routing_domain)
  if domain == 'test-queue' then
    return kumo.make_queue_config { retry_interval = "2 minutes" }
  end
  if domain == 'base-queue' then
    return kumo.make_queue_config { retry_interval = "5 minutes" }
  end
end)

So i send a message => TransientFailure => enters requeue_message => msg still appears in old queue and has it’s delay computed from the old queue config (5 minutes) instead of the time set with set_scheduling first_attempt (1 minute) or having it computed from the new queue config (2 minutes)

/opt/kumomta/sbin/kcli inspect-sched-q base-queue
{
  "queue_name": "base-queue",
  "messages": [
    {
      "id": "1ccffbd3068d11f1b1c2005056a1933a",
      "message": {
        "sender": "xxx@xxx.xxx.xxx",
        "recipient": "xxxxxxx@xxxx.xxx",
        "meta": {
          "campaign": "xxx.xxx.xxx",
          "hostname": "xxxxx",
          "http_auth": "127.0.0.1",
          "queue": "test-queue",
          "received_from": "127.0.0.1",
          "received_via": "0.0.0.0",
          "reception_protocol": "HTTP",
          "tenant": "dev_2361f0c9-c149-40cf-b651-633f955ab655",
          "x_customer_id": "2361f0c9-c149-40cf-b651-633f955ab655",
          "x_dkim_selectors": "dkimdomain",
          "x_email_purpose": "TRANSACTIONAL",
          "x_routing_message_id": "719fcb84-53d0-4f74-b5bf-0d4ec068661a",
          "x_tenant_id": "dev"
        },
        "data": null,
        "due": "2026-02-10T14:35:57.136008306Z",
        "num_attempts": 1
      }
    }
  ],
  "num_scheduled": 1,
  "queue_config": {
    "egress_pool": "pool-TRANSACTIONAL-MEDIUM-default",
    "max_age": "1day",
    "max_message_rate": null,
    "max_retry_interval": null,
    "protocol": {
      "smtp": {
        "mx_list": []
      }
    },
    "provider_name": null,
    "reap_interval": "10m",
    "refresh_interval": "1m",
    "refresh_strategy": "Ttl",
    "retry_interval": "5m",
    "shrink_policy": [],
    "strategy": "SingletonTimerWheel",
    "timerwheel_tick_interval": null
  },
  "delayed_metric": 1,
  "now": "2026-02-10T14:31:33.036866704Z",
  "last_changed": "2026-02-10T14:30:57.136002121Z"
}

Is it normal expected behavior or did i miss something?

do you see any error messages in the diagnostic log?

AH yes there was an error, i though set_scheduling first_attempt accepted a time object but it accepts a string
replacing with

local due_time = kumo.time.now() + kumo.time.parse_duration('1m')
msg:set_scheduling { first_attempt = due_time.rfc3339 }

now everything works fine & the message is delayed by 1min

Thank you !