Přejít k navigační liště

Zdroják » PHP » Jak funguje WordPress Cron a proč občas selhává

Jak funguje WordPress Cron a proč občas selhává

Články PHP, Webový vývoj

„Cron mi nějak neběhá.“ Klasická věta, která ve WordPress světě může znamenat cokoli od špatně nastavené WP_SITEURL, přes loopback zablokovaný Cloudflarem, až po fatal error v callbacku, který nechal viset transient doing_cron. WP-Cron totiž není skutečný scheduler — je to pseudo-cron závislý na návštěvnosti webu a HTTP loopbacku, se všemi pastmi, které si dokážete představit. Tenhle článek je hloubkový průchod jeho vnitřnostmi: co se reálně děje při spawn_cron(), kde vznikají race conditions, proč selhává a čím ho v produkci nahradit.

Nálepky:

Jak funguje WordPress Cron a proč občas selhává

WP-Cron je jedna z těch věcí ve WordPressu, které vypadají na první pohled jednoduše, ale jakmile na ně narazíte v produkci, zjistíte, že pod kapotou se děje něco úplně jiného, než co byste čekali od skutečného scheduleru. Občas neběhá, občas běhá moc, občas se zasekne a občas spustí stejný task dvakrát. V tomto článku se podíváme, jak WP-Cron interně funguje, kde má slabá místa, jak ho debuggovat a kdy ho v produkci raději úplně vypnout a nahradit něčím robustnějším.

Co je WP-Cron a proč to není cron

Klasický Unix/Linux cron je systémový daemon (crond), který běží nezávisle na čemkoli ostatním, čte crontab a v určených časech spouští úlohy. Je to aktivní scheduler — nezávisí na tom, jestli někdo přijde na váš web, jestli máte spuštěný Apache nebo jestli vůbec běží PHP. Pokud nastavíte úlohu na 03:00, ve 03:00 se spustí. Tečka.

WP-Cron funguje úplně jinak. Je to pseudo-cron — pasivní mechanismus, který se aktivuje pouze tehdy, když někdo navštíví váš web. Při každém HTTP requestu (pokud nejsou splněny určité podmínky) WordPress zkontroluje, jestli v databázi nejsou naplánované úlohy, jejichž čas už uplynul, a pokud ano, pokusí se je spustit asynchronně přes loopback request.

Důvod, proč WordPress vůbec takhle vznikl, je čistě historický. WordPress byl od počátku navržen tak, aby fungoval všude, kde uživatel typicky nemá SSH přístup, nemá přístup ke crontabu a často nemá ani možnost spustit dlouhotrvající procesy. Pseudo-cron byl elegantní (i když problematické) řešení: pokud potřebujete něco spouštět pravidelně, zavěsíte se na HTTP requesty a doufáte, že je jich dost. Tato volba s sebou nese kompromisy, které jsou dodnes patrné v každém WordPressu na světě.

Zásadní rozdíl je v garancích. Systémový cron garantuje, že úloha bude spuštěna v daný čas (pokud server běží). WP-Cron negarantuje vůbec nic. Pokud váš web nemá návštěvnost mezi 02:00 a 06:00, žádná úloha naplánovaná na tu dobu se nespustí. Spustí se až s prvním requestem po 06:00 — a to ještě jen pokud loopback projde.

Interní fungování WP-Cronu

Datová struktura: option cron

Všechny naplánované úlohy WordPress ukládá do jediného řádku v tabulce wp_options s názvem cron. Je to serializované PHP pole se specifickou strukturou. Pokud si ho vypíšete, vypadá zhruba takto:

array(
    1716200000 => array(
        'wp_version_check' => array(
            '40cd750bba9870f18aada2478b24840a' => array(
                'schedule' => 'twicedaily',
                'args'     => array(),
                'interval' => 43200,
            ),
        ),
        'wp_update_plugins' => array(
            '40cd750bba9870f18aada2478b24840a' => array(
                'schedule' => 'twicedaily',
                'args'     => array(),
                'interval' => 43200,
            ),
        ),
    ),
    1716203600 => array(
        'my_custom_hook' => array(
            '5d41402abc4b2a76b9719d911017c592' => array(
                'schedule' => false,
                'args'     => array('user_id' => 42),
            ),
        ),
    ),
    'version' => 2,
)Code language: PHP (php)

První úroveň je timestamp — UNIX čas, kdy se má úloha spustit. Druhá úroveň je hook (název akce, kterou WordPress zavolá přes do_action()). Třetí úroveň je hash argumentů — MD5 z serializovaných argumentů, který umožňuje rozlišit stejný hook s různými parametry. Čtvrtá úroveň je metadata: jméno schedule (např. hourly, twicedaily, daily nebo váš custom interval), argumenty a interval v sekundách (u opakovaných úloh).

To, že je všechno v jediném řádku, má důsledky. Při každém zápisu nového eventu se celé pole deserializuje, modifikuje a serializuje zpět. Při tisícovkách eventů (např. WooCommerce s velkým množstvím retry tasků) může tato operace zatížit DB nezanedbatelně.

Vstupní bod: wp-cron.php

Soubor wp-cron.php v rootu WordPress instalace je samostatný skript, který:

  1. Načte WordPress bootstrap (ale jen jeho část — definuje DOING_CRON a načte minimum potřebné).
  2. Získá lock přes transient doing_cron, který obsahuje aktuální microtime jako string.
  3. Načte cron option, projde events s timestamp menším nebo rovným aktuálnímu času.
  4. Pro každý event zavolá do_action_ref_array($hook, $args).
  5. Po doběhnutí (nebo timeout) lock uvolní.

Skript se nespouští sám od sebe — někdo ho musí zavolat. V defaultní konfiguraci ho volá samotný WordPress přes loopback při každém HTTP requestu, pokud zjistí, že je něco k vykonání.

Spouštění při HTTP requestu a spawn_cron()

Klíčová funkce, která rozhoduje, jestli se cron spustí, je wp_cron() v wp-includes/cron.php. Volá se na hooku init přes add_action('init', 'wp_cron'). Interní logika je zhruba:

function wp_cron() {
    if ( defined('DOING_CRON') ) {
        return;
    }
    if ( false === ( $crons = _get_cron_array() ) ) {
        return;
    }
    $gmt_time = microtime(true);
    $keys = array_keys($crons);
    if ( isset($keys[0]) && $keys[0] > $gmt_time ) {
        return; // nic zatím není po splatnosti
    }
    spawn_cron($gmt_time);
}Code language: PHP (php)

Funkce spawn_cron() je ta, která reálně iniciuje loopback. Než to udělá, kontroluje:

  • Jestli neběží jiná instance cronu (lock přes transient doing_cron).
  • Jestli lock není starší než WP_CRON_LOCK_TIMEOUT (default 60 sekund).
  • Jestli od posledního pokusu uplynulo dost času (rate limiting na úrovni samotného spawnu).

Pokud kontroly projdou, nastaví nový lock a zavolá:

$cron_request = apply_filters('cron_request', array(
    'url'  => site_url('wp-cron.php?doing_wp_cron=' . $doing_wp_cron),
    'key'  => $doing_wp_cron,
    'args' => array(
        'timeout'   => 0.01,
        'blocking'  => false,
        'sslverify' => apply_filters('https_local_ssl_verify', false),
    ),
));

wp_remote_post($cron_request['url'], $cron_request['args']);Code language: PHP (php)

Důležité detaily: blocking => false znamená, že WordPress nečeká na odpověď. timeout => 0.01 je 10 milisekund — to znamená, že připojení musí stihnout TCP handshake v této extrémně krátké době, jinak je request ukončen. To stačí na to, aby HTTP server dostal požadavek a začal ho zpracovávat, ale uživatel, který stránku načítal, není zdržován.

Loopback request se obvykle vrací zpět na stejný server — typicky na https://example.com/wp-cron.php?doing_wp_cron=1716200000.1234. Server tento request přijme jako kompletně nový PHP proces a spustí v něm wp-cron.php, který provede skutečnou práci.

Locking přes doing_cron

Locking je tu proto, aby se neprovedlo vykonání dvojitě, když přijde víc requestů zároveň. Mechanismus je ale poměrně primitivní:

$lock = get_transient('doing_cron');

if ( $lock > $gmt_time + WP_CRON_LOCK_TIMEOUT ) {
    $lock = 0;
}

if ( $lock != 0 && $lock != $doing_wp_cron ) {
    return; // jiná instance má lock
}

set_transient('doing_cron', $doing_wp_cron);Code language: PHP (php)

Lock není atomický. Mezi get_transient a set_transient existuje race window, kdy dva paralelní requesty mohou oba vidět prázdný lock, oba ho nastavit a oba se pak chovat jako jediná oprávněná instance. V praxi se to neprojevuje často, ale na vysoce zatížených webech ano. O tom dále.

Klíčové funkce API

Pro registraci a manipulaci s úlohami WordPress poskytuje tyto funkce:

  • wp_schedule_event($timestamp, $recurrence, $hook, $args) — naplánuje opakovanou úlohu. $recurrence musí být klíč registrovaný v cron_schedules.
  • wp_schedule_single_event($timestamp, $hook, $args) — jednorázová úloha v daný čas.
  • wp_next_scheduled($hook, $args) — vrátí timestamp další naplánované instance daného hooku, nebo false. Pozor: musíte předat stejné argumenty, jinak funkce nenajde existující event (hash argumentů se liší).
  • wp_unschedule_event($timestamp, $hook, $args) — odstraní konkrétní event. Opět musí sedět timestamp a argumenty.
  • wp_clear_scheduled_hook($hook, $args) — odstraní všechny instance daného hooku s danými argumenty.
  • spawn_cron($gmt_time) — interní funkce, která iniciuje loopback. Můžete ji volat ručně, ale obvykle to není potřeba.

Praktické příklady

Registrace vlastní opakované úlohy

Klasický pattern pro registraci hooku, který se spouští každou hodinu:

register_activation_hook(__FILE__, 'myplugin_activate');
register_deactivation_hook(__FILE__, 'myplugin_deactivate');

function myplugin_activate() {
    if ( ! wp_next_scheduled('myplugin_sync_orders') ) {
        wp_schedule_event(time(), 'hourly', 'myplugin_sync_orders');
    }
}

function myplugin_deactivate() {
    wp_clear_scheduled_hook('myplugin_sync_orders');
}

add_action('myplugin_sync_orders', 'myplugin_do_sync_orders');

function myplugin_do_sync_orders() {
    // skutečná logika
    $synced = MyPlugin\OrderSync::run();
    error_log(sprintf('[myplugin] Synced %d orders', $synced));
}Code language: PHP (php)

Důležité detaily, na které začátečníci často narazí:

  • Bez wp_next_scheduled() guardu by se při každé aktivaci pluginu (např. při updatech) přidal nový event do option, takže byste měli stovky duplicit.
  • Hook (add_action) musí být registrován vždy, ne jen při aktivaci. Když cron běží, neví, který plugin ho má obsloužit — projde všechny zaregistrované hooky.
  • Volání wp_clear_scheduled_hook() v deactivation hooku je důležité. Bez něj zůstanou eventy v databázi i po deaktivaci pluginu a WordPress je bude pravidelně zkoušet vykonat (a logovat warning o neexistujícím callbacku).

Vlastní interval přes cron_schedules

WordPress defaultně registruje pouze hourly, twicedaily, daily a (od WP 5.0) weekly. Pokud potřebujete jiný interval, musíte si ho zaregistrovat sami:

add_filter('cron_schedules', function($schedules) {
    $schedules['every_five_minutes'] = array(
        'interval' => 5 * MINUTE_IN_SECONDS,
        'display'  => __('Every 5 Minutes', 'myplugin'),
    );
    $schedules['every_thirty_seconds'] = array(
        'interval' => 30,
        'display'  => __('Every 30 Seconds', 'myplugin'),
    );
    return $schedules;
});

wp_schedule_event(time(), 'every_five_minutes', 'myplugin_quick_check');Code language: PHP (php)

Interval kratší než minuta je v praxi zbytečný — pseudo-cron stejně negarantuje submilisekundovou přesnost a obvykle se vykoná až s nejbližší návštěvou stránky. Pro skutečně časté úlohy potřebujete jiný mechanismus.

Jednorázová úloha s parametry

Typický use-case: uživatel udělá nějakou akci a vy potřebujete za pět minut zaslat follow-up email, ale nechcete blokovat aktuální request.

function send_welcome_followup($user_id) {
    add_action('myplugin_send_followup', 'myplugin_do_send_followup');

    if ( ! wp_next_scheduled('myplugin_send_followup', array($user_id)) ) {
        wp_schedule_single_event(
            time() + 5 * MINUTE_IN_SECONDS,
            'myplugin_send_followup',
            array($user_id)
        );
    }
}

function myplugin_do_send_followup($user_id) {
    $user = get_user_by('id', $user_id);
    if ( ! $user ) {
        return;
    }
    wp_mail($user->user_email, 'Vítejte!', 'Tady je váš onboarding...');
}Code language: PHP (php)

Pozor: wp_next_scheduled() pro vyhledání existujícího eventu musí dostat identické argumenty (porovnává se hash). Pokud při registraci předáte array(42) a při kontrole array("42"), hash bude jiný a vy získáte duplicitní event.

Bezpečné odstranění eventu

Odstranění konkrétního eventu vyžaduje znalost jeho timestamp:

$timestamp = wp_next_scheduled('myplugin_send_followup', array($user_id));
if ( $timestamp ) {
    wp_unschedule_event($timestamp, 'myplugin_send_followup', array($user_id));
}Code language: PHP (php)

Pokud chcete vymazat všechny naplánované instance hooku (např. při deaktivaci pluginu):

wp_clear_scheduled_hook('myplugin_send_followup');Code language: JavaScript (javascript)

Ale pozor — pokud používáte hook s různými argumenty pro různé uživatele, wp_clear_scheduled_hook() bez argumentů vymaže všechny instance bez ohledu na argumenty pouze od WordPress 6.x. Ve starších verzích vymazala jen instance bez argumentů. Pro jistotu projděte option cron ručně, pokud máte podezření na zaseklé eventy.

Logování a debugging vlastního cronu

Pro vlastní úlohy je užitečné mít wrapper, který logsuje start, konec, dobu trvání a případné výjimky:

function myplugin_run_with_logging($hook, callable $callback) {
    add_action($hook, function(...$args) use ($hook, $callback) {
        $start = microtime(true);
        error_log("[cron] START {$hook}");
        try {
            $callback(...$args);
        } catch (\Throwable $e) {
            error_log(sprintf(
                '[cron] FAIL %s: %s in %s:%d',
                $hook, $e->getMessage(), $e->getFile(), $e->getLine()
            ));
            // znovuhození výjimky, ať WP zaloguje fatal,
            // pokud nechcete, aby se cron tvářil úspěšně
            throw $e;
        }
        $elapsed = round(microtime(true) - $start, 3);
        error_log("[cron] END {$hook} in {$elapsed}s");
    });
}

myplugin_run_with_logging('myplugin_sync_orders', function() {
    MyPlugin\OrderSync::run();
});Code language: PHP (php)

WordPress sám o sobě cron logy neuchovává. Pokud něco selže uvnitř callbacku, výjimka se v wp-cron.php propaguje až do PHP fatal erroru, který skončí jen v server error logu (pokud ho máte zapnutý).

Proč WP-Cron selhává v produkci

Nízká návštěvnost

Nejčastější příčina selhání. Web s 5 návštěvami denně rozprostřenými mezi 09:00 a 17:00 nikdy nespustí úlohu naplánovanou na 03:00. WP-Cron se prostě nemá kdy aktivovat. Důsledky: zastaralé tranzienty, neodeslané emaily, neuložené statistiky, neprovedené zálohy, neaktualizované RSS feedy.

Tichá past je, že nepravidelná návštěvnost vede k nepravidelnému spouštění. Úloha „daily“ se může v jeden den spustit ve 22:00 a další den v 09:00 — tj. s 11hodinovým rozdílem. Pokud na ní něco kritického závisí (např. expirace voucherů), dostanete se do problémů.

Vysoká návštěvnost a race conditions

Opačný problém. Na webu s 50+ requesty za sekundu se může stát, že paralelní requesty proklouznou kolem locku popsaného výše. V option cron pak může dojít k situaci, kdy dvě instance wp-cron.php běží zároveň, obě načtou stejný event a obě ho začnou vykonávat. Pokud váš callback není idempotentní — třeba odesílá emaily nebo zapisuje do externího API — dostanete duplicity.

Druhý problém u vysoké návštěvnosti je, že každý request spouští wp_cron() kontrolu. Na webu s plnou cache to není velký problém (cache obvykle obsluhuje request před PHP), ale na admin requestech a non-cached endpointech to vede k tisícovkám zbytečných deserializací option cron za minutu.

Loopback requesty selhávají

Tohle je nejzákeřnější kategorie problémů. Server musí být schopen volat sám sebe přes HTTP. Když to nejde, WP-Cron neběhá vůbec — a chyba je tichá, protože loopback je blocking => false a nikdo nečeká na odpověď.

Typické příčiny selhání loopbacku:

  • Špatně nastavený WP_HOME / WP_SITEURL. WordPress volá site_url('wp-cron.php'). Pokud je WP_SITEURL nastavený třeba na public doménu za CDN, kterou interní síť serveru neumí resolvnout, loopback nedoběhne.
  • Basic Auth na staging/dev prostředích. Loopback nemá credentials a server vrátí 401 dřív, než se k wp-cron.php dostane. Řešení: vyloučit wp-cron.php z Basic Auth na úrovni webserveru.
  • Reverse proxy / Cloudflare. Pokud server volá svou vlastní veřejnou doménu, request jde přes Cloudflare a vrací se zpátky. Cloudflare může mít rate limity, WAF pravidla nebo bot protection, která loopback zablokují (zvlášť pokud volá z IP, kterou Cloudflare nezná jako legitimní origin).
  • DNS rebinding / split-horizon DNS. Server nemusí být schopen resolvnout svou vlastní doménu, pokud DNS odpověď ukazuje na externí load balancer, který server nemá v allowlistu.
  • Firewall / iptables. Outbound connections na port 80/443 mohou být blokované.
  • SSL problémy. Self-signed certifikáty nebo chyby v certifikátovém řetězci způsobí, že wp_remote_post() selže. WordPress to defaultně řeší filtrem https_local_ssl_verify, který vrací false, ale plugin nebo mu.config to může přepsat.

Rychlý test, jestli loopback funguje, je zavolat wp-cron.php ručně z příkazové řádky:

curl -v "https://example.com/wp-cron.php?doing_wp_cron=$(date +%s).0"Code language: JavaScript (javascript)

Pokud dostanete 200 OK, loopback by měl jít. Pokud dostanete 401, 403, 502 nebo timeout, máte tam blokátor.

Timeouty

PHP má max_execution_time, typicky 30 sekund na klasickém hostingu. Pokud váš cron callback běží dlouho (např. synchronizace 50 000 produktů z externího API), může dojít k tvrdému ukončení procesu uprostřed práce. Stav v databázi pak může být nekonzistentní — některé produkty zaktualizované, jiné ne — a další běh nemusí pokračovat tam, kde předchozí skončil.

Pro dlouhotrvající úlohy obvykle WordPress zvedne limit přes set_time_limit(0) v wp-cron.php, ale to platí jen pro tu konkrétní instanci procesu. Pokud webserver má vlastní timeout (Nginx fastcgi_read_timeout, Apache Timeout, PHP-FPM request_terminate_timeout), bude useknut bez ohledu na PHP.

Fatal errory a deadlocky

Toto je častý zdroj „zaseknutého“ cronu. Scénář:

  1. Cron běží, drží lock doing_cron.
  2. V některém callbacku dojde k fatal erroru (z paměti, deprecated funkce, syntax error po updatu, nedostupný externí servis…).
  3. PHP proces končí, ale lock se neuvolní, protože shutdown handler na to nestihl reagovat.
  4. Další requesty vidí, že lock je nastavený, a nepokoušejí se spouštět cron.
  5. Po uplynutí WP_CRON_LOCK_TIMEOUT (60 sekund) by se měl lock považovat za stale, ale tato logika není vždy spolehlivá — pokud žádný request nepřijde, nikdo lock nezkontroluje.

V praxi to znamená, že po fatalu se cron může zaseknout na dlouhé minuty nebo hodiny. Symptom: events v cron option mají čím dál starší timestamp, ale nic se nespouští.

Memory limit

Cron callbacky často pracují s velkými datasety. Načtení 10 000 postů přes get_posts(['posts_per_page' => -1]) může snadno spotřebovat stovky megabajtů. memory_limit v PHP-FPM nemusí být dostatečný a proces skončí s Allowed memory size exhausted.

Typický anti-pattern:

// ŠPATNĚ
$posts = get_posts(['posts_per_page' => -1, 'post_type' => 'product']);
foreach ($posts as $post) {
    update_post_meta($post->ID, 'last_sync', time());
}Code language: PHP (php)

Lepší přístup: batchování s explicitním wp_cache_flush() mezi batchi (WordPress jinak akumuluje object cache i u nepotřebných objektů):

$paged = 1;
do {
    $posts = get_posts([
        'posts_per_page' => 100,
        'paged'          => $paged,
        'post_type'      => 'product',
        'fields'         => 'ids',
        'no_found_rows'  => true,
    ]);
    foreach ($posts as $post_id) {
        update_post_meta($post_id, 'last_sync', time());
    }
    wp_cache_flush();
    $paged++;
} while ( count($posts) === 100 );Code language: PHP (php)

Duplicity

Tichý zabiják datové konzistence. Vzniká několika způsoby:

  • Race condition u locku (paralelní vykonání stejného eventu).
  • Plugin při každé aktivaci nebo updatu naplánuje event znovu, aniž by zkontroloval wp_next_scheduled().
  • Argumenty se mírně liší (int vs string), takže hash je jiný a WordPress považuje eventy za různé.
  • Multisite — stejný event je naplánován na více blozích bez koordinace.

Jediná spolehlivá ochrana je idempotentní callback. O tom dále v best practices.

Jak WP-Cron debuggovat

WP Crontrol

Plugin WP Crontrol od Johna Blackbourna je standardní nástroj. Ukáže vám obsah option cron v čitelné formě, umožní eventy editovat, ručně spouštět a mazat. Ve většině produkčních setupů ho ale nechcete mít zapnutý trvale (přidává nezanedbatelnou režii do admin requestů a otevírá další attack surface). Místo toho ho instalujte jen na dobu debugu.

WP-CLI

Pro CLI debugging je wp cron nezbytný:

# Seznam všech naplánovaných eventů
wp cron event list

# Seznam ve formátu vhodném pro grep
wp cron event list --format=csv --fields=hook,next_run_relative

# Spuštění konkrétního eventu hned (mimo plánovaný čas)
wp cron event run myplugin_sync_orders

# Spuštění všech eventů, jejichž čas už uplynul
wp cron event run --due-now

# Smazání všech eventů daného hooku
wp cron event delete myplugin_sync_orders

# Test, jestli vůbec WP-Cron funguje (jestli loopback projde)
wp cron testCode language: PHP (php)

Příkaz wp cron test je extrémně užitečný — WordPress se pokusí o loopback request a vrátí výsledek s případnými chybami. Pokud na produkci podezíráte loopback problém, je to první věc, kterou spustit.

Při spuštění přes WP-CLI jsou eventy vykonávány přímo v CLI procesu, ne přes HTTP. To znamená, že vy obejdete celou pseudo-cron logiku včetně locku a loopbacku. Užitečné pro debug, ale pamatujte, že chování v CLI nemusí 1:1 odpovídat běhu přes wp-cron.php přes HTTP.

Debug log a tichý fail

Zapnutí debug logu je základ:

define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
define('SCRIPT_DEBUG', true);Code language: JavaScript (javascript)

Cron běží přes wp-cron.php, takže výstup jde do wp-content/debug.log. Pozor — pokud váš callback selže s E_ERROR nebo PHP Throwable, WordPress to obvykle do debug logu zapíše, ale stack trace bývá zkrácený. Pro produkční debug doporučuji vlastní error handler:

add_action('init', function() {
    if ( defined('DOING_CRON') && DOING_CRON ) {
        set_error_handler(function($errno, $errstr, $errfile, $errline) {
            error_log(sprintf(
                '[cron-error] %d %s in %s:%d',
                $errno, $errstr, $errfile, $errline
            ));
            return false; // ať WP standardně reaguje
        });
        register_shutdown_function(function() {
            $err = error_get_last();
            if ( $err && in_array($err['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR])) {
                error_log(sprintf(
                    '[cron-fatal] %s in %s:%d',
                    $err['message'], $err['file'], $err['line']
                ));
            }
        });
    }
});Code language: PHP (php)

Jak poznat zaseknutý cron

Příznaky:

  • V wp cron event list mají eventy stále stejný (minulý) timestamp, který se neaktualizuje.
  • doing_cron transient je nastavený dlouho (zkontrolujte přes wp transient get doing_cron).
  • wp cron test hlásí selhání loopbacku.
  • Sledování DB ukazuje, že option cron se nemění, přestože by mělo být po splatnosti spousta eventů.

Quick fix pro zaseknutý lock:

wp transient delete doing_cronCode language: JavaScript (javascript)

Pak ručně spustit eventy:

wp cron event run --due-now

Produkční best practices

Vypnout WP-Cron a nahradit systémovým cronem

Standardní doporučení pro jakoukoli produkční instalaci. Princip:

// wp-config.php
define('DISABLE_WP_CRON', true);Code language: JavaScript (javascript)

Tato konstanta říká WordPressu, aby nikdy nespouštěl loopback z HTTP requestů. Eventy se stále plánují normálně, jen se nikdy nevykonají, dokud někdo neaktivuje wp-cron.php externě.

Pak nastavíte systémový cron:

*/5 * * * * cd /var/www/html && php wp-cron.php >/dev/null 2>&1Code language: JavaScript (javascript)

Nebo elegantněji přes WP-CLI, který nepoužívá HTTP layer:

*/5 * * * * cd /var/www/html && /usr/local/bin/wp cron event run --due-now >/dev/null 2>&1Code language: JavaScript (javascript)

Varianta s wp cron event run je obvykle preferovaná, protože:

  • Neobchází se wp-cron.php přes HTTP, takže odpadají všechny problémy s loopbackem.
  • Můžete přesně logovat výstup.
  • Nedrží se lock přes transient — eventy se spustí sekvenčně v CLI procesu.
  • Snáz se monitoruje (návratový kód, výstup).

Pokud nemáte SSH přístup (typický shared hosting), můžete použít externí cron service (např. cron-job.org), která bude pravidelně volat URL:

*/5 * * * * curl -s "https://example.com/wp-cron.php?doing_wp_cron=$(date +%s).0" > /dev/nullCode language: JavaScript (javascript)

Tady jen pozor — endpoint je veřejný a každý si ho může zavolat. Útočník tím nemůže nic provést (cron jen spustí to, co už je v databázi), ale může web zatěžovat. Pro bezpečnost ho můžete chránit přes HTTP basic auth (a do cronu předat credentials) nebo přes vlastní query parameter validovaný v MU pluginu.

Frekvence vs latence

Interval systémového cronu (*/5) určuje maximální latenci spuštění úlohy. Pokud máte event naplánovaný na 12:01:30 a cron běží každých 5 minut v 12:00, 12:05, 12:10…, event se spustí v 12:05 — tedy o 3,5 minuty později, než byl plánován. Pokud potřebujete menší latenci, snižte interval na * * * * * (každou minutu). Frekvence pod minutou v klasickém Unixovém cronu není podporována — museli byste použít systemd timers nebo cyklicky volat skript s sleep.

Oddělit těžké joby do queue systému

WP-Cron je v pořádku pro lehké periodické úlohy: čištění tranzientů, aktualizace metadat, kontrola updates. Není v pořádku pro:

  • Hromadné odesílání tisíců emailů (newslettery).
  • Generování velkých reportů.
  • Synchronizaci velkých datasetů s externími API.
  • Zpracování uploadu velkých souborů.
  • Cokoli, co má SLA na dobu vykonání.

Pro tyhle případy potřebujete skutečný queue systém. Možnosti:

  • Action Scheduler (knihovna od Automattic, používá ji WooCommerce, AutomateWoo, Mailpoet a další). Funguje nad WP-Cronem, ale ukládá joby do vlastních tabulek (actionscheduler_actions, actionscheduler_logs) místo do option cron. To řeší škálovací problémy se serializovanou option a poskytuje robustní logging.
  • Externí queue (Redis přes Predis/PHPRedis, Beanstalkd, RabbitMQ, AWS SQS). Worker procesy běží mimo PHP-FPM jako daemon a zpracovávají úlohy nezávisle na WordPressu.
  • Laravel Horizon / Symfony Messenger, pokud máte hybridní stack a část logiky běží mimo WP.

Action Scheduler vs WP-Cron

Action Scheduler je v podstatě nadstavba, která řeší většinu produkčních problémů WP-Cronu:

  • Joby v dedikované tabulce, ne v serializované option.
  • Real failed/in-progress/completed status pro každou akci.
  • Native podpora pro batching (zpracuje N akcí najednou).
  • Per-job logging (kompletní historie pokusů, errorů, výstupů).
  • Admin UI pro správu (pokud máte WooCommerce, najdete ho v Tools → Scheduled Actions).
  • Kontrola na běhu (max_execution_time s rezervou, abort při dosažení).

API je podobné jako u WP-Cronu:

as_schedule_single_action(time() + 300, 'myplugin_send_email', array($user_id));
as_schedule_recurring_action(time(), HOUR_IN_SECONDS, 'myplugin_sync');
as_unschedule_action('myplugin_send_email', array($user_id));Code language: PHP (php)

Pokud něco z toho děláte ve WooCommerce kontextu, používejte Action Scheduler. Pokud píšete plugin, který má fungovat samostatně, zvažte, jestli ho jako dependency přidat — knihovna je distribuovaná jako Composer package i jako standalone bundle.

Idempotence callbacků

Toto je nejdůležitější pravidlo. Předpokládejte, že váš callback se v krajním případě může spustit dvakrát (race condition, retry, ruční trigger z WP-CLI při debugu). Pokud to způsobí problém, je váš callback špatně.

Ukázka idempotentního callbacku pro odesílání emailu:

function myplugin_send_welcome_email($user_id) {
    $sent = get_user_meta($user_id, '_welcome_email_sent', true);
    if ( $sent ) {
        return;
    }
    $user = get_user_by('id', $user_id);
    if ( ! $user ) {
        return;
    }
    $result = wp_mail($user->user_email, 'Welcome', 'Welcome to our service');
    if ( $result ) {
        update_user_meta($user_id, '_welcome_email_sent', time());
    }
}Code language: PHP (php)

Pokud se callback spustí dvakrát, druhé spuštění skončí na první kontrole. Pokud spadne mezi wp_mail() a update_user_meta(), email může být odeslán dvakrát — ale to je akceptabilní degradace oproti tomu, že by úplně nedošel.

Pro silné garance „přesně jednou“ potřebujete distribuovaný lock (Redis SETNX) nebo idempotence key, podle kterého poznáte, že daná operace už proběhla. WP-Cron tohle z principu neposkytuje.

Retry strategie

WP-Cron nemá zabudovaný retry. Pokud váš callback selže (výjimka, false return), event se prostě v dalším cyklu pokusí znovu — ale jen pokud je to opakovaná úloha. Jednorázové eventy po vykonání mizí před spuštěním callbacku, takže pokud selžou, jsou ztracené.

Vzor pro vlastní retry:

function myplugin_sync_with_retry($attempt = 1) {
    try {
        MyPlugin\ExternalApi::sync();
    } catch (\Throwable $e) {
        if ( $attempt < 5 ) {
            $delay = min(300, pow(2, $attempt) * 30); // exponential backoff
            wp_schedule_single_event(
                time() + $delay,
                'myplugin_sync_with_retry',
                array($attempt + 1)
            );
            error_log("[myplugin] Retry #{$attempt} naplánován za {$delay}s: {$e->getMessage()}");
        } else {
            error_log("[myplugin] Vzdaluji se po {$attempt} pokusech: {$e->getMessage()}");
            // tady případně notifikovat (Slack, sentry, ...)
        }
    }
}
add_action('myplugin_sync_with_retry', 'myplugin_sync_with_retry');Code language: PHP (php)

Action Scheduler tohle dělá nativně přes nastavení retries u jobu — další důvod ho použít pro důležité úlohy.

Závěr

WP-Cron je užitečný nástroj pro to, k čemu byl navržen: drobné periodické úlohy na malých až středně velkých webech, kde nikomu nevadí pár minut latence a kde návštěvnost stačí na to, aby se loopback pravidelně aktivoval. Pro typický blog s pár aktualizacemi denně, čištěním tranzientů a kontrolou updates je naprosto adekvátní.

Jakmile se ale dostanete do produkčního prostředí s konkrétními SLA, dlouhotrvajícími úlohami, vysokou zátěží nebo kritickými business procesy, narazíte na limity. Pseudo-cron, který závisí na návštěvnosti a loopbacku, vám nikdy nedá garance, které potřebujete. V tu chvíli je správný postup: vypnout WP-Cron přes DISABLE_WP_CRON, nahradit ho systémovým cronem (ideálně přes wp cron event run --due-now), těžké úlohy oddělit do queue systému (Action Scheduler, Redis-backed worker, nebo cokoli, co odpovídá vašemu stacku) a každý task psát idempotentně.

Důležitější než znát „best practice řešení“ je rozumět, proč WP-Cron funguje tak, jak funguje. Když víte, že je to jen pasivní mechanismus napojený na HTTP requesty, který ukládá vše do jedné serializované option a synchronizuje se přes nespolehlivý lock, pak vás žádný produkční problém nepřekvapí. Můžete předem rozhodnout, jestli vám tahle abstrakce stačí, nebo jestli potřebujete něco robustnějšího — a hlavně, dokážete debugovat, když to selže. A ono to v určitý okamžik selže vždycky.

Komentáře

Odebírat
Upozornit na
guest
0 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
Zobrazit všechny komentáře

Baro vs. Claude Code: Když paralelní agenti prohrají s jedním sezením (a co s tím udělat)

AI
Komentáře: 0
Více agentů musí být rychlejší než jeden, ne? Miodrag Todorović z JigJoy postavil baro - CLI, které paralelně spouští pět Claude sezení místo jednoho - a postavil ho proti novému příkazu /goal v Claude Code. Čekal jasnou výhru paralelismu. Místo toho prohrál v čase, v tokenech i v kvalitě výsledného kódu. Z analýzy tří konkrétních selhání ale vyšel jeden nečekaný závěr: problém nebyl v koordinaci mezi agenty, ale v rozhodnutích, která padla ještě před tím, než se kdokoli z nich probudil. Oprava trvala 200 řádků kódu - a v odvetě baro porazilo /goal o 4 minuty.