La orden funciona en tu terminal y no produce nada a las tres de la madrugada. Cron no está roto. Ejecuta tu orden en un entorno que casi no se parece en nada a tu shell, y esa diferencia es toda la respuesta.

Mira qué ocurrió

grep CRON /var/log/syslog | tail -20

Si tu tarea no aparece ahí, cron ni lo intentó. Si aparece, cron la ejecutó y lo que falló fue otra cosa.

1. PATH está casi vacío

Tu shell sabe dónde está php. Cron no: su PATH suele ser solo /usr/bin:/bin. Usa siempre la ruta completa.

which php
# /usr/bin/php8.3

# in the crontab
0 3 * * * /usr/bin/php8.3 /var/www/site/cron.php

2. La salida no va a ninguna parte

Una tarea sin redirección manda su salida a un correo que no está configurado, así que los errores se evaporan. Mándala a un archivo y el próximo fallo se explicará solo.

0 3 * * * /usr/bin/php8.3 /var/www/site/cron.php >> /var/log/site-cron.log 2>&1

3. El usuario equivocado

Una tarea en la crontab de root escribe archivos que pertenecen a root, y el servidor web ya no puede cambiarlos. Ponla en la crontab del usuario dueño del sitio.

crontab -u youruser -l

4. Un signo de porcentaje

En una crontab, % significa salto de línea. Un formato de fecha como +%Y-%m-%d se convierte en silencio en tres líneas y la orden se rompe. Escápalo: \%.

Pruébala como la ejecutará cron

env -i /bin/sh -c "/usr/bin/php8.3 /var/www/site/cron.php"

Eso la ejecuta con un entorno vacío, que es lo que hace cron. Si falla aquí, fallará a las tres.

El cron de WordPress no es cron: se dispara al cargar páginas, así que un sitio tranquilo no ejecuta nunca sus tareas. Desactiva WP_CRON y llama a wp-cron.php desde una tarea cron de verdad.