Atualizado em 19/06/2026
Quase todo site WordPress precisa, em algum momento, rodar uma tarefa "sozinho": limpar transients expirados, verificar atualizações, publicar um post agendado, sincronizar estoque com um ERP, enviar um resumo por e-mail. O WordPress oferece para isso o WP-Cron — um agendador de tarefas. O problema é que ele tem um comportamento que confunde quase todo mundo no começo e que, mal compreendido, gera tarefas que "não rodam no horário", rodam em duplicidade ou simplesmente nunca rodam. Este guia explica como o WP-Cron funciona por dentro, como agendar corretamente e como torná-lo confiável em produção.
1. O ponto mais importante: WP-Cron não é o cron do sistema
Se você guardar uma única coisa deste artigo, que seja esta: o WP-Cron não tem um relógio próprio. Ele não é um daemon rodando no servidor que dispara tarefas em horários exatos. O nome engana.
O que acontece de fato é o seguinte: a cada requisição ao site, perto do fim do carregamento, o WordPress dispara uma chamada HTTP de loopback (uma requisição não-bloqueante para si mesmo) para o arquivo wp-cron.php. Esse arquivo verifica a fila de eventos agendados e, se encontrar algum cujo horário já passou, executa o hook correspondente. Em outras palavras:
- O WP-Cron só é verificado quando alguém visita o site (front-end ou admin).
- A verificação é "preguiçosa": acontece como efeito colateral de uma visita, não em um horário pré-definido.
Isso tem duas consequências práticas que você precisa internalizar antes de escrever qualquer linha de código:
Site sem tráfego não executa tarefas no horário. Se ninguém acessa o site entre 2h e 6h, uma tarefa agendada para as 3h só vai disparar na primeira visita após esse horário. Num site de baixo movimento, atrasos de horas são normais.
Site com muito tráfego pode disparar a checagem com frequência demais. Em picos, cada visita tenta abrir a chamada de loopback, o que adiciona sobrecarga. É por isso que, em produção séria, o padrão é desligar o WP-Cron e controlar o disparo pelo cron real.
2. Agendar uma tarefa: recorrente e evento único
Há duas funções centrais. Para algo que se repete em intervalos, use wp_schedule_event(); para disparar uma vez no futuro, use wp_schedule_single_event():
<?php
// Recorrente: ( timestamp inicial, recorrência, hook, args opcionais )
wp_schedule_event( time(), 'hourly', 'dwp_sincroniza_estoque' );
// Evento único: ( timestamp futuro, hook, args opcionais )
wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'dwp_envia_email_boas_vindas', array( $user_id ) );
Os timestamps são sempre Unix, em UTC. Constantes como HOUR_IN_SECONDS, DAY_IN_SECONDS e WEEK_IN_SECONDS existem no núcleo justamente para deixar esses cálculos legíveis.
O callback precisa estar ganchado SEMPRE. Agendar só coloca o hook na fila. Quem executa o trabalho é o add_action() ligado àquele hook — e ele precisa estar registrado em toda requisição, não apenas no momento em que você agendou. Esse é, de longe, o erro nº 1 de quem usa WP-Cron pela primeira vez.
3. As recorrências padrão e como criar a sua
O WordPress já traz quatro intervalos prontos. A partir do WordPress 5.4, o weekly também faz parte do núcleo:
| Recorrência | Intervalo | Uso típico |
|---|---|---|
hourly | 1 hora | Sincronizar dados, checar filas |
twicedaily | 12 horas | Verificações duas vezes ao dia |
daily | 24 horas | Relatórios, limpeza diária |
weekly | 7 dias (WP 5.4+) | Resumos semanais, manutenção |
Quando esses intervalos não bastam (por exemplo, "a cada 15 minutos"), você cria a sua própria recorrência com o filtro cron_schedules. Lembre-se: um filtro sempre retorna o valor — aqui, o array de recorrências com a sua adicionada:
<?php
add_filter( 'cron_schedules', 'dwp_intervalos_personalizados' );
function dwp_intervalos_personalizados( $schedules ) {
$schedules['cada_15_min'] = array(
'interval' => 15 * MINUTE_IN_SECONDS, // em segundos
'display' => __( 'A cada 15 minutos', 'meu-plugin' ),
);
return $schedules; // nunca esqueça de retornar o array completo
}
Depois disso, você pode usar 'cada_15_min' como recorrência em wp_schedule_event(), exatamente como usaria 'hourly'.
4. Agendar na ativação e evitar duplicação
Onde agendar? O lugar certo é o register_activation_hook() do plugin, que roda uma única vez, quando o plugin é ativado. Mas mesmo ali você deve verificar se o evento já não existe, com wp_next_scheduled() — caso contrário, reativar o plugin (ou rodar o agendamento por engano em outro ponto) empilha eventos duplicados, e a tarefa passa a rodar várias vezes.
<?php
// Roda UMA vez, na ativação do plugin
register_activation_hook( __FILE__, 'dwp_agendar_eventos' );
function dwp_agendar_eventos() {
// Só agenda se ainda não houver esse evento na fila
if ( ! wp_next_scheduled( 'dwp_sincroniza_estoque' ) ) {
wp_schedule_event( time(), 'hourly', 'dwp_sincroniza_estoque' );
}
}
// IMPORTANTE: o gancho do callback fica FORA da ativação,
// no carregamento normal do plugin, para rodar em toda requisição.
add_action( 'dwp_sincroniza_estoque', 'dwp_executa_sincronizacao' );
function dwp_executa_sincronizacao() {
// ... o trabalho de verdade acontece aqui.
// Mantenha idempotente: rodar de novo não deve duplicar efeitos.
}
5. Limpar o agendamento na desativação
Ao desativar o plugin, você precisa remover os eventos da fila — senão o WordPress continuará tentando disparar um hook cujo código não está mais carregado. Use wp_clear_scheduled_hook(), que remove todos os eventos agendados para aquele hook:
<?php
register_deactivation_hook( __FILE__, 'dwp_limpar_eventos' );
function dwp_limpar_eventos() {
wp_clear_scheduled_hook( 'dwp_sincroniza_estoque' );
}
Não confunda desativar com desinstalar. Limpe os agendamentos na desativação (com register_deactivation_hook). Se o seu evento foi agendado com argumentos (o array $args), o WordPress trata cada combinação de argumentos como um agendamento distinto — wp_clear_scheduled_hook() remove todos eles para aquele hook, o que normalmente é o que você quer.
6. Confiabilidade e performance: desligue o WP-Cron em produção
Esta é a parte que separa o código amador do profissional. Como vimos, depender das visitas é frágil (tarefas atrasam) e custoso (cada visita tenta um loopback). A solução padrão de mercado é desabilitar o WP-Cron e disparar o WordPress por um cron de verdade.
Primeiro, no wp-config.php, antes do comentário "That's all, stop editing":
<?php
// Impede o WordPress de disparar o cron a partir das visitas
define( 'DISABLE_WP_CRON', true );
Com isso, a fila de eventos continua existindo e sendo gravada normalmente — você apenas tira o gatilho "por visita". Agora, configure o cron do sistema para chamar o WordPress em intervalos fixos. A forma recomendada é via WP-CLI, porque ele roda os eventos vencidos no contexto PHP correto, sem depender de HTTP:
# crontab -e (no servidor)
# Roda os eventos vencidos a cada 5 minutos via WP-CLI (recomendado)
*/5 * * * * cd /var/www/seusite/html && wp cron event run --due-now >/dev/null 2>&1
# Alternativa sem WP-CLI: chamar wp-cron.php por HTTP
# */5 * * * * wget -q -O - "https://seusite.com.br/wp-cron.php?doing_wp_cron" >/dev/null 2>&1
# (ou, com curl:)
# */5 * * * * curl -s "https://seusite.com.br/wp-cron.php?doing_wp_cron" >/dev/null 2>&1
A diferença entre as duas abordagens importa. Chamar wp-cron.php por wget/curl funciona, mas passa por toda a pilha HTTP do site e está sujeito a cache, firewall e timeouts da requisição web. O wp cron event run --due-now executa em PHP-CLI, sem o teto de tempo de uma requisição web e sem passar por proxy/cache — é mais robusto para tarefas que demoram.
7. Depurar o que está agendado
Você não precisa adivinhar o que está na fila. Com o WP-CLI, wp cron event list mostra todos os eventos, o hook, a recorrência e quando cada um dispara — essencial para diagnosticar duplicações e atrasos:
# Lista todos os eventos agendados (hook, próxima execução, recorrência)
wp cron event list
# Lista as recorrências disponíveis (inclui as suas, via cron_schedules)
wp cron schedule list
# Dispara manualmente um hook específico, ignorando o horário
wp cron event run dwp_sincroniza_estoque
Resumo das funções principais
| Função | Para quê |
|---|---|
wp_schedule_event() | Agendar um evento recorrente (com recorrência) |
wp_schedule_single_event() | Agendar um evento único no futuro |
wp_next_scheduled() | Verificar o próximo disparo de um hook (evita duplicar) |
wp_clear_scheduled_hook() | Remover todos os eventos de um hook (na desativação) |
wp_unschedule_event() | Remover um evento específico (timestamp + hook + args) |
Armadilhas comuns (e como evitar)
| Sintoma | Causa provável | Correção |
|---|---|---|
| Tarefa nunca executa | add_action do hook só registrado na ativação | Registre o add_action no carregamento normal, sempre |
| Tarefa atrasa horas | Site de baixo tráfego (WP-Cron por visita) | Desligue o WP-Cron e use cron do sistema |
| Tarefa roda em duplicidade | Agendou sem checar wp_next_scheduled() | Verifique antes de wp_schedule_event() |
| Recorrência customizada ignorada | Filtro cron_schedules não retornou o array | Sempre dê return $schedules |
| Tarefa morre no meio | Estourou o max_execution_time | Divida em lotes e reagende; torne idempotente |
| Horário errado | Confusão de fuso (tudo é UTC internamente) | Calcule timestamps em UTC; cuidado com strtotime() |
O WP-Cron é um agendador poderoso desde que você respeite a regra de ouro — ele depende de um gatilho. Para tarefas críticas (cobranças, sincronização de estoque, backups), nunca confie no disparo por visita: desligue o WP-Cron e use o cron do sistema. E lembre-se de que o agendamento vive dentro de um plugin que registra seus hooks em toda requisição.
Perguntas frequentes
O WP-Cron é o mesmo que o cron do sistema (crontab)?
Não, e essa é a confusão que mais causa problema. O WP-Cron é um agendador simulado dentro do WordPress: ele não tem um relógio próprio rodando no servidor. Em vez disso, a cada requisição o WordPress dispara uma chamada de loopback para o arquivo wp-cron.php, que verifica se há tarefas vencidas e as executa. O cron do sistema (o crontab do Linux) é o oposto: um daemon que roda comandos em horários exatos, independentemente de haver visitas. O ideal, em produção, é desligar o WP-Cron e deixar o cron do sistema disparar o WordPress.
Por que minha tarefa agendada não roda no horário certo?
Porque o WP-Cron só é verificado quando alguém acessa o site. Se o seu blog não recebe visitas entre 2h e 6h da manhã, uma tarefa agendada para as 3h só vai rodar quando chegar a primeira visita depois disso — pode ser às 8h. Em sites de baixo tráfego o atraso é a regra, não a exceção. A solução é usar o cron real do sistema chamando wp-cron.php (ou, melhor, o WP-CLI) em intervalos fixos.
Por que o callback precisa estar ganchado fora do momento de agendar?
Porque agendar um evento (wp_schedule_event) só grava na fila qual hook deve disparar e quando. Quem executa de fato é o add_action() ligado àquele hook. Se você registrar o add_action apenas dentro do bloco de ativação, no momento em que o cron disparar (em outra requisição, dias depois) o callback não estará pendurado e nada acontece. O add_action tem que rodar em toda requisição, no carregamento normal do plugin.
Como evito que a tarefa seja agendada várias vezes?
Sempre verifique com wp_next_scheduled( 'seu_hook' ) antes de chamar wp_schedule_event(). Essa função devolve o timestamp do próximo evento agendado para aquele hook, ou false se não houver nenhum. Agendar dentro do register_activation_hook() (que roda uma única vez) somado a essa verificação evita filas com o mesmo evento repetido, que fariam a tarefa rodar em duplicidade.
Em qual fuso horário o WP-Cron trabalha?
Internamente, tudo é UTC. Os timestamps que você passa para wp_schedule_event() e wp_schedule_single_event() devem ser timestamps Unix (sempre em UTC). Para agendar "às 3h da manhã no horário de Brasília", calcule o timestamp considerando o fuso — usar time() já devolve UTC corretamente, mas montar horários "humanos" com strtotime() exige cuidado com o fuso configurado no PHP e no WordPress. Não confie no fuso do painel para a lógica do agendamento.
O que acontece se minha tarefa de cron demorar muito?
Como o WP-Cron roda dentro de uma requisição PHP, ele está sujeito ao max_execution_time. Uma tarefa longa (processar milhares de registros, gerar um relatório pesado) pode estourar o tempo e morrer no meio, deixando o trabalho pela metade. A correção é dividir o trabalho em lotes, reagendando o próximo lote como evento único, e tornar a tarefa idempotente — segura para rodar de novo sem duplicar efeitos. Veja também otimização de performance.