jstedfast/MailKit · error · InvalidOperationException
The IMAP command queue is empty.
Error message
The IMAP command queue is empty.
What it means
ImapEngine.PopNextCommand() throws this InvalidOperationException when it is asked to dequeue the next command to run but the internal command queue has zero entries. This is an internal synchronization invariant of the IMAP pipeline: a thread signaled the engine to run a command, but nothing was enqueued. It almost always indicates a race condition in caller thread usage of ImapClient.
Solutions
- Wrap every ImapClient call sequence in lock (imapClient.SyncRoot) { ... } so only one thread enqueues and processes commands at a time
- Use one thread/task per ImapClient, or create a separate ImapClient (and connection) per thread
- Check for double-consume bugs: ensure no code path signals/dispatches the queue more than once per enqueued command
- Upgrade MailKit - older versions had queue races; newer releases serialize via the engine task
Example fix
// before
await imapClient.Inbox.GetMessageAsync(id);
Task.Run(() => imapClient.Noop()); // unsynchronized
// after
lock (imapClient.SyncRoot) {
imapClient.Noop();
}
await imapClient.Inbox.GetMessageAsync(id); Defensive patterns
Strategy: try-catch
Validate before calling
if (queueIsEmptySignaled && !clientIsBusy) {
// safe to proceed; otherwise synchronize first
}
lock (client.SyncRoot) {
bool busy = client.IsBusy;
} Type guard
bool CanIssueCommand(ImapClient client) {
lock (client.SyncRoot) { return !client.IsBusy && client.IsConnected && client.IsAuthenticated; }
} Try / catch
lock (client.SyncRoot) {
try {
// IMAP operations here
} catch (InvalidOperationException ex) when (ex.Message.Contains("command queue is empty") || ex.Message.Contains("currently busy")) {
// log and re-serialize access; do not retry blindly
}
} Prevention
- Always lock SyncRoot around Sync API usage
- Prefer async APIs which serialize internally
- One client per thread of work
When it happens
Trigger: Calling ImapClient send/authenticate/folder APIs concurrently from multiple threads without synchronizing on ImapClient.SyncRoot; calling methods after another thread has already drained the queue; mixing Sync and Async calls on the same client from different threads.
Common situations: Multi-threaded apps (e.g. background workers plus UI threads) sharing a single ImapClient instance; a keepalive/NOOP thread racing a fetch thread; incorrectly hand-rolled synchronization instead of locking SyncRoot.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- The ImapClient is currently busy processing a command in…
- The IMAP server did not return the requested message.
- The IMAP server does not support the THREAD extension.
- Value cannot be null. (Parameter 'name')
- Value cannot be null. (Parameter 'rights')
AI-assisted analysis of jstedfast/MailKit@9d3859a785 (2026-09-15).
Data as JSON: /api/errors/d066fdabf9d77c70.
Report an issue: GitHub.
Appendix: source
Thrown at MailKit/Net/Imap/ImapEngine.cs:3133
token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. {0}", token);
} else if (atom.Equals ("VANISHED", StringComparison.OrdinalIgnoreCase) && folder != null) {
await folder.OnVanishedAsync (this, cancellationToken).ConfigureAwait (false);
await SkipLineAsync (cancellationToken).ConfigureAwait (false);
} else {
// don't know how to handle this... eat it?
await SkipLineAsync (cancellationToken).ConfigureAwait (false);
}
}
}
[MemberNotNull (nameof (current))]
void PopNextCommand ()
{
lock (queue) {
if (queue.Count == 0)
throw new InvalidOperationException ("The IMAP command queue is empty.");
if (IsBusy)
throw new InvalidOperationException ("The ImapClient is currently busy processing a command in another thread. Lock the SyncRoot property to properly synchronize your threads.");
current = queue[0];
queue.RemoveAt (0);
try {
current.CancellationToken.ThrowIfCancellationRequested ();
} catch {
queue.RemoveAll (x => x.CancellationToken.IsCancellationRequested);
current = null;
throw;
}
}
}
/// <summary>View on GitHub (pinned to 9d3859a785)