analyza-woocommerce-email-throttling.md 9.8 KB

Analýza: zahlcení wp_mail při hromadné změně stavu objednávek ve WooCommerce

Verze: 1.0 Datum: 29. 6. 2026 Oblast: WordPress / WooCommerce, doručování transakčních e-mailů Závažnost: střední (provozní – část zákazníků nedostane notifikaci)


1. Shrnutí

Při hromadné změně stavu velkého počtu objednávek (100+) na stav „Dokončeno" se WooCommerce pokouší odeslat zákaznickou notifikaci o dokončení objednávky pro každou objednávku. Všechna volání wp_mail() proběhnou synchronně v rámci jediného HTTP requestu, prakticky v jeden okamžik. Poskytovatel hostingu tento nárazový tok vyhodnotí jako potenciální zneužití (spam burst) a z bezpečnostních důvodů funkci odchozí pošty dočasně zablokuje / rate-limituje.

Důsledkem je, že část zákazníků e-mail nedostane, request může spadnout do timeoutu a chování je nedeterministické (záleží, kdy limit hostingu „spadne").

Doporučené řešení: odpojit synchronní odesílání a notifikace zařadit do rate-limitované fronty (Action Scheduler) s pevným odstupem mezi e-maily. Dlouhodobě navíc přejít na externí transakční SMTP/API službu.


2. Reprodukce a projevy

Aspekt Pozorování
Spouštěč Hromadná akce v adminu: Objednávky → vybrat 100+ → Změnit stav na „Dokončeno"
Projev Část e-mailů se neodešle; v logu hostingu/MTA chyby typu rate-limit / „too many connections" / dočasné odmítnutí
Práh Závisí na limitu hostingu (typicky desítky e-mailů za minutu na sdíleném hostingu)
Reprodukovatelnost Vysoká při dostatečném počtu objednávek v jedné dávce

3. Analýza příčiny (root cause)

3.1 Tok odeslání e-mailu ve WooCommerce

Při změně stavu objednávky proběhne uvnitř jednoho requestu řetězec:

WC_Order::update_status('completed')
   └─ status_transition()
        └─ do_action('woocommerce_order_status_completed', $order_id, $order)
             └─ WC_Emails::send_transactional_email()
                  └─ do_action('woocommerce_order_status_completed_notification', …)
                       └─ WC_Email_Customer_Completed_Order::trigger()
                            └─ ->send()
                                 └─ wp_mail()   ← skutečné odeslání

Při hromadné akci se tento řetězec provede N-krát za sebou v témže PHP procesu. Mezi jednotlivými wp_mail() není žádná prodleva ani dávkování.

3.2 Kde vzniká problém

  1. Synchronnost – odeslání je svázané s requestem hromadné akce. 100+ e-mailů odejde v rámci jednotek sekund.
  2. Bez rate-limitu na straně aplikace – WooCommerce ve výchozím stavu nijak nereguluje tempo odesílání.
  3. Bezpečnostní limit hostingu – sdílené hostingy omezují počet odchozích zpráv za jednotku času, aby zabránily zneužití účtu pro spam. Nárazový tok tento limit překročí a hosting odesílání pozastaví.

Příčina tedy není v hostingu jako takovém – ten se chová správně. Problém je, že aplikace generuje neregulovaný burst, který do bezpečnostního limitu naráží.


4. Dopady

  • Funkční: část zákazníků nedostane potvrzení o dokončení objednávky.
  • Provozní: nekonzistentní stav – nelze snadno určit, komu e-mail dorazil.
  • UX/reputace: chybějící notifikace = dotazy zákazníků, zhoršená důvěra.
  • Deliverability: opakované bursty mohou poškodit reputaci domény/IP.
  • Výkon: request hromadné akce může spadnout do timeoutu (PHP max_execution_time).

5. Možná řešení – srovnání

# Řešení Tvrdý strop/min Odpojení od requestu Náročnost Poznámka
A Filtr woocommerce_defer_transactional_emails velmi nízká Jednořádek; dávky AS ale stále mohou poslat ~25 najednou
B Vlastní rate-limitovaná fronta (Action Scheduler) nízká–střední Doporučeno – pevný odstup mezi e-maily
C Externí transakční SMTP/API (SES, Mailgun, Brevo…) ~ ✗* střední Obejde limit hostingu, zlepší deliverability; *bez fronty pořád burst
D Menší dávky ručně nulová Obejití, ne řešení; lidská chyba
E Hotový plugin třetí strany dle pluginu dle pluginu nízká Závislost navíc, různá kvalita

5.1 Varianta A – vestavěný defer (rychlá úleva)

add_filter( 'woocommerce_defer_transactional_emails', '__return_true' );

WooCommerce e-maily zařadí jako asynchronní akce Action Scheduleru → odpojí je od requestu hromadné akce. Omezení: Action Scheduler zpracovává dávky (default ~25 akcí na běh), takže při jednom běhu cronu může odejít několik desítek e-mailů téměř naráz. Negarantuje tedy strop „X e-mailů za minutu" a u přísného limitu hostingu nemusí stačit.

5.2 Varianta B – rate-limitovaná fronta (doporučeno)

Princip:

  1. Odpojí se výchozí synchronní trigger e-mailu „objednávka dokončena".
  2. Každá objednávka se místo toho zařadí do fronty s vlastní časovou známkou, která navazuje na předchozí (např. +12 s ⇒ 5 e-mailů/min).
  3. Action Scheduler nikdy nezpracuje akci dříve, než nastane její čas → tempo je dané timestampy, ne frekvencí cronu. To je tvrdý, deterministický strop.

Counter dalšího slotu se sám „uzdravuje" (max(now, next_slot)), takže po vyprázdnění fronty se tempo resetuje na aktuální čas.

Referenční implementace je v přiloženém mu-pluginu wc-mail-throttle.php. Tempo se ladí filtrem:

add_filter( 'wcmt_interval_seconds', function () { return 12; } ); // 5/min

5.3 Varianta C – externí transakční služba

Napojení na Amazon SES / Mailgun / Brevo / SendGrid přesune odchozí poštu mimo lokální sendmail hostingu, čímž odpadne jeho bezpečnostní blok a výrazně se zlepší doručitelnost. Ale: samotný přechod neřeší burst – každé wp_mail() při bulku proběhne dál synchronně (u SMTP dokonce pomaleji kvůli handshake). I providers mají rate-limity (často per-second). Proto se varianta C kombinuje s frontou (B).


6. Doporučení

Primárně: nasadit variantu B (rate-limitovaná fronta) – řeší jádro problému deterministicky a bez závislosti na třetích stranách.

Sekundárně / dlouhodobě: přejít na variantu C (externí transakční SMTP/API) pro lepší doručitelnost a odolnost, ponechat frontu z B jako regulátor tempa.

Variantu A lze použít jako okamžitou úlevu, než se nasadí B.


7. Implementační poznámky

7.1 Spolehlivý cron

Frontu vyprazdňuje Action Scheduler nad WP-Cronem, který se spouští jen při návštěvě stránky. Pro spolehlivé tempo:

  1. Vypnout WP-Cron ve wp-config.php:

    define( 'DISABLE_WP_CRON', true );
    
  2. Nastavit systémový cron (každou minutu), např. přes WP-CLI:

    * * * * * cd /cesta/k/webu && wp action-scheduler run >/dev/null 2>&1
    

    nebo HTTP variantou:

    * * * * * wget -q -O - "https://web.tld/wp-cron.php?doing_wp_cron" >/dev/null 2>&1
    

Bez spolehlivého cronu throttling funguje, ale fronta se vyprazdňuje jen nárazově.

7.2 Konfigurace tempa

interval = 12 s ⇒ 5 e-mailů/min ⇒ 300/h. Hodnotu nastavit podle reálného limitu hostingu (vyžádat si od podpory přesné číslo odchozích zpráv za minutu/hodinu).

7.3 Rozšíření na další stavy

Stejnou logiku lze použít i pro „Zpracovává se", „Vráceno" apod. – v detach_default odpojit příslušnou e-mailovou třídu (WC_Email_Customer_Processing_Order, …) a přidat hook na její _notification. Sdílení jednoho slot counteru zajistí, že se rate-limit počítá globálně napříč typy e-mailů.


8. Monitoring a ověření

Co sledovat Kde
Stav fronty (pending/complete/failed), rozestupy WooCommerce → Status → Scheduled Actions, skupina wcmt
Skutečné odeslání Log MTA / hostingu, příp. log transakční služby
Chyby wp_mail Vlastní logování v wp_mail_failed hooku
Timeout hromadné akce PHP error log (max_execution_time) – po nasazení B by měl zmizet

Doporučený test: na staging spustit hromadnou změnu 100+ objednávek a ověřit, že e-maily ve frontě odcházejí v očekávaném tempu a všechny dojdou.


9. Rizika a edge cases

  • Action Scheduler nedostupný – plugin má fallback (pošle hned); při bulku by se ale projevil původní problém. Ověřit, že WooCommerce (a tím AS) je aktivní.
  • Duplicitní odeslání – pokud by zůstal aktivní i výchozí trigger; proto je nutné spolehlivé odpojení v detach_default.
  • Velmi nízký interval – příliš krátký odstup opět překročí limit hostingu; hodnotu validovat proti reálnému limitu.
  • Růst slot counteru – ošetřeno přes max(now, next); po vyprázdnění se resetuje.
  • Vyšší PHP verze a předávání argumentů AS – akce plánovat s indexovaným polem argumentů (array( $order_id )), ne asociativním.

10. Závěr

Problém vzniká neregulovaným nárazovým odesíláním transakčních e-mailů v rámci jednoho requestu, který naráží na bezpečnostní limit hostingu. Nejde o chybu hostingu, ale o chybějící regulaci tempa na straně aplikace. Nasazením rate-limitované fronty (varianta B) se odesílání odpojí od requestu a rozloží do deterministicky řízeného tempa; dlouhodobě je vhodné doplnit externí transakční službu (varianta C) pro doručitelnost a odolnost. Tím se chování stane stabilním a předvídatelným.


Přílohy

  • wc-mail-throttle.php – referenční mu-plugin (rate-limitovaná fronta, varianta B)