La API de WordPress permite configurar eventos de ejecución periódica, más conocidos como cron jobs o tareas programadas, a través de la función wp_schedule_event(). Los casos de uso son muy variados. Por ejemplo, el core de WordPress lo utiliza para comprobar actualizaciones o para publicar posts futuros.

A lo largo de este tutorial veremos como registrar nuestros propios cron jobs en WordPress, como funcionan y como configurar cron jobs en el servidor para mejorar las limitaciones de WordPress.

La función wp_schedule_event()

La sintaxis de la función wp_schedule_event() es:

wp_schedule_event(integer $timestamp, string $recurrence, string $hook[, array $args]);

Dónde:

$timestamp
(int) (requerido) Número entero que indica la fecha y hora en formato UNIX a la que se ejecutará el cron job por primera vez. Para que esta primera vez ocurra cuando se crea el cron job («ahora»), se puede utilizar current_time( 'timestamp' ).
$recurrence
(string) (requerido) Periodicidad con la que se ejecutará el evento. Puede ser hourly (cada hora), twicedaily (cada 12 horas) o daily (cada 24 horas). Si queremos intervalos diferentes, por ejemplo cada cinco minutos, semanal o mensual, tenemos que definir nuestros propios intervalos como veremos a continuación.
$hook
(string) (requerido) Nombre del evento (action hook) que se ejecutará en el intervalo especificado.
$args
(array) (opcional) Este es el único parámetro opcional. Es un array que podemos utilizar si necesitamos pasar parámetros al action definido en $hook.

Para evitar que terminemos con el mismo cron job programado numerosas veces, wp_schedule_event() se ha de ejecutar una única vez por cada cron job que vayamos a registrar, generalmente durante la activación del plugin. Se puede comprobar si el cron job ya existe con wp_next_scheduled().

Igualmente es importante eliminar el cron job durante la desactivación del plugin, de lo contrario el cron job seguirá intentado ejecutarse aunque el plugin no se encuentre activo.

Ejemplo básico

// 1.- Registramos el evento programado al desactivar el plugin
register_activation_hook( __FILE__, 'cyb_plugin_activation' );
function cyb_plugin_activation() {
    // Si no existe el evento, lo registramos
    if( ! wp_next_scheduled( 'cyb_hourly_cron_job' ) ) {
        wp_schedule_event( current_time( 'timestamp' ), 'hourly', 'cyb_hourly_cron_job' );
    }
}

// 2.- Anclamos una función al evento registrado anteriormente
add_action( 'cyb_hourly_cron_job', 'cyb_hourly_do_this_job' );
function cyb_hourly_do_this_job() {
    // Hacer algo cada hora
}

// 3.- Eliminamos el evento al desactivar el plugin
register_deactivation_hook( __FILE__, 'cyb_plugin_deactivation' );
function cyb_plugin_deactivation() {
    wp_clear_scheduled_hook( 'cyb_hourly_cron_job' );
}

El código anterior sigue esta secuencia:

  1. Durante la activación del plugin se registra el evento programado cyb_hourly_cron_job. Se ejecutará por primera vez en el momento de activación del plugin y se repetirá cada hora.
  2. Se utiliza add_action() para anclar una función al evento que hemos registrado en el paso anterior. Este paso se puede repetir cuantas veces necesitemos y anclar varias funciones al mismo evento. WordPress ejecutará cada hora do_action( 'cyb_hourly_cron_job' ) y ejecutará todas las funciones que hayamos anclado al evento. En este caso solo hay una, la función cyb_hourly_do_this_job(). Ya tenemos el cron job listo.
  3. Durante la desactivación del plugin se elimina el cron job con la función wp_clear_scheduled_hook().

Registro de intervalos propios

WordPress ofrece tres intervalos predefinidos: hourly, twicedaily y daily. Si queremos registrar un cron job para que se ejecute en intervalos diferentes debemos utilizamos el filtro cron_shedules para definir el nombre del intervalo y el número de segundos que contiene este intervalo.

Por ejemplo, para definir el intervalo semanal haríamos lo siguiente:

add_filter( 'cron_schedules', 'cyb_cron_schedules');
function cyb_cron_schedules( $schedules ) {
     $schedules['weekly'] = array(
        'interval' => 604800, // segundos en una semana (7 días)
        'display' => __( 'Weekly', 'cyb-textdomain' ) //nombre del intervalo
     );
     return $schedules;
}

Ejemplo: cron jobs semanal y mensual

Ya hemos visto como registrar un cron job y como definir los intervalos que necesitemos en cada caso. Para registrar un cron job semanal y otro mensual podríamos hacer lo siguiente:

add_filter( 'cron_schedules', 'cyb_cron_schedules');
function cyb_cron_schedules( $schedules ) {

     $schedules['weekly'] = array(
        'interval' => 604800, // segundos en una semana
        'display' => __( 'Weekly', 'cyb-textdomain' ) //nombre del intervalo
     );

     $schedules['monthly'] = array(
        'interval' => 2592000, // segundos en 30 dias
        'display' => __( 'Monthly', 'cyb-textdomain' ) // nombre del intervalo
     );

     return $schedules;
}

register_activation_hook( __FILE__, 'cyb_plugin_activation' );
function cyb_plugin_activation() {

    if( ! wp_next_scheduled( 'cyb_weekly_cron_job' ) ) {
        wp_schedule_event( current_time( 'timestamp' ), 'weekly', 'cyb_weekly_cron_job' );
    }

    if( ! wp_next_scheduled( 'cyb_monthly_cron_job' ) ) {
        wp_schedule_event( current_time( 'timestamp' ), 'monthly', 'cyb_monthly_cron_job' );
    }
}

add_action( 'cyb_weekly_cron_job', 'cyb_do_this_job_weekly' );
function cyb_do_this_job_weekly() {
	// Hacer algo cada semana
}

add_action( 'cyb_monthly_cron_job', 'cyb_do_this_job_monthly' );
function cyb_do_this_job_monthly() {
	// Hacer algo cada mes
}

register_deactivation_hook( __FILE__, 'cyb_plugin_deactivation' );
function cyb_plugin_deactivation() {
     wp_clear_scheduled_hook( 'cyb_weekly_cron_job' );
     wp_clear_scheduled_hook( 'cyb_monthly_cron_job' );
}

Pasando parámetros al cron job

Como vimos en la sintaxis de wp_schedule_event(), el cuarto parámetro es un array opcional con el que podemos pasar parámetros al callback anclado al cron job. El array utilizado ha de ser un array secuencial (indexado o no asociativo). WordPress transforma cada elemento del array en un parámetro individual y los pasa a la función del cron job.

Ten en cuenta que al definir las funciones a ejecutar con add_action tenemos que decir el número de argumentos que se aceptan (vea la documentación add_action).

Por ejemplo, si se pasan dos parámetros:

register_activation_hook( __FILE__, 'cyb_plugin_activation' );
function cyb_plugin_activation() {

    if( ! wp_next_scheduled( 'cyb_weekly_cron_job' ) ) {
	wp_schedule_event(
         current_time( 'timestamp' ),
         'daily',
         'cyb_weekly_cron_job',
         array( 'valor_param_1', 'valor_param_2' )
       );
    }
}

// El último parámetro indica el número de parámetros que acepta
// el callback. En este caso 2.
add_action( 'cyb_daily_cron_job', 'cyb_do_this_job_daily', 10, 2 );
function cyb_do_this_job_daily( $param1, $param2 ) {
	// Hacer algo cada día
}

Limitaciones de los cron jobs en WordPress

Cada vez que WordPress recibe una visita se carga el archivo wp-cron.php, se comprueba si hay alguna tarea programada pendiente y, sí la hay, se ejecuta. La carga del archivo wp-cron.php se realiza mediante una solicitud HTTP separada, de esta forma el usuario que ha generado la visita no tiene que esperar a la ejecución de wp-cron.php.

Pero esta forma de funcionamiento también provoca importantes limitaciones. Destacan:

  1. Los cron jobs no se ejecutan si no hay visitas a la web. Tampoco si se utilizan sistemas de caché que simulan HTML estático ya que, aunque haya visitas, se sirve un documento HTML creado previamente sin que intervenga WordPress para nada. Algunas tareas programadas pueden retrasarse considerablemente.
  2. El archivo wp-cron.php se carga en cada visita a la web. Si tienes 1000 visitas concurrentes, habrá 1000 cargas de wp-cron.php, lo que es un gasto innecesario de recursos.

Imagina que registras un cron job para que se ejecute cada 5 minutos y tu web no recibe visitas durante 1 hora. Durante toda esa hora el cron job no se habrá ejecutado ni una sola vez. Por este motivo, si necesitas un cron job que se ejecute en un intervalo exacto, los cron jobs de WordPress no son apropiados.

Además, si hay tareas intensas que realizar en los cron jobs y estos no se ejecutan durante mucho tiempo, se pueden acumular, generar picos muy altos de consumo de recursos y, en los casos más extremos, colapsar sin que las tareas hayan terminado y generar múltiples ejecuciones de wp-cron al mismo tiempo.

Afortunadamente hay solución.

Cron jobs en el servidor para controlar wp-cron

Para solucionar los problemas anteriores, se necesitan dos cosas:

1.- Desactivar la carga normal de wp-cron.php realizada por WordPress. Para hacerlo añade esta línea a wp-config.php:

define( 'DISABLE_WP_CRON', true );

2.- Configurar un cron job a nivel de servidor que cargue wp-cron.php. El comando de este cron job sería:

wget -q "https://ejemplo.es/wp-cron.php?doing_wp_cron"

El comando wget crea un archivo con la respuesta en cada ejecución, si no quieres que se cree este archivo y tampoco quieres recibir las notificaciones del cron al email:

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

El intervalo idóneo para el cron job del servidor es el intervalo más pequeño utilizado por las tareas programadas existentes en WordPress. Puedes verlas todas y conocer cual es el intervalo más pequeño con plugins como WP Crontrol. Por ejemplo en esta web utilizo 5 minutos.

De esta forma nos aseguramos que wp-cron.php se ejecuta a intervalos regulares de forma exacta, no perdemos ninguna tarea, no importa si tenemos visitas o no y podemos utilizar sistemas de caché sin problemas.

Además, wp-cron.php se carga una sola vez a cada intervalo establecido en el cron job del servidor, tengamos 1 o 1 millón de visitas. No hay recursos malgastados y wp-cron.php se hace fácilmente escalable.

WP-Cron Control

Personalmente soy fan del plugin WP-Cron Control (no confundir con WP Crontrol). Este plugin requiere la configuración de un cron job en el servidor, igual que antes, pero además asegura que no se producen múltiples ejecuciones de wp-cron.php al mismo tiempo.

Como ventaja extra permite la ejecución de wp-cron.php mediante php-cli, que supone importantes ventajas sobre el comando wget. wp-cron.php no es adecuado para su ejecución mediante php-cli pero WP-Cron Control tiene su propio script que sí se puede cargar mediante php-cli y a través de este script ejecuta wp-cron.php.

Echale un vistazo, es genial.