Hi everyone, I noticed something unusual with `rec...
# general
g
Hi everyone, I noticed something unusual with
reconsumeLater
. When I set
delayTime
to around 1 second (or less), the message is redelivered immediately. I wrote a small integration test to reproduce this behavior (the test requires a valid Docker installation to start a Pulsar container). When running the
TestReconsumeLater.java
test, if the constant
REDELIVER_DELAY
is set to less than 1010 ms, the test fails because the message is redelivered in 34 ms instead of the expected delay:
Copy code
java.lang.AssertionError:
Expected actual:
  34L
to be greater than or equal to:
  1009L
Is this a known limitation ? Is it documented anywhere ?
d
Not a bug — what you’re hitting is documented (if subtle) behavior of the in-memory delayed delivery tracker. Your 1010ms cutoff is the smoking gun. Two broker defaults are at play: -
delayedDeliveryTickTimeMillis=1000
— the HashedWheelTimer resolution backing delayed delivery. -
isDelayedDeliveryDeliverAtTimeStrict=false
— the broker is allowed to deliver messages up to one tick early. The tracker’s add path is essentially: if (deliverAt <= now + tickTime) { // don’t bother tracking, deliver on the next dispatch return false; } So any delay ≤ tickTime gets short-circuited — the message never enters the delayed index, and the dispatcher picks it up on the next cycle (~tens of ms). That matches your 34ms observation exactly. At delay=1010ms,
deliverAt
clears
now + tickTime
by ~10ms, so the tracker accepts it, and you get the real delay.
reconsumeLater
is built on top of the retry letter topic, which republishes the message with a
deliverAt
property. So it inherits the same floor — it isn’t a separate code path with its own delay semantics. This is documented under the “Broker” section in https://pulsar.apache.org/docs/next/concepts-messaging/#broker, though it’s easy to miss. If you actually need sub-second delays, you have a few options: 1. Set
isDelayedDeliveryDeliverAtTimeStrict=true
in
broker.conf
. The cutoff becomes
now
instead of
now + tickTime
, so a 500ms delay actually waits ~500ms (within tick-time precision on the late side). 2. Reduce
delayedDeliveryTickTimeMillis
to ~100ms. Gives you sub-second precision and shrinks the early-delivery window. Cost is more timer wake-ups, but the overhead is small unless the tracker is very large. 3. Floor your delays above tickTime. If you don’t control the broker config, just pad above 1s. We ended up doing exactly this for a rate-limit retry scenario where ‘a bit early’ would re-trigger the same upstream 429 — added a fixed 1s and floored everything at 2s. For your case, option 1 is probably what you want. If the delays you’re scheduling are time-sensitive enough that 34ms vs. 500ms matters, strict mode is the cleanest fix and doesn’t require padding everywhere in your application code.
g
Thank you for your very clear answer.
d
If you want to verify this yourself with your existing test, you don’t need to rebuild anything — the Pulsar Docker image picks up broker config via env vars prefixed with
PULSAR_PREFIX_
. Add them to your container setup: PulsarContainer pulsar = new PulsarContainer(...) .withEnv(“PULSAR_PREFIX_isDelayedDeliveryDeliverAtTimeStrict”, “true”) .withEnv(“PULSAR_PREFIX_delayedDeliveryTickTimeMillis”, “100"); A few things worth trying: Flip just
isDelayedDeliveryDeliverAtTimeStrict=true
and leave the tick at its default. Re-run with
REDELIVER_DELAY=500
and you should see the observed delay land somewhere between ~500ms and ~1500ms — the message actually enters the delayed index now, but it can still wait up to one tick after
deliverAt
before getting dispatched. That alone proves the early-delivery shortcut was the culprit. Then add
delayedDeliveryTickTimeMillis=100
on top. Same 500ms test, and now you should see ~500–600ms. That’s the precision knob doing its job. If you’re curious about the trade-off, try setting only the smaller tick (leave strict at the default
false
). You’ll still get early delivery, but the window shrinks from “up to 1000ms early” to “up to 100ms early.” Useful for understanding why both knobs exist independently — one bounds how early delivery can happen, the other bounds how late. Have fun with it — it’s a pretty satisfying thing to see the behavior change cleanly as you flip each setting.