Grégory (Kosmos)
05/29/2026, 3:42 PMreconsumeLater. 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:
java.lang.AssertionError:
Expected actual:
34L
to be greater than or equal to:
1009L
Is this a known limitation ? Is it documented anywhere ?David K
05/29/2026, 3:56 PMdelayedDeliveryTickTimeMillis=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.Grégory (Kosmos)
05/29/2026, 3:59 PMDavid K
05/29/2026, 4:30 PMPULSAR_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.