Tắt WP-Cron và chạy bằng system cron
WP-Cron chỉ chạy khi có người mở trang nên dễ trễ hoặc dồn job. Tắt bằng DISABLE_WP_CRON, dùng WP-CLI với system cron và kiểm tra event đã chạy.
WP-Cron là gì và vì sao system cron thay thế nó
WP-Cron là bộ lập lịch tác vụ tích hợp trong WordPress. Nó chỉ chạy khi có người yêu cầu một trang. Không có thành phần nào trong WordPress tự thức dậy để chạy. Với mỗi request không được phục vụ từ cache, WordPress đọc danh sách các job đã lên lịch. Nếu có job đến hạn, WordPress gửi thêm một HTTP request đến chính nó tại /wp-cron.php để thực hiện job đó. Chuyển job này sang system cron giúp job chạy một lần theo lịch cố định và có thể dự đoán, bất kể trong phút đó site có một nghìn visitor hay không có visitor nào.
Hai dòng thực hiện công việc chính: một constant trong wp-config.php và một mục crontab. Mọi nội dung khác trong hướng dẫn này giải thích những điều mà hai dòng đó không thể hiện. Job phải chạy với user nào, cách xác nhận các event đã lên lịch thực sự được thực thi, và 3 cách cấu hình có thể fail mà không hiển thị lỗi nào trên site.
Các ví dụ sử dụng /srv/www/example.com làm thư mục WordPress và www-data làm user của web server. Hãy thay bằng path và user của bạn ở mọi vị trí.
Chi phí cron trên một site bận do visitor nào kích hoạt
Mỗi request không lấy từ cache đều phải thực hiện bước kiểm tra này. WordPress tải option cron, so sánh timestamp, rồi khi có tác vụ đến hạn sẽ gọi spawn_cron(). Hàm này gửi một loopback request không chặn đến /wp-cron.php. Visitor không phải chờ kết quả. Một PHP worker thì có. Trên VPS nhỏ chạy PHP-FPM với pm.max_children = 5, một scheduled job chậm có thể chiếm 1/5 năng lực PHP trong suốt thời gian nó chạy. Job đó nhiều khả năng được kích hoạt vào phút bận nhất, vì đó là lúc có nhiều page load nhất.
WordPress có giới hạn việc chạy trùng. Nó tạo một lock có thời hạn WP_CRON_LOCK_TIMEOUT, mặc định là 60 giây, để các visitor đồng thời không cùng khởi chạy một lần chạy. Lock này giới hạn việc chạy trùng. Nó không đưa công việc ra khỏi request path.
Hãy đếm tần suất nó chạy trên chính server của bạn trước khi quyết định vấn đề này có đáng kể hay không. Mỗi loopback đều xuất hiện trong access log của web server:
sudo grep -c 'wp-cron.php' /var/log/nginx/access.logApache ghi vào /var/log/apache2/access.log thay vào đó. Hàng nghìn lần mỗi ngày là một chi phí thực tế. Bạn nên đo con số này trên chính máy của mình thay vì chỉ đọc trong một bài viết, giống như khi bạn benchmark VPS trước và sau một thay đổi khác.
Cache làm thay đổi vấn đề. Nếu page cache phục vụ hầu hết request dưới dạng HTML tĩnh, PHP không chạy cho những request đó, nên bước kiểm tra cron cũng không xảy ra. Một site bận có cache tốt sẽ bắt đầu hoạt động giống site yên ắng bên dưới.
Lượt truy cập nào đã kích hoạt cron trên một site ít người truy cập
Không có người truy cập thì không có cron. Một site chỉ có vài lượt truy cập mỗi ngày sẽ chạy các job đã lên lịch cũng chỉ vài lần mỗi ngày, vào những thời điểm ngẫu nhiên khi các lượt truy cập đó xảy ra.
Các triệu chứng thường giống nhau. Một bài viết được lên lịch đăng lúc 09:00 vẫn nằm trong danh sách bài viết với trạng thái Missed schedule cho đến khi có người tải một trang. Plugin backup bỏ qua lần chạy ban đêm. Việc kiểm tra bản cập nhật bị chậm, nên dashboard không hiển thị bản nào cần cập nhật trong khi bản security release đã được phát hành. Email đơn hàng, thông báo gia hạn và cảnh báo hết hạn đều được gửi muộn.
Không có lỗi nào được ghi vào log. Theo cách WordPress nhìn nhận, job không hề bị trễ vì nó chưa từng được khởi chạy.
Bước 1: tắt trigger từ visitor trong wp-config.php
Mở /srv/www/example.com/wp-config.php và thêm constant sau:
define( 'DISABLE_WP_CRON', true );Đặt nó bên trên dòng có nội dung /* That's all, stop editing! Happy publishing. */, vì dòng ngay dưới comment đó yêu cầu wp-settings.php, còn wp-settings.php là nơi WordPress gắn cron check vào init. Nếu định nghĩa constant sau lệnh require đó thì đã quá muộn để thay đổi hành vi. File vẫn trông đúng, nhưng trigger vẫn tiếp tục chạy.
Xác nhận dòng này nằm đúng vị trí:
grep -n "DISABLE_WP_CRON\|stop editing" /srv/www/example.com/wp-config.phpConstant này không ngăn việc schedule event. Các plugin vẫn tiếp tục thêm job vào queue như trước. Nó chỉ ngăn page load chạy queue đó. Vì vậy queue sẽ không chạy nữa cho đến khi bạn hoàn tất bước 3.
Nó cũng không chặn request trực tiếp đến /wp-cron.php. Bất kỳ ai vẫn có thể request URL đó. Việc này thường không gây vấn đề vì file chỉ chạy các job đã đến hạn. Bạn có thể tùy chọn chặn URL này trong cấu hình web server. Nếu chặn, fallback curl ở gần cuối guide này cũng sẽ ngừng hoạt động.
Bước 2: cài đặt WP-CLI
WP-CLI là công cụ dòng lệnh chính thức cho WordPress. Công cụ này cần binary PHP cho dòng lệnh. Đây là package riêng với PHP module của web server.
php -v
sudo apt install -y php-cliCài WP-CLI từ bản build phar. Đây là cách được hướng dẫn cài đặt chính thức khuyến nghị:
cd /tmp
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
php wp-cli.phar --info
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --infophp wp-cli.phar --info in đường dẫn đến binary PHP, phiên bản PHP và phiên bản WP-CLI. Nếu cả ba giá trị đều được in ra thì phar hoạt động. Tính đến tháng 8 năm 2026, hướng dẫn cài đặt yêu cầu tối thiểu PHP 7.2.24. Ubuntu 24.04 đi kèm PHP 8.3, nên server hiện tại cao hơn đáng kể so với mức này. Sau này cập nhật bằng sudo wp cli update.
Chạy WP-CLI bằng user của site, không bao giờ chạy bằng root:
sudo -u www-data /usr/local/bin/wp --path=/srv/www/example.com core versionKhi chạy bằng root, WP-CLI sẽ từ chối khởi động:
Error: YIKES! It looks like you're running this as root.Công cụ đề xuất --allow-root. Không dùng cách đó ở đây. Lý do được giải thích trong failure mode đầu tiên bên dưới.
Lưu ý rằng sudo -u www-data -i không hoạt động vì login shell của account đó là /usr/sbin/nologin và bạn nhận được This account is currently not available.. Truyền lệnh thẳng cho sudo -u sẽ bỏ qua login shell, nên lệnh chạy bình thường.
Bây giờ xác nhận WordPress thực sự đọc được constant từ bước 1:
sudo -u www-data /usr/local/bin/wp --path=/srv/www/example.com eval 'var_dump( DISABLE_WP_CRON );'Lệnh này in ra bool(true). Fatal error báo constant chưa được định nghĩa nghĩa là dòng define() chưa được thực thi. Nguyên nhân thường là dòng đó được đặt bên dưới require.
Bước 3: thêm cron entry dưới đúng user
User đúng là user sở hữu các file mà PHP ghi vào. Kiểm tra cả hai phía:
stat -c '%U %G' /srv/www/example.com/wp-content/uploads
grep -E '^(user|group) = ' /etc/php/8.3/fpm/pool.d/www.confTrên bản cài Ubuntu mặc định, cả hai lệnh đều trả về www-data. Nếu bạn cấp cho site một PHP-FPM pool riêng với user riêng, thường là kết quả của cách thiết lập theo từng site trong LAMP stack trên Ubuntu 24.04, hãy dùng user đó cho mọi bước bên dưới.
Tạo một thư mục log mà user đó có thể ghi:
sudo install -d -o www-data -g www-data -m 750 /var/log/wp-cronChỉnh sửa crontab của user đó:
sudo crontab -u www-data -eThêm một dòng:
*/5 * * * * /usr/bin/flock -n /run/lock/wp-cron-example.lock /usr/local/bin/wp --path=/srv/www/example.com cron event run --due-now >> /var/log/wp-cron/example.log 2>&1Giải thích từng phần. */5 chạy lệnh mỗi 5 phút. flock -n lấy một lock file và thoát ngay nếu lần chạy trước vẫn đang giữ lock. /usr/local/bin/wp là đường dẫn tuyệt đối mà cron yêu cầu. --path cho phép lệnh chạy từ bất kỳ working directory nào. --due-now chỉ chạy các event đã đến thời điểm, thay vì chạy mọi event trong queue. Redirect chuyển output thông thường và lỗi vào một file duy nhất để bạn đọc.
Trong thực tế, redirect này không phải tùy chọn. Cron gửi output của job qua mail cho user, nhưng hầu hết image VPS không cài mail transfer agent. Sau đó cron ghi log (CRON) info (No MTA installed, discarding output) và loại bỏ output. Lưu vào file giúp giữ lại bằng chứng.
Kiểm tra file đã được lưu:
sudo crontab -u www-data -lVới nhiều site, dùng mỗi site một dòng và lệch phút chạy để chúng không khởi động cùng lúc:
*/5 * * * * /usr/bin/flock -n /run/lock/wp-cron-one.lock /usr/local/bin/wp --path=/srv/www/one.example.com cron event run --due-now >> /var/log/wp-cron/one.log 2>&1
2-59/5 * * * * /usr/bin/flock -n /run/lock/wp-cron-two.lock /usr/local/bin/wp --path=/srv/www/two.example.com cron event run --due-now >> /var/log/wp-cron/two.log 2>&1Log sẽ tăng mãi nếu bạn không rotate nó. Ghi /etc/logrotate.d/wp-cron:
/var/log/wp-cron/*.log {
weekly
rotate 4
missingok
notifempty
compress
create 640 www-data www-data
}Kiểm tra cú pháp mà không thực hiện thay đổi nào: sudo logrotate --debug /etc/logrotate.d/wp-cron.
Bước 4: xác nhận các event đã lên lịch thực sự được chạy
Một dòng crontab lưu thành công không chứng minh được gì. Hãy kiểm tra từ bước đơn giản nhất đến bước có thể xác nhận chắc chắn.
Trước tiên, cron có khởi chạy command không? Cron ghi log vào journal trong unit riêng:
journalctl -u cron.service --since "15 min ago" | grep wpMột entry bình thường có dạng như sau, sau khi bỏ timestamp và hostname ở đầu dòng:
CRON[24913]: (www-data) CMD (/usr/bin/flock -n /run/lock/wp-cron-example.lock /usr/local/bin/wp --path=/srv/www/example.com cron event run --due-now >> /var/log/wp-cron/example.log 2>&1)Dòng này có nghĩa cron đã khởi chạy command của bạn với tư cách www-data. Dòng này không cho biết command có chạy thành công hay không.
Tiếp theo, WordPress có thực thi gì không? Đọc file log:
sudo tail -n 20 /var/log/wp-cron/example.logWP-CLI in một dòng cho mỗi event, sau đó in tổng số:
Executed the cron event 'wp_version_check' in 0.418s.
Success: Executed a total of 2 cron events.Lỗi cũng được ghi vào cùng file. Đây là mục đích của 2>&1. Hầu hết các lần chạy sẽ không có event nào đến hạn và chỉ ghi rất ít nội dung. Vì vậy, hãy đọc file sau một lần chạy mà bạn biết đang có event chờ xử lý.
Cuối cùng, hãy xác minh toàn bộ quy trình. Lên lịch một event đánh dấu rồi theo dõi đến khi nó biến mất:
sudo -u www-data /usr/local/bin/wp --path=/srv/www/example.com cron event schedule wp_cli_cron_check now
sudo -u www-data /usr/local/bin/wp --path=/srv/www/example.com cron event list --fields=hook,next_run_relative --format=csv | grep wp_cli_cron_checkChờ một interval, sau đó chạy lại command liệt kê. Hook đã biến mất vì event chạy một lần sẽ bị xóa khỏi queue khi được chạy. Không có plugin nào đăng ký callback cho tên hook đó, nên việc chạy hook không gây thêm thay đổi nào cho site. Nếu hook vẫn còn trong danh sách sau hai interval, queue chưa được chạy. Hai bước kiểm tra đầu tiên sẽ cho biết vấn đề nằm ở cron hay WP-CLI.
Không dùng wp cron test cho việc này. Command đó kiểm tra việc spawn do visitor trigger có hoạt động hay không, và sẽ báo lỗi khi DISABLE_WP_CRON là true. Trên server được cấu hình đúng, lỗi này là output dự kiến, không phải sự cố.
Phương án dùng systemd timer
Nếu các tác vụ định lịch khác trên máy đã chạy bằng systemd service và timer, hãy đưa WordPress vào cùng cơ chế đó. Mỗi lần chạy sẽ xuất hiện trong systemctl list-timers, còn output được ghi vào journal thay vì một file mà bạn phải tự rotate.
Tạo /etc/systemd/system/wp-cron-example.service:
[Unit]
Description=Run due WordPress cron events for example.com
[Service]
Type=oneshot
User=www-data
Group=www-data
ExecStart=/usr/local/bin/wp --path=/srv/www/example.com cron event run --due-nowSau đó tạo /etc/systemd/system/wp-cron-example.timer:
[Unit]
Description=Run WordPress cron for example.com every 5 minutes
[Timer]
OnCalendar=*:0/5
AccuracySec=30s
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now wp-cron-example.timer
systemctl list-timers wp-cron-example.timer
journalctl -u wp-cron-example.service -n 20systemd không chạy đồng thời 2 bản sao của cùng một service, nên phiên bản này không cần flock. Persistent=true giúp chạy bù một lần bị bỏ lỡ khi máy đang tắt, điều mà một entry trong crontab không làm được.
Chọn crontab hoặc timer. Nếu chạy cả 2 cho cùng một site, queue sẽ bị xử lý 2 lần. Các lần chạy trùng của job email hoặc job đơn hàng sẽ bị khách hàng nhìn thấy.
Vì sao cron job không được chạy dưới quyền root
Đây là cách đầu tiên trong 3 cách khiến thiết lập bị lỗi. Nếu đặt job vào crontab của root, WP-CLI sẽ dừng trước khi thực hiện bất kỳ thao tác nào:
Error: YIKES! It looks like you're running this as root.Queue không bao giờ chạy. Nếu bạn không redirect output, bạn cũng không thấy thông báo lỗi. Cách sửa nguy hiểm là thêm --allow-root, vì khi đó mọi file mà plugin ghi trong lần chạy đó đều thuộc về root. Request web tiếp theo chạy dưới quyền www-data, không thể ghi vào các thư mục đó, và website bắt đầu báo các lỗi như:
Unable to create directory wp-content/uploads/2026/08. Is its parent directory writable by the server?Sửa lại ownership, rồi chuyển job:
sudo chown -R www-data:www-data /srv/www/example.com/wp-contentCrontab của root và crontab của www-data là 2 file riêng biệt. Xóa dòng trong file này không ảnh hưởng đến file kia. Kiểm tra cả 2:
sudo crontab -u root -l
sudo crontab -u www-data -lVì sao cron báo wp: not found
Đây là lỗi thứ hai. Cron cấp cho các job của user một PATH rất ngắn, /usr/bin:/bin. WP-CLI được cài vào /usr/local/bin, nhưng thư mục này không nằm trong danh sách đó. Job khởi chạy, thất bại trong chưa đến một giây, và log chỉ có một dòng:
/bin/sh: 1: wp: not foundHãy tự kiểm tra environment của cron thay vì đoán. Thêm tạm một dòng:
*/5 * * * * env > /tmp/cron-env.txt 2>&1Sau một khoảng thời gian, đọc /tmp/cron-env.txt rồi xóa dòng đó. Giá trị PATH= trong file này chính xác là giá trị mà job của bạn nhận được.
Có 2 cách sửa. Dùng đường dẫn tuyệt đối /usr/local/bin/wp, như ở bước 3. Hoặc đặt PATH một lần ở đầu crontab, phía trên mọi dòng job:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binLỗi tương tự cũng xảy ra ở cấp thấp hơn. Phar wp bắt đầu bằng #!/usr/bin/env php, nên shell cũng phải tìm được php. Nếu PHP nằm ngoài /usr/bin, điều thường xảy ra với các bản build tùy chỉnh và bản build của control panel, bạn sẽ gặp:
/usr/bin/env: 'php': No such file or directoryTrong trường hợp đó, hãy gọi rõ interpreter, ví dụ /usr/local/bin/php /usr/local/bin/wp --path=/srv/www/example.com cron event run --due-now.
Vì sao khoảng thời gian một phút tái tạo vấn đề ban đầu
Đây là lỗi thứ ba. * * * * * có vẻ an toàn hơn 5 phút, nhưng trên một site bận, nó khiến bạn quay lại đúng tình trạng ban đầu. Nếu một lần chạy kéo dài hơn khoảng thời gian đã đặt, lần chạy tiếp theo sẽ bắt đầu khi lần đầu vẫn chưa kết thúc. Mười phút sau, có 10 tiến trình PHP, mỗi tiến trình giữ bộ nhớ riêng và một kết nối database riêng.
Kiểm tra trực tiếp xem các lần chạy có bị dồn hay không:
ps -eo etimes,user,args | grep '[c]ron event run'etimes là tuổi của tiến trình tính bằng giây. Một dòng là bình thường. Nhiều dòng có tuổi lớn hơn nhiều so với khoảng thời gian bạn đặt nghĩa là các lần chạy đang chồng lên nhau. Trên một VPS nhỏ, tình trạng này có thể kết thúc bằng lỗi MySQL Too many connections hoặc khiến kernel kill PHP để thu hồi bộ nhớ. Bạn có thể xác nhận việc đó bằng sudo dmesg -T | grep -i 'killed process'.
WP-CLI chạy trực tiếp các event callback thay vì gửi yêu cầu đến wp-cron.php, nên cơ chế lock 60 giây mà WordPress dùng để ngăn tạo tiến trình trùng lặp không áp dụng ở đây. flock -n trong mục ở bước 3 là cơ chế ngăn các lần chạy chồng lấp hiện nay. Một lần chạy bị bỏ qua sẽ thoát ngay và không ghi thông báo, theo thiết kế.
Chọn khoảng thời gian dựa trên lịch ngắn nhất mà bạn thực sự phụ thuộc vào, và đo thời gian chạy trước:
time sudo -u www-data /usr/local/bin/wp --path=/srv/www/example.com cron event run --due-now5 phút là giá trị mặc định hợp lý: bài viết được lên lịch lúc 09:00 sẽ được đăng trước 09:05. 15 phút phù hợp với site không có tác vụ nào cần xử lý đúng thời điểm. Khoảng thời gian một phút chỉ phù hợp với store và các plugin điều khiển bằng queue thực sự cần tần suất đó, và chỉ nên dùng sau khi bạn biết một lần chạy hoàn tất trong vài giây.
Nếu bạn không thể cài đặt WP-CLI
Một số host chặn các công cụ shell. Gửi HTTP request thông thường đến wp-cron.php sẽ chạy cùng queue, nhưng đi qua toàn bộ web stack:
*/5 * * * * /usr/bin/curl -sS --max-time 120 "https://example.com/wp-cron.php?doing_wp_cron" > /dev/nullNhững gì bạn phải chấp nhận:
- Lần chạy bị giới hạn bởi timeout của web server và PHP-FPM, nên job dài có thể bị dừng giữa chừng.
- Certificate phải hợp lệ, nếu không
curlsẽ dừng vớiSSL certificate problem. Vì vậy, hãy duy trì việc gia hạn bằng Certbot trên nginx. - Page cache không được cache
wp-cron.php, nếu không các request từ cron sẽ nhận response đã cache và không có gì chạy. - Bạn không nhận được output cho từng event, nên bằng chứng duy nhất cho thấy một job đã chạy là tác động mà nó tạo ra.
-sS giữ cho curl không hiển thị gì khi thành công nhưng vẫn in lỗi. Đây là hành vi phù hợp khi chạy trong cron job.
Những việc nào khác nên được đưa vào lịch của máy chủ
Sau khi cron của hệ thống quản lý queue của WordPress, hãy đặt các tác vụ định kỳ còn lại của máy chủ tại cùng một nơi để dễ theo dõi. Bản vá bảo mật của hệ điều hành nên do unattended upgrades quản lý, thay vì một dòng cron bạn phải tự duy trì. Việc cập nhật plugin và theme của WordPress là một quyết định khác: wp plugin update --all trong crontab có thể dễ dàng làm hỏng site đang chạy vào lúc 3am mà không ai theo dõi, vì vậy hãy chạy có chủ đích, hoặc thực hiện sau một bước staging và backup.
FAQ
Việc tắt WP-Cron có ngăn các bài viết đã lên lịch được đăng không?
Không, miễn là có tiến trình khác chạy queue. DISABLE_WP_CRON chỉ ngăn page load kích hoạt queue. Các event vẫn được lên lịch như trước. Bài viết đặt lịch lúc 09:00 sẽ được đăng trong lần chạy cron đầu tiên sau 09:00, nên với khoảng thời gian 5 phút, bài viết sẽ được đăng trước 09:05. Nếu đặt constant nhưng không thêm cron entry, bài viết sẽ nằm trong danh sách với trạng thái Missed schedule cho đến khi có tiến trình chạy queue.
Nên chạy cron job của WordPress bằng user nào?
Dùng user sở hữu các file mà PHP ghi vào. Trên Ubuntu cài mặc định, đó là www-data. Kiểm tra bằng stat -c '%U %G' /srv/www/example.com/wp-content/uploads rồi đối chiếu với dòng user = trong cấu hình PHP-FPM pool. Chạy job bằng root khiến WP-CLI dừng với lỗi YIKES. Ép chạy bằng --allow-root sẽ tạo các file thuộc sở hữu của root trong wp-content, sau đó web server không thể ghi vào đó.
System cron nên chạy WordPress cron bao lâu một lần?
Mỗi 5 phút phù hợp với hầu hết site. Chọn interval theo lịch ngắn nhất mà bạn thực sự cần, đồng thời để interval dài hơn đáng kể thời gian một lần chạy. Bạn có thể đo thời gian này bằng cách thêm time trước lệnh WP-CLI. Trên site bận, interval 1 phút có thể khiến các lần chạy chồng lên nhau nếu không có flock để ngăn việc đó.
Tại sao wp cron test fail sau khi tôi tắt WP-Cron?
Vì command đó kiểm tra cơ chế spawn do visitor kích hoạt, và sẽ báo lỗi khi DISABLE_WP_CRON được đặt thành true. Đây là kết quả đúng trên server được cấu hình như vậy. Hãy kiểm tra đường dẫn system cron: đọc /var/log/wp-cron/example.log, hoặc lên lịch một marker event bằng wp cron event schedule rồi xác nhận marker đó đã biến mất khỏi wp cron event list sau lần chạy tiếp theo.
Tôi có cần WP-CLI không, hay dùng curl đến wp-cron.php là đủ?
Curl vẫn hoạt động và là lựa chọn phù hợp khi bạn không thể cài WP-CLI. Cách này chậm hơn vì nó load WordPress thông qua web server và bị giới hạn bởi request timeout. WP-CLI chạy các event trong một tiến trình PHP trên command line, không bị web timeout, đồng thời in một dòng cho mỗi event kèm thời lượng chạy. Vì vậy, log cho biết chính xác event nào đã chạy và mất bao lâu.