メインコンテンツにスキップ

Odoo 大量メール送信のバッチタイムアウト — 2026 年に今すぐ修正する方法

Odoo の大量メール送信が 17/18/19 の送信中にタイムアウトになりましたか?完全な詳細ガイドを使用して、バッチ サイズ、ワーカーの時間制限、キューのスロットリングを安全に調整します。

E
ECOSIRE Research and Development Team
|2026年5月4日6 分で読める1.2k 語数|

この記事は現在英語版のみです。翻訳は近日公開予定です。

You launch an Odoo mass mailing to 10,000 recipients. After 30 minutes, the campaign shows "in progress". After an hour, the worker dies with:

You launch an Odoo mass mailing to 10,000 recipients. After 30 minutes, the campaign shows "in progress". After an hour, the worker dies with:

ERROR: Worker (12345) exceeded limit_time_real (3600), terminating
WARNING: mass.mailing batch incomplete: sent 4327, remaining 5673

Some recipients got the email, some did not, and re-sending risks duplicates. This is a classic batch-timeout problem in Odoo 17.0/18.0/19.0 — fixable with worker tuning, batch sizing, and queue strategy.

Quick Fix

Raise the worker's wallclock limit and lower the batch size:

# odoo.conf
limit_time_real_cron = 14400      # 4 hours wallclock max for cron
limit_time_real = 600              # 10 minutes for HTTP requests
mail_mass_mailing_batch_size = 200 # smaller batches resume faster

Restart Odoo, then resume the campaign:

mailing = self.env['mailing.mailing'].browse(<id>)
mailing.action_send_mail()    # resumes from where it left off

For ongoing or chronic timeout issues, install OCA's queue_job and route mass mailing through it.

Why This Happens

Odoo mass mailing flow:

  1. Campaign creates mailing.trace rows for each recipient.
  2. A cron picks up traces in batches and sends them.
  3. Each send is one SMTP round-trip plus DB writes.

The five common timeout causes:

  1. Worker time limit too low. Odoo's limit_time_real_cron defaults to 1 hour. A 10K-recipient send at 0.5 sec each = 83 minutes — does not fit.
  2. SMTP rate limit. Gmail throttles to 100/minute, SES to ~200/sec — Odoo waits between sends, the wait pushes total time over the limit.
  3. DB contention. Heavy writes to mail.mail and mailing.trace plus other concurrent activity slow each send.
  4. Memory growth during send. As the cron processes more emails, ORM cache grows, RAM bloats, eventually the worker is killed.
  5. DKIM signing or external image embedding adds latency per send. Cumulative latency exhausts the time budget.

Step-by-Step Diagnosis

1. Check campaign status.

SELECT m.id, m.name, m.state, m.sent, m.total
FROM   mailing_mailing m
WHERE  m.id = <campaign_id>;

sent < total with state='sending' for hours = stuck batch.

2. Check mailing.trace for incomplete sends.

SELECT trace_status, count(*) FROM mailing_trace
WHERE  mass_mailing_id = <id>
GROUP BY trace_status;

outgoing rows are unsent. error rows failed for a reason — read failure_reason.

3. Check worker logs around the timeout.

grep "limit_time_real\|exceeded\|terminated" /var/log/odoo/odoo.log | tail

OOM-killed workers mean memory growth, not just timeout. Address with batch tuning.

4. Measure single-send latency. Send to 1 test recipient, time the worker:

import time
start = time.time()
mailing._send([test_recipient_id])
print(f"Per-recipient: {time.time() - start:.2f} sec")

If single sends are slow (over 0.5 sec), reduce send latency before increasing volume.

5. Check provider rate limits. Look at SES SendStatistics or Gmail's API quota. Throttled sends queue, dramatically increasing total time.

Permanent Fix

Tune worker time limits for mass mail:

# odoo.conf
limit_time_real_cron = 14400       # 4 hours per cron
limit_memory_soft = 2147483648     # 2 GB before recycling
limit_memory_hard = 3221225472     # 3 GB before kill
max_cron_threads = 2

For very large campaigns, consider running a dedicated cron-only Odoo instance with its own tuning, separate from web workers.

Reduce batch size and add intra-batch commits:

# In a custom override of mailing.mailing
@api.model
def _process_mass_mailing_queue(self):
    BATCH_SIZE = 100
    pending = self.env['mailing.trace'].search([
        ('trace_status', '=', 'outgoing')
    ], limit=BATCH_SIZE)

    while pending:
        for trace in pending:
            try:
                trace._send()
            except Exception as e:
                trace.write({
                    'trace_status': 'error',
                    'failure_reason': str(e),
                })
        self.env.cr.commit()        # release locks, persist progress
        self.env.invalidate_all()   # cap memory growth

        pending = self.env['mailing.trace'].search([
            ('trace_status', '=', 'outgoing')
        ], limit=BATCH_SIZE)

Smaller batches mean smaller transactions, faster recovery from failure, and more frequent garbage collection.

Use queue_job for proper scaling:

from odoo.addons.queue_job.job import job

class MailingMailing(models.Model):
    _inherit = 'mailing.mailing'

    @job(default_channel='root.mail')
    def _send_one(self, trace_id):
        self.env['mailing.trace'].browse(trace_id)._send()

    def action_send_mail_async(self):
        for trace in self.mailing_trace_ids.filtered(lambda t: t.trace_status == 'outgoing'):
            self.with_delay()._send_one(trace.id)

queue_job processes mails outside the request/cron cycle. Scales horizontally — add queue workers, throughput rises linearly.

Throttle to provider limits:

import time

class MailingMailing(models.Model):
    _inherit = 'mailing.mailing'

    SEND_RATE_PER_SEC = 50   # SES safe default

    def _process_mass_mailing_queue(self):
        last_send = time.time()
        for trace in self.mailing_trace_ids.filtered(lambda t: t.trace_status == 'outgoing'):
            elapsed = time.time() - last_send
            wait = max(0, (1.0 / self.SEND_RATE_PER_SEC) - elapsed)
            if wait:
                time.sleep(wait)
            trace._send()
            last_send = time.time()

Self-throttled sending stays under provider limits. Pair with provider-specific knowledge — SES is 14/sec on cold accounts, 200/sec when warmed.

Resume on partial failure. Mass mailing should be idempotent: if a worker dies mid-batch, the next cron run should pick up from where it left off. Odoo's default behaviour does this via mailing.trace.trace_status='outgoing' filter — verify your custom code preserves this.

How to Prevent It

  • queue_job for mass mailing. Single biggest reliability win for high-volume sending. Decouples send work from cron lifecycle.
  • Test at scale in staging. Send a 5K test campaign in staging before launching 50K in production. Catches batch-size and rate-limit issues before they affect customers.
  • Provider rate awareness. Know your provider's limit. SES, Gmail, Mailgun all publish rate caps. Throttle below the cap with margin.
  • Monitor mailing.trace queue depth. Alert when outgoing count exceeds threshold or grows monotonically. Catches stalls before customers complain.
  • Smaller batches by default. 100 to 500 per batch is the sweet spot for most campaigns. Larger batches risk timeout; smaller batches risk overhead.
  • Use a dedicated sending domain. Improves deliverability (warm-up isolated from transactional traffic) and lets you set per-domain rate limits at the provider.

Frequently Asked Questions

How fast can Odoo realistically send mass mail?

Single-server, single-cron-thread, with SES and a warmed sending domain: 10K mails in about 8 minutes (around 20/sec including DB and DKIM overhead). With queue_job and 4 workers, around 75/sec sustained. Beyond that requires dedicated infrastructure or a transactional service like SendGrid running alongside Odoo.

Should I use Odoo's built-in mass mailing or an external service?

For under 50K/month: Odoo's built-in is fine. Above that, integrate a transactional service like SendGrid or Mailgun via API. The OCA mass_mailing_partner and similar modules provide hooks. Hybrid is common: transactional via SES, marketing via dedicated provider.

Can I pause and resume a mass mailing?

Yes. Pause: change state from sending to draft. Resume: re-run action_send_mail(). The pending traces in outgoing state pick up where they left off. Avoid editing the campaign content between pauses — partial recipients see different versions.

How do I avoid sending duplicates after a crash?

Idempotent design. Each recipient gets one mailing.trace row. The send loop only processes traces with state='outgoing'. Successful sends update to state='sent'. A crash leaves rows in outgoing; resumption picks them up. As long as your custom send code respects this state machine, duplicates are impossible.


Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.

共有:
E

執筆者

ECOSIRE Research and Development Team

テクニカルライティング

ECOSIRE のテクニカルライティングチームは、Odoo ERP、Shopify による EC、AI エージェント、Power BI による分析、GoHighLevel による自動化、そしてエンタープライズソフトウェアのベストプラクティスを扱っています。私たちのガイドは、企業が十分な情報に基づいてテクノロジーを判断できるよう支援します。

ECOSIRE

Odoo ERP でビジネスを変革

Odoo の専門家による実装、カスタマイズ、サポートにより、業務を合理化します。