Skip to content

Sync that runs out of memory leaves the mailbox locked and is retried forever #13778

Open

Description

@dillardblom

Describe the bug

When a mailbox sync runs out of PHP memory, the PHP process dies with a fatal error. The finally blocks in ImapToDbSynchronizer::sync() that release sync_new_lock, sync_changed_lock and sync_vanished_lock do not run on a fatal error, so the mailbox stays locked until Mailbox::LOCK_TIMEOUT (300 s) expires.

Meanwhile the web UI keeps loading the envelope list. GET /api/messages returns 409 (MailboxLockedException: … is already being synced) and Mailbox.vue waits 15 s and tries again. As soon as the lock expires, the next POST /api/mailboxes/{id}/sync starts the same sync, which runs out of memory again. The sync tokens are only stored after a successful sync, so every attempt starts from the same point and fails in the same way.

The result is a loop that does not end by itself:

  • The inbox takes a very long time to load, or does not load, because every envelope request waits for the 15 s retry.
  • A PHP fatal error happens every 5 minutes: Allowed memory size of 536870912 bytes exhausted … horde-imap-client/lib/Horde/Imap/Client/Base.php (in _updateCache(), also Data/Fetch.php).
  • The IMAP server sees a new login every 2 seconds from the retrying requests (about 30 per minute instead of 2 or 3).
  • Other requests from the same session report Slow session_start detected.

It stayed like this for over an hour until the sync was run once with a higher memory limit. After that one successful run, the same sync needed 0.4 to 1.7 s and 44 to 71 MB again.

Steps to reproduce

We cannot name the exact trigger. It is probably a rare race condition that mostly shows up with very large mailboxes. Mailboxes of this size are not unusual in companies, though, so the app should be robust against it.

What we saw:

  1. An INBOX with a large number of messages (here about 18,000) on a Dovecot server with CONDSTORE/QRESYNC.
  2. Something (unknown) made the next partial sync process a large set of messages.
  3. PHP's memory limit at 512M (the Nextcloud AIO default).
  4. Mail opened in the browser.

Expected behavior

Record sync progress along the way, and at the very least when the sync crashes, so that the next attempt does not start over from the beginning and end up in an endless loop. For example, fetch changed messages in chunks and store the sync tokens per chunk.

In addition:

  • A sync that keeps failing stops and shows an error instead of being retried in exactly the same form forever.
  • A lock whose process no longer exists does not block the mailbox for another 5 minutes.
  • The UI does not keep a retry loop running without telling the user anything.

Actual behavior

The loop described above. Running the partial sync from the CLI with the same parameters as the browser (20 known ids, partial sync) reproduces the fatal error at 512M. With a 4G limit it succeeds after 82.5 s with a peak of 586 MB.

Mail app version

5.12.2

Nextcloud version

34.0.4 (All-in-One)

PHP version

8.4 (AIO), memory_limit 512M

Mail server

Dovecot 2.4 (DirectAdmin), IMAP with CONDSTORE/QRESYNC

Additional context

The lock timeout, the finally-based unlocking and the 15 s retry in Mailbox.vue are unchanged on main.

Related, but not the same: #13473 (retry loop on MailboxNotCachedError).


Assisted-by: ClaudeCode:claude-opus-5-5 (debugging)

Activity

  1. ChristophWurst commented on Oct 20, 2026

    @ChristophWurst
    Member

    Please use the issue templates. Raw issues fall through the issue triage process.

  2. giggio commented on Nov 1, 2026

    @giggio

    Same problem here, and I have a state that explains why the partial sync gets so large.

    Mail 5.12.4, Nextcloud 34.0.4, Dovecot. The nextcloud-cron service started failing about once an hour with Allowed memory size of 1073741824 bytes exhausted in Horde's Stream.php and Rfc822/List.php, about 57 times in three days. The job was SyncJob, on a mailbox with 110,902 messages.

    Mail's cache of that mailbox was nearly empty, but its sync tokens were set (uid=110875), with a matching UID validity. With tokens set, ImapToDbSynchronizer runs runPartialSync(), which asks for every message after the token (about 110k) in one go. Only the database inserts are chunked. Memory grows with the number of new messages, so a higher limit only moves the failure.

    The initial sync doesn't have this problem. It stops at MAX_NEW_MESSAGES and resumes through IncompleteSyncException. Running the same mailbox through it took 60-150MB per round. The partial sync could use the same bound, or the chunked fetch with tokens stored per chunk that you are asking for.

    To repair it, I cleared the cache and synced again:

    occ mail:mailbox:clear-cache <mailbox id>
    occ mail:account:sync <account id> -vvv
    

    The second command needs to be repeated until the initial sync completes. It hasn't happened again in the three weeks since.

    Like you, I can't name the trigger. I don't know how the tokens ended up set on an almost empty cache. An easy way to catch it is to compare the number of cached messages with the token's UID: a mailbox with a high UID in its token and almost nothing cached will go down this path on its next sync.

  3. dillardblom commented on Nov 1, 2026

    @dillardblom
    ContributorAuthor

    It's already fixed in #13823 3 weeks ago, but apparently nobody has time to review my PR..... Another user (stiller) has tested my patch, so if the problem reappears please try that fix to see if it helps you as well.

  4. dillardblom commented on Nov 2, 2026

    @dillardblom
    ContributorAuthor

    Correction to my comment above: #13823 fixes a different path (the client sync request without known ids). The case @giggio describes is the background partial sync, which fetches all messages after the sync token without the MAX_NEW_MESSAGES bound the initial sync has. That still needs a fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions