Introduction

Azure Service Bus uses a PeekLock delivery mode where messages are locked for a configurable duration (default 30 seconds) while being processed. If processing takes longer than the lock duration and the lock is not renewed, the message becomes visible to other receivers, causing AbandonedException, duplicate processing, or message loss. The .NET SDK provides automatic lock renewal, but it must be configured correctly and the message must be completed within the renewal window to avoid issues.

Symptoms

  • ServiceBusException: The lock supplied is invalid during CompleteMessageAsync
  • Message processed multiple times by different consumers
  • AbandonedException thrown after long-running processing
  • Messages return to queue after partial processing
  • Auto-complete enabled but processing fails silently

Error output: `` Azure.Messaging.ServiceBus.ServiceBusException: The lock supplied is invalid. Either the lock expired, or the message has already been removed from the queue. Reference:abc123, TrackingId:def456

Common Causes

  • Processing time exceeds message lock duration (default 30 seconds)
  • MaxAutoLockRenewalDuration not configured or set too low
  • Message not completed (CompleteMessageAsync) after processing
  • AutoCompleteMessages set to true but processing throws exception
  • Long-running operations without checkpointing or progress tracking

Step-by-Step Fix

  1. 1.Configure lock renewal for long-running message processing:
  2. 2.```csharp
  3. 3.var client = new ServiceBusClient(connectionString);

var options = new ServiceBusProcessorOptions { AutoCompleteMessages = false, MaxConcurrentCalls = 1, // Set to TimeSpan.Zero for infinite renewal (renews until processing completes) MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(10), ReceiveMode = ServiceBusReceiveMode.PeekLock };

var processor = client.CreateProcessor("my-queue", options);

processor.ProcessMessageAsync += async (args) => { var message = args.Message; var body = message.Body.ToString();

_logger.LogInformation("Processing message: {MessageId}", message.MessageId);

try { // This can take longer than the default 30s lock await ProcessOrderAsync(body, args.CancellationToken);

// Complete AFTER processing is done await args.CompleteMessageAsync(message, args.CancellationToken); _logger.LogInformation("Message completed: {MessageId}", message.MessageId); } catch (Exception ex) { _logger.LogError(ex, "Failed to process message: {MessageId}", message.MessageId); await args.AbandonMessageAsync(message, new Dictionary<string, object> { { "ErrorReason", ex.Message }, { "ErrorTime", DateTime.UtcNow.ToString("O") } }, args.CancellationToken); } };

processor.ProcessErrorAsync += (args) => { _logger.LogError(args.Exception, "Service Bus error: {ErrorSource}", args.ErrorSource); return Task.CompletedTask; };

await processor.StartProcessingAsync(); ```

  1. 1.Manually renew lock for very long operations:
  2. 2.```csharp
  3. 3.processor.ProcessMessageAsync += async (args) =>
  4. 4.{
  5. 5.var message = args.Message;
  6. 6.var cts = new CancellationTokenSource();

try { // Simulate multi-step processing that takes minutes for (int step = 1; step <= 5; step++) { _logger.LogInformation("Step {Step}/5 for message {MessageId}", step, message.MessageId);

await ProcessStepAsync(step, message.Body.ToString(), cts.Token);

// Manually renew lock before it expires await args.RenewMessageLockAsync(message, cts.Token); _logger.LogInformation("Lock renewed for message {MessageId}", message.MessageId); }

await args.CompleteMessageAsync(message, cts.Token); } catch (OperationCanceledException) { _logger.LogWarning("Processing cancelled for message {MessageId}", message.MessageId); await args.AbandonMessageAsync(message, cts.Token); } catch (Exception ex) { _logger.LogError(ex, "Error processing message {MessageId}", message.MessageId); await args.AbandonMessageAsync(message, cts.Token); } }; ```

  1. 1.Configure lock duration at queue/subscription level:
  2. 2.```csharp
  3. 3.// Using Azure.ResourceManager.ServiceBus or Azure portal/CLI
  4. 4.// Queue lock duration can be set from 30 seconds to 5 minutes

// Azure CLI - create queue with 5-minute lock duration az servicebus queue create \ --resource-group MyResourceGroup \ --namespace-name MyNamespace \ --name MyQueue \ --lock-duration "PT5M" \ --max-delivery-count 5 \ --default-message-time-to-live "P14D"

// Azure CLI - update existing queue az servicebus queue update \ --resource-group MyResourceGroup \ --namespace-name MyNamespace \ --name MyQueue \ --lock-duration "PT5M"

// For operations longer than 5 minutes, use session-based processing // or break work into smaller chunks with state tracking ```

  1. 1.Use session-aware processing for ordered long-running work:
  2. 2.```csharp
  3. 3.var sessionProcessor = client.CreateSessionProcessor(
  4. 4."my-session-queue",
  5. 5.new ServiceBusSessionProcessorOptions
  6. 6.{
  7. 7.MaxConcurrentSessions = 1,
  8. 8.MaxAutoLockRenewalDuration = TimeSpan.FromHours(1),
  9. 9.TryLockTimeout = TimeSpan.FromSeconds(10)
  10. 10.});

sessionProcessor.ProcessMessageAsync += async (args) => { // Session lock is automatically renewed for the configured duration _logger.LogInformation("Processing session: {SessionId}", args.SessionId); await ProcessSessionMessageAsync(args.Message, args.CancellationToken); await args.CompleteMessageAsync(args.Message, args.CancellationToken); };

await sessionProcessor.StartProcessingAsync(); ```

Prevention

  • Set MaxAutoLockRenewalDuration based on worst-case processing time
  • Use AutoCompleteMessages = false for explicit completion control
  • Add Dead Letter handling for messages that fail repeatedly
  • Monitor DeliveryCount to detect messages being reprocessed
  • Break long-running operations into smaller steps with state persistence
  • Use Service Bus sessions for ordered processing with longer locks
  • Set up alerts on the dead-letter queue for failed messages
  • Log message lock renewal events for production troubleshooting