nextcloud / nextcloud/mail

PreviewEnhancementProcessingJob loads the whole 14-day backlog into memory at once and crash-loops

Open
#13,391 2 comments 1 reaction 1 assignee View on GitHub

@kesselb is already working on this.

Since Aug 3, 2026.

1. to develop bug
Dominant language
JavaScript
Stars
1k
Forks
348
Avg merge
12h 28m
Merged PRs (30d)
91

Description

Steps to reproduce
  1. Configure a mail account whose recent history is large — e.g. an account that has just been added, or one where a bulk import/migration gave several thousand messages a sent_at within the last 14 days, so they are all sitting with structure_analyzed = false.
  2. Let background jobs run normally (system cron, every 5 minutes).
  3. Wait for OCA\Mail\BackgroundJob\PreviewEnhancementProcessingJob to be picked up, and watch the PHP error log / journal.
Expected behavior

The job processes unanalysed messages in bounded batches, so its peak memory is a function of the batch size and not of how many messages are waiting. Progress is persisted per batch, so a run that is interrupted (OOM, deploy, SIGTERM) still leaves the messages it did analyse marked as analysed, and the next run resumes rather than starting over.

Actual behavior

The job dies with a PHP fatal and makes no progress at all, then repeats the same failure on every subsequent run — it never converges, because nothing is persisted until the very end of each mailbox.

Jul 30 19:32:27 server php[736579]: PHP Fatal error: Allowed memory size of 536870912 bytes exhausted (tried to allocate 692224 bytes) in /nix/store/…-nextcloud-34.0.2/3rdparty/sabre/vobject/lib/Property/Binary.php on line 61
Jul 30 19:32:27 server systemd[1]: nextcloud-cron.service: Main process exited, code=exited, status=255/EXCEPTION
Jul 30 19:32:27 server systemd[1]: nextcloud-cron.service: Consumed 42.002s CPU time over 1min 10.305s wall clock time, 585.4M memory peak

PID 736579 maps to this job (occ background-job:running, timestamps in UTC, journal in -03):

| 112416118501888000 | OCA\Mail\BackgroundJob\PreviewEnhancementProcessingJob | 2026-07-30 22:31:57 | 170 | 736579 | 21 minutes |

The failing allocation is only 676 KB, so the vobject frame is where it happened to land, not the cause — the process was already at ~511 MB of the 512 MB limit. For comparison, every other background job on this instance peaks at 27–41 MB (occ background-job:history).

The code path, on main as of today:

  • PreviewEnhancementProcessingJob::run() calls PreprocessingService::process() with $limitTimestamp = now - 14 days.
  • PreprocessingService::process() does array_chunk($mailboxIds, 1000) — that chunks the mailbox ids, not the messages — and array_merges every result into one $messages array.
  • MessageMapper::getUnanalyzed() is SELECT * FROM oc_mail_messages WHERE sent_at > ? AND structure_analyzed = false AND mailbox_id IN (…) ORDER BY sent_at ASC with no LIMIT, hydrated through findEntities(). So every unanalysed message of the last 14 days becomes an entity in memory in one go.
  • PreviewEnhancer::process() then calls getAttachmentNames() per message and a single getBodyStructureData() for all UIDs of a mailbox, holding the Horde fetch results alongside the entity array.
  • updatePreviewDataBulk() runs only after a whole mailbox is done, so a run that dies partway persists nothing and the next run rebuilds the identical working set.

Suggested fix: give getUnanalyzed() a LIMIT (and offset or a sent_at/id cursor), and persist per batch rather than once per mailbox, so an interrupted run still makes forward progress.

Possibly the same root cause as the much older #7481, which reports the identical 512 MB fatal without a diagnosis.

Workaround, for anyone finding this: raise the CLI memory limit for the cron process only (on NixOS, services.nextcloud.cli.memoryLimit, which adds -dmemory_limit= to nextcloud-cron.service without touching php-fpm). That buys headroom but does not bound the growth. Note also that the two-week window means an old backlog eventually ages out of the query on its own.

Mail app version

5.10.10

Nextcloud version

34.0.2

Mailserver or service

dovecot and then Gmail

Operating system

NixOS 26.05, x86_64

PHP engine version

Other

Nextcloud memory caching

memcache.local = \OC\Memcache\APCu, memcache.distributed = \OC\Memcache\Redis, memcache.locking = \OC\Memcache\Redis

Web server

Nginx

Database

PostgreSQL

Additional info
  • PHP 8.4.23 (hence "Other" above), nginx 1.30.4 behind Traefik, PostgreSQL 17.10 over a unix socket.
  • memory_limit is 512 MB — on NixOS the Nextcloud module ties it to services.nextcloud.maxUploadSize, whose default is 512M, which is why the number in the log is exactly 536870912.
  • Hardware is modest (8 GB RAM), which is why the limit is left at the default rather than raised globally: memory_limit is shared with every php-fpm child, and the module's default pool is pm.max_children = 120.
  • The instance has two mail accounts; the crashing job carries {"accountId":2}.
  • Related observation while diagnosing this: because the job dies with a fatal, its run row stays RUNNING in oc_job_runs forever, and oc_jobs.reserved_at keeps the job reserved for 12 hours, so the retry cadence is twice a day rather than hourly.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.