Retries and delays
Fibril separates three related behaviors:
- immediate retry after a consumer rejects work
- redelivery after a lease expires
- delayed delivery or delayed retry after a specific timestamp
Immediate retry
Section titled “Immediate retry”Manual-ack consumers can request immediate requeue:
msg.retry().await?;await msg.retry();await msg.retry()msg.Retry()msg.Retry();At the state layer, this removes the offset from inflight, increments the retry count, and returns the offset to ready unless the retry policy is exhausted.
Lease expiry
Section titled “Lease expiry”When a message is delivered, it becomes inflight with a lease deadline. If the consumer disappears or does not settle the message, the broker checks for expired inflight messages and returns them to ready.
This is the core failure-recovery path for best-effort at-least-once delivery.
Delayed publish
Section titled “Delayed publish”Fibril persists delayed-delivery state. Messages can be held until not_before, and delayed-delivery state is included in recovery snapshots.
The clients expose delayed publish methods:
publisher.publish_delayed(payload, std::time::Duration::from_secs(30)).await?;publisher.publish_delayed_confirmed(payload, std::time::Duration::from_secs(30)).await?;await publisher.publishDelayed(payload, 30_000);await publisher.publishDelayedConfirmed(payload, 30_000);await publisher.publish_delayed(payload, 30)await publisher.publish_delayed_confirmed(payload, 30)publisher.PublishDelayed(ctx, payload, 30*time.Second)publisher.PublishDelayedConfirmed(ctx, payload, 30*time.Second)await publisher.PublishDelayedAsync(payload, TimeSpan.FromSeconds(30));await publisher.PublishDelayedConfirmedAsync(payload, TimeSpan.FromSeconds(30));Delay units differ by client: Rust a Duration, TypeScript milliseconds by default (or an explicit { seconds } / { ms } / { minutes }, or a Date deadline), Python seconds (or a timedelta), Go a time.Duration, C# a TimeSpan.
The delayed publish path uses a distinct protocol frame instead of adding an optional delay field to the common publish frame.
Delayed retry
Section titled “Delayed retry”Manual-ack consumers can ask the broker to retry a message after a delay. Fibril records a not_before Unix-millisecond deadline on the settlement event, keeps the offset out of ready delivery until that deadline, and then makes it eligible for redelivery.
msg.retry_after(std::time::Duration::from_secs(30)).await?;msg.retry_after(std::time::Duration::from_millis(250)).await?;await msg.retryAfter(30_000);await msg.retryAfter(new Date(Date.now() + 30_000));await msg.retry_after(30)msg.RetryAfter(30 * time.Second)msg.RetryAfter(TimeSpan.FromSeconds(30));The delay units match delayed publish for each client.
Message TTL (drop by age)
Section titled “Message TTL (drop by age)”A message can be dropped if it is not consumed before a deadline. This is the work-queue “do not process stale work” behavior, and it is distinct from queue expiration (auto-deleting an idle queue), which is not implemented.
Set a TTL per message, or a per-queue default that applies when a message carries no TTL of its own. A per-message TTL wins over the queue default. With neither set a message never expires. The owner resolves the deadline against its own clock at publish, so it survives recovery and replication.
An expired message is never dropped while it is in flight. When it does
drop, it follows the queue’s dead-letter policy: discarded when no DLQ is
configured, otherwise dead-lettered with reason expired.
Per-message TTL via an expiring publisher (a per-message WithTTL/with_ttl
still overrides the default):
let publisher = client.publisher("rpc.reply")?.expiring(std::time::Duration::from_secs(30));publisher.publish(reply).await?;const publisher = client.publisher("rpc.reply").expiring(30_000);await publisher.publish(reply);publisher = client.publisher("rpc.reply").expiring(30)await publisher.publish(reply)publisher := client.Publisher("rpc.reply").Expiring(30 * time.Second)publisher.Publish(ctx, fibril.Text("reply"))var publisher = client.Publisher("rpc.reply").Expiring(TimeSpan.FromSeconds(30));await publisher.PublishAsync(Message.Text("reply"));Per-queue default TTL at declare time:
client .declare_queue(QueueConfig::new("rpc.reply")?.default_message_ttl(std::time::Duration::from_secs(30))) .await?;await client.declareQueue( new QueueConfig("rpc.reply").defaultMessageTtl(30_000),);await client.declare_queue(QueueConfig("rpc.reply").default_message_ttl(30))client.DeclareQueue(ctx, fibril.NewQueueConfig("rpc.reply"). DefaultMessageTTL(30*time.Second))await client.DeclareQueueAsync("rpc.reply", new QueueConfig{ DefaultMessageTtl = TimeSpan.FromSeconds(30),});The broker resolves the deadline from Publish.ttl_ms (or the queue’s
default_message_ttl_ms) and the expiry worker drops expired ready messages on
its normal tick.