Tắt WP-Cron, chạy WordPress bằng system cron
WP-Cron chỉ chạy khi có request nên site vắng bị trễ, site đông bị dồn job. Tắt bằng DISABLE_WP_CRON, dùng WP-CLI và xác nhận cron đã 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 và 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ập lịch. Nếu có job đến hạn, WordPress gửi một HTTP request thứ hai đến chính nó tại /wp-cron.php để thực hiện công việc. 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 được, bất kể trong phút đó website có một nghìn lượt truy cập hay không có lượt 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 phần mà hai dòng đó không thể hiện. Job phải chạy dưới user nào, cách xác nhận các scheduled event thực sự đã chạy và ba trường hợp khiến cấu hình thất bại mà không in ra bất kỳ thông báo nào trên website.
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 thế bằng path và user của bạn ở mọi vị trí.
Lượt truy cập nào kích hoạt cron và gây tốn tài nguyên trên site có nhiều traffic
Mỗi request không có trong cache đều phải thực hiện bước kiểm tra này. WordPress tải option cron, so sánh timestamp, và khi có tác vụ đến hạn, nó 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ả. Nhưng 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ể giữ 1/5 năng lực xử lý PHP trong suốt thời gian job chạy. Job này nhiều khả năng được kích hoạt vào phút có nhiều traffic 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 truy cập đồ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 tác vụ 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 mức độ ảnh hưởng. Mỗi loopback request 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 đó. Nếu số lượng lên đến hàng nghìn mỗi ngày thì đó là một chi phí thực tế. Bạn nên đo con số này trên chính server 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 các thay đổi khác.
Caching làm thay đổi tình hình. 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 các request đó, nên bước kiểm tra cron cũng không xảy ra. Một site có nhiều traffic và được cache tốt sẽ bắt đầu hoạt động giống site ít traffic ở phần dưới.
Cron bị kích hoạt bởi visitor nào trên một site ít truy cập
Không có visitor 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 scheduled job tương ứng 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 đều giống nhau. Một bài viết được lên lịch 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 page. Plugin backup bỏ qua lần chạy ban đêm. Việc kiểm tra update bị chậm, nên dashboard không hiển thị update nào trong khi một 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 trễ.
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 bao giờ đượ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 constant này bên trên dòng có nội dung /* That's all, stop editing! Happy publishing. */, vì dòng ngay bên dưới comment đó yêu cầu wp-settings.php, còn wp-settings.php là nơi WordPress gắn việc kiểm tra cron 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í bạn mong muốn:
grep -n "DISABLE_WP_CRON\|stop editing" /srv/www/example.com/wp-config.phpConstant này không ngăn các event được schedule. 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.
Constant này 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. Chặn URL này trong cấu hình web server là tùy chọn. Nếu bạn chặn, cơ chế fallback curl ở gần cuối hướng dẫn này cũng sẽ không hoạt động.
Bước 2: cài đặt WP-CLI
WP-CLI là công cụ command line chính thức cho WordPress. Công cụ này cần PHP binary dành cho command line. Đâ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 mà 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 ra đường dẫn PHP binary, phiên bản PHP và phiên bản WP-CLI. Nếu cả 3 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 mức này khá xa. 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 từ chối khởi động:
Error: YIKES! It looks like you're running this as root.Nó đề xuất --allow-root. Không dùng cách đó ở đây. Lý do nằm trong failure mode đầu tiên bên dưới.
Cũng 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 command thẳng cho sudo -u sẽ bỏ qua login shell, nên command chạy bình thường.
Bây giờ xác nhận WordPress đã nhận 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 về undefined constant nghĩa là chưa chạy đến dòng define(). Thông thường nguyên nhân là dòng đó nằm bên dưới lệnh require.
Bước 3: thêm cron entry bằng đúng user
User đúng là user sở hữu các file mà PHP ghi vào. Kiểm tra cả hai đầu:
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 Ubuntu cài mặc định, cả hai đề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, đây thường là kết quả của mô hình per-site trên 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ó quyền ghi:
sudo install -d -o www-data -g www-data -m 750 /var/log/wp-cronSử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>&1Phân tích từng phần. */5 chạy lệnh mỗi 5 phút. flock -n lấy 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. Phần redirect ghi output bình thường và error 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 của job, 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) rồi loại bỏ output. Ghi 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úng không khởi chạy 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. 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 đã được chạy thật
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 ít tốn công nhất đến bước có thể xác nhận nguyên nhâ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 hợp lệ sẽ có dạng sau, trong đó timestamp và hostname ở đầu đã được lược bỏ:
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. Nó 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 chính 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 chắc đang có event chờ xử lý.
Cuối cùng, hãy xác minh toàn bộ chuỗi hoạt động. Schedule một marker event rồi theo dõi cho đế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, rồi chạy lại command list. Hook đã biến mất vì một one-off event được xóa khỏi queue khi chạy. Không có plugin nào đăng ký callback cho tên hook đó, nên việc chạy hook không làm gì khác với site. Nếu hook vẫn được liệt kê sau hai interval, queue không đượ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 visitor-triggered spawning có hoạt động 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 được mong đợi, không phải sự cố.
Phương án dùng systemd timer
Nếu các tác vụ định kỳ khác trên máy chủ đã 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 chủ tắt, điều mà một mục 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. Việc chạy trùng các job gửi email hoặc xử lý đơn hàng sẽ lộ rõ với khách hàng.
Vì sao cron job không được chạy bằng root
Đây là cách đầu tiên trong ba nguyên nhân khiến thiết lập bị lỗi. Nếu thêm 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 sẽ 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 bằng www-data, không thể ghi vào các thư mục đó, và site 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, sau đó 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à hai 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ả hai:
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, fail trong chưa đến một giây, và log chỉ có một dòng:
/bin/sh: 1: wp: not foundHãy tự xem environment của cron thay vì đoán. Thêm một dòng tạm thời:
*/5 * * * * env > /tmp/cron-env.txt 2>&1Đọc /tmp/cron-env.txt sau một khoảng thời gian, 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ư trong bước 3. Hoặc đặt PATH một lần ở đầu crontab, bên trên mọi dòng job:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binCùng lỗi này còn xuất hiện ở cấp thấp hơn. Phar wp bắt đầu bằng #!/usr/bin/env php, vì vậy 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 lỗi:
/usr/bin/env: 'php': No such file or directoryTrong trường hợp đó, gọi interpreter một cách rõ ràng, ví dụ /usr/local/bin/php /usr/local/bin/wp --path=/srv/www/example.com cron event run --due-now.
Vì sao chu kỳ một phút lại tạo ra đúng vấn đề ban đầu
Đây là lỗi lần thứ ba. * * * * * có vẻ an toàn hơn năm phút, nhưng trên site bận, nó lại đưa bạn về đúng tình trạng ban đầu. Nếu một lần chạy mất nhiều thời gian hơn chu kỳ, lần chạy tiếp theo sẽ bắt đầu khi lần trước vẫn chưa kết thúc. Mười phút sau, có mười tiến trình PHP, mỗi tiến trình giữ vùng nhớ riêng và một kết nối database riêng.
Kiểm tra trực tiếp tình trạng chạy dồn:
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 cao hơn nhiều so với chu kỳ của bạn cho thấy các lần chạy đang bị chồng lên nhau. Trên một VPS nhỏ, tình trạng này có thể dẫn đến lỗi MySQL Too many connections hoặc kernel phải dừng 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ế khóa 60 giây mà WordPress dùng để ngăn việc spawn trùng không áp dụng ở đây. flock -n trong mục ở bước 3 hiện là cơ chế ngăn các lần chạy chồng lên nhau. Lần chạy bị bỏ qua sẽ thoát ngay và không ghi thông báo, theo đúng thiết kế. Bạn phải xử lý việc chồng lấn trong crontab, thay vì trông chờ host kernel tự xử lý: ngay cả cơ chế sắp xếp task có xét đến cache được thêm vào Linux kernel 7.2 cũng chỉ quyết định tiến trình chạy trên core nào, chứ không quyết định bạn đã khởi chạy bao nhiêu tiến trình.
Chọn chu kỳ dựa trên lịch ngắn nhất mà bạn thực sự phụ thuộc vào, rồi đ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-nowNăm phút là giá trị mặc định hợp lý: một bài viết được lên lịch lúc 09:00 sẽ được publish muộn nhất vào 09:05. Mười lăm phút phù hợp với site không có tác vụ nào cần xử lý theo thời gian thực. 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 chu kỳ này, 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 trực tiếp đến wp-cron.php vẫn 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 chạy lâu 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ì được 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 in gì khi thành công nhưng vẫn in error. Đây là hành vi phù hợp khi chạy trong cron job.
Những việc nào khác nên đưa vào lịch của server
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 server ở 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 xử 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 một site đang chạy lúc 3 giờ sáng mà không ai theo dõi. Hãy thực hiện việc này một cách có chủ đích, hoặc chạy sau một bước staging và backup.
FAQ
Vô hiệu hóa 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 được đặt lịch đăng lúc 09:00 sẽ được đăng trong lần cron đầu tiên sau 09:00, nên interval năm phút sẽ đăng bài chậm nhất lúc 09:05. Nếu bạn đặt constant này 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?
Hãy 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. Nếu ép chạy bằng --allow-root, các file bên trong wp-content sẽ thuộc sở hữu của root và web server không thể ghi vào đó sau này.
system cron nên chạy WordPress cron bao lâu một lần?
Năm phút một lần 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 giữ interval dài hơn đáng kể so với thời gian chạy một lần. Bạn có thể đo thời gian này bằng cách đặt time trước lệnh WP-CLI. Trên site bận, interval một phút có thể khiến các lần chạy chồng lên nhau, trừ khi có flock ngăn việc đó.
Tại sao wp cron test bị lỗi sau khi tôi vô hiệu hóa WP-Cron?
Vì command đó kiểm tra cơ chế khởi chạy do visitor kích hoạt và 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. Thay vào đó, 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 curl đến wp-cron.php là đủ?
curl 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ì WordPress được load 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 PHP process 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 gian chạy. Nhờ đó, log cho biết chính xác event nào đã chạy và mất bao lâu.