# 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) ```php 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: ```php 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`: ```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)