GLOSSARY

What is IMAP IDLE?

IMAP IDLE (RFC 2177) is an IMAP extension that lets a client say it is waiting and keep the connection open, so the server can push untagged notifications about new or changed messages the moment they happen instead of the client polling for them.

Free forever plan · No credit card required · Cancel anytime

Quick definition

IMAP IDLE (RFC 2177) is an IMAP extension that lets a client say it is waiting and keep the connection open, so the server can push untagged notifications about new or changed messages the moment they happen instead of the client polling for them.

In one line: the client says IDLE, the server says + idling, and nobody speaks again until there is news.

What it means

Base IMAP is strictly request and response: the client asks, the server answers, the connection goes quiet. To notice a new message the client has to ask again, which means either checking every few seconds and wasting most of those checks, or checking every few minutes and telling the user their mail is late.

IDLE, defined in RFC 2177 in 1997 and supported by essentially every IMAP server since, inverts that for as long as the client wants. The client issues IDLE, the server answers with a continuation line rather than a completion, and the connection stays open with the client silent. From then until the client sends DONE, the server is free to push untagged responses the instant something changes in the selected mailbox.

It is a small extension with an outsized effect: it is the difference between a CRM that shows a customer reply while the agent is still reading the previous one, and a CRM that shows it two minutes later.

The protocol, exactly

Three details trip up almost every first implementation. First, the server response to IDLE is a command continuation request, a line starting with +, not a tagged OK. The tagged completion only arrives after you end the idle. Second, while idling the connection is not yours to use: you may send nothing except the termination. Third, the termination is the bare word DONE followed by CRLF, with no tag, no arguments and no command name.

Everything the server sends in between is untagged. Those responses are the actual payload of the feature, and handling them correctly is where most of the work is.

Sequence numbers are a trap

EXISTS and EXPUNGE both speak in message sequence numbers, which are positions in the mailbox rather than stable identities. A single EXPUNGE renumbers every message above it, immediately, and it can arrive while you are still processing an earlier response. Fetching by sequence number after that gets you the wrong message, silently, and no error is ever raised.

The correct pattern is to record UIDVALIDITY and UIDNEXT when you select the mailbox, then treat every EXISTS as a signal to run UID FETCH n:* from your stored UIDNEXT, and never to use a sequence number in a command at all. UIDVALIDITY is the escape hatch the server has for saying "forget everything": if it changes, all your stored UIDs are void and the mailbox needs a full resynchronisation.

Even the UID is not an identity across folders. A message moved to another folder gets a new UID there, and on Gmail the same message is visible under several folders at once. That is why the durable key is the Message-ID, with the UID acting only as a cursor.

Connections are the scarce resource

One IDLE watches one mailbox, so a naive design opens a connection per folder per account. Gmail allows 15 simultaneous IMAP connections per account, shared with the user own phone and laptop, and other providers are stricter. Cross the line and you get * BYE and a period during which reconnecting makes it worse.

In practice: idle on the inbox only, poll the other folders on a slow cursor, and cap connections per account rather than per worker. After a deploy, reconnect with jitter. A hundred workers restarting simultaneously and each opening two connections looks exactly like an attack, and the provider responds accordingly.

The failure that is hardest to see

An idle connection that is quiet and an idle connection that is dead look identical from the application side. TCP will not necessarily tell you: a middlebox can drop the flow without sending a reset, so your socket stays open forever and no bytes ever arrive. This is why the 29-minute re-IDLE is not optional. Add TCP keepalives, add an application-level timer that tears the connection down and rebuilds it, and add a slow reconciliation sweep that fetches by UID range regardless of what the push path has told you.

Authentication failures have their own signature. Since Google withdrew basic password access, a plain password produces AUTHENTICATIONFAILED with a link to a help page rather than anything descriptive, and the fix is an App Password or OAuth with XOAUTH2. Treat that response as a permanent credential error that needs the user, not as a transient error to retry, or you will keep hammering a mailbox that will never let you in.

Related concepts

  • Message-ID: the identity that UIDs cannot provide.
  • Webhook: the push model you would use instead if every provider offered one.
  • Bounce rate: bounce messages arrive over the same sync path as everything else.
  • DKIM: the authentication headers you read off the messages this connection delivers.

How Pinlyx handles it

Mailbox sync holds one IDLE connection per connected account on the inbox, with the Sent folder tracked on its own cursor so a message you sent is never re-imported as an inbound reply. The idle is recycled well inside the 29-minute window, reconnects use backoff with jitter so a deploy does not stampede the provider, and a periodic UID-range sweep reconciles anything the push path missed. Credential failures are separated from transient ones and surface as a reconnect prompt on the mailbox rather than a silent retry loop.

On the wire

A complete IDLE session, byte for byte.

Client lines are tagged. Server lines starting with an asterisk are untagged, and the line starting with a plus is a continuation request.

Select, idle, receive, fetch

C: a1 CAPABILITY
S: * CAPABILITY IMAP4rev1 IDLE MOVE CONDSTORE QRESYNC UIDPLUS
S: a1 OK CAPABILITY completed

C: a2 SELECT INBOX
S: * 412 EXISTS
S: * 0 RECENT
S: * OK [UIDVALIDITY 1435678901] UIDs valid
S: * OK [UIDNEXT 913] Predicted next UID
S: a2 OK [READ-WRITE] SELECT completed

C: a3 IDLE
S: + idling
        ... connection stays open, client says nothing ...
S: * 413 EXISTS
S: * 1 RECENT
C: DONE
S: a3 OK IDLE terminated

C: a4 UID FETCH 913:* (FLAGS BODY.PEEK[HEADER.FIELDS (MESSAGE-ID FROM SUBJECT DATE)])
S: * 413 FETCH (UID 913 FLAGS () BODY[HEADER.FIELDS ...] {118}
S: Message-ID: <CAF9x2mQ0nJ8y+Wr3TkPq7z@mail.gmail.com>
S: From: maya@example.com
S: Subject: Re: Onboarding call next week?
S: )
S: a4 OK UID FETCH completed

C: a5 IDLE
S: + idling

Note BODY.PEEK rather than BODY. A plain BODY fetch sets the Seen flag, so a background sync would mark the user mail as read behind their back.

The two errors you will actually see

C: a1 LOGIN maya@example.com ********
S: a1 NO [AUTHENTICATIONFAILED] Invalid credentials (Failure)
   -> permanent. 2FA is on: the account needs an App Password or OAuth,
      not a retry.

S: * BYE Too many simultaneous connections. (Failure)
   -> the 15-connection Gmail cap, shared with the user's own devices.
      Cap connections per account, reconnect with jitter, never in a tight loop.
Untagged responses

What the server can push, and what to do about it.

ResponseWhat it meansCorrect handling
* 413 EXISTSThe selected mailbox now holds 413 messages. It is a total, not a delta, and it is a sequence number rather than a UID.Do not fetch by sequence number. Issue UID FETCH from the UIDNEXT you recorded at SELECT time, then update your stored UIDNEXT.
* 7 EXPUNGEMessage number 7 is gone. Every sequence number above 7 has just shifted down by one, including in responses you already have in flight.Never keep sequence numbers between commands. Track UIDs, and treat EXPUNGE as an instruction to invalidate any cached numbering.
* 12 FETCH (FLAGS (\Seen \Answered))Flags changed, usually because the user read or replied to the message on another device.Update read state locally. This is the response that keeps a CRM inbox from showing unread counts the customer already cleared on their phone.
* 1 RECENTOne message has the Recent flag, which means no other session has seen it yet. Gmail and several others always report 0.Ignore it. Build nothing on RECENT; it is unreliable across providers and meaningless with multiple concurrent clients.
* BYE Too many simultaneous connections.The provider connection limit has been hit, commonly 15 concurrent IMAP connections per Gmail account across all clients.Reduce connections per account, share one connection across folders where possible, and reconnect with jitter rather than immediately.
Production checklist

Six rules for an IDLE loop that survives a month.

  • Check CAPABILITY for IDLE before using it, and fall back to a 60 second poll when it is absent.
  • Recycle the idle every 25 to 29 minutes, whether or not anything has arrived.
  • Store UIDVALIDITY and UIDNEXT, fetch by UID only, and resynchronise from scratch if UIDVALIDITY changes.
  • Use BODY.PEEK so a background sync never marks a message as read.
  • Cap simultaneous connections per account, not per worker, and reconnect with exponential backoff plus jitter.
  • Run a slow UID-range reconciliation sweep alongside the push path, because a dead socket looks exactly like a quiet mailbox.

IMAP IDLE: FAQ

What you hit in week two of running a mailbox sync in production.

RFC 2177 advises clients to terminate the IDLE and re-issue it at least every 29 minutes, because servers, NAT devices and load balancers all time out idle TCP connections and many of them do it silently. Without the timer you get a half-open socket: your process still believes it is listening, the server has already forgotten you, and nothing arrives until someone restarts the worker. Twenty-nine minutes is chosen to sit just inside the common 30-minute server timeout.
Not on one connection. IDLE applies to the mailbox currently selected, and while idling you cannot issue any other command, so watching Inbox and Sent means two connections. That multiplies quickly across a multi-tenant sync, and Gmail caps simultaneous IMAP connections per account at 15 including the user own devices, so a design that idles on every label will exhaust the limit and start receiving BYE responses.
Send the literal string DONE followed by CRLF. It is not a tagged command and it takes no arguments; the server then completes the original IDLE command with its tag. Anything else you write while idling is a protocol violation, so a client that wants to issue a command must first send DONE, wait for the tagged OK, and only then send the command.
Reconnect with exponential backoff and jitter, then re-SELECT the mailbox and compare the UIDVALIDITY and UIDNEXT you stored against what the server reports. If UIDVALIDITY changed, every UID you hold is meaningless and the mailbox must be resynchronised from scratch. If it is unchanged, fetch from your stored UIDNEXT forward. Reconnecting every worker at the same instant after a deploy is what turns a routine restart into a provider rate limit.
For latency, yes: a message appears in seconds rather than at the next poll. For robustness, it needs more care, because a silent connection is indistinguishable from a quiet mailbox. The production answer is both: IDLE for immediacy, plus a low-frequency reconciliation pass every few minutes that fetches by UID range and catches anything the push path missed.
CONDSTORE and QRESYNC (RFC 7162) make resynchronisation far cheaper by letting you ask only for changes since a modification sequence, and NOTIFY (RFC 5465) allows notifications about mailboxes other than the selected one. Provider-specific push, Gmail API watch with Pub/Sub or Microsoft Graph subscriptions, is better still where available. IDLE remains the lowest common denominator that works against any IMAP host, which is why a CRM that connects arbitrary mailboxes has to implement it.
Ready to ship

Replies in seconds, not on the next poll.

Pinlyx holds a live IDLE connection per mailbox, recycles it inside the RFC window, and reconciles by UID range so nothing is lost when a socket dies quietly.

Free forever plan · GDPR-ready · No credit card required

We value your privacy

We use cookies to improve our site, analyze traffic, and personalize ads. You can accept all, reject non-essential, or customize your choices. Read our Cookie Policy.