#3441·inbox-zero

Bulk Process Emails is client-driven, so large self-hosted backfills can't run to completion

Author: MrRussLutherCreated Aug 31, 2026Updated Aug 31, 2026

Self-hosted, ghcr.io/elie222/inbox-zero:latest, standard compose stack. I'm backfilling rules across a 26k message Outlook inbox and hit a scale limit. Before I build anything I wanted to check whether you'd be open to moving bulk processing server side.

The loop in apps/web/app/(app)/[emailAccountId]/assistant/bulk-run.ts runs in the browser. It pages /api/threads with nextPageToken, filters on thread.plan, and awaits runAiRules for each batch of 25, feeding the client side ai-queue store. I can see useBeforeUnload(isBusy) is there, so I know this is by design rather than an oversight.

At a few hundred emails that's fine. At my volume it isn't:

  • sustained throughput is about 5 to 6 messages a minute
  • 26k messages comes out around 70 hours
  • that's 70 hours of a tab staying open and a machine staying awake

I measured it with a 10 second sampler watching ExecutedRule inserts, "Fetching message for rule execution" log lines, and TCP connections to the web container:

TIME       ROWS  DELTA  FETCH  CONN
12:20:12   3227      3      4     2    running
12:20:22   3228      1      3     0    tab closed
12:20:33   3228      0      2     0
12:20:43   3228      0      0     0
12:21:04   3228      0      0     0
12:22:07   3228      0      0     0    flat for 90s
12:28:46   3231      3      3     2    tab reopened, resumed

Rows froze the moment the connections dropped. The fetch lines drained to zero over the next 21 seconds, which is just the 30 second lookback window emptying. Reopening picked up from 3228 instead of restarting, so progress itself is durable. It's the driver that's ephemeral.

Would you take a PR moving this onto BullMQ? The pieces are already in place: QUEUE_BACKEND=bullmq, a worker container, and existing queues for email-digest-all, automation-jobs, digest-item-summarize and others. Rough shape I have in mind:

  • a bulk run queue with one job per run, holding the paging loop that's currently in bulk-run.ts
  • run state persisted so progress survives a disconnect and the UI can reattach
  • pause, resume and stop become operations on the job rather than on a client store
  • BulkRunRules polls run state instead of deriving progress from the local ai-queue

I'm happy to design it properly and write it if that direction works for you. If you'd rather keep the client driven model I understand, and I'll just batch it locally by date range.