Botble có thể báo cron job đã được tạo trên Hostinger nhưng email, dọn dữ liệu hoặc tác vụ định kỳ vẫn im re. Một nguyên nhân khá đặc trưng nằm ngay trong ô command: chuỗi >> /dev/null 2>&1 vốn bình thường trên cron chạy qua shell lại bị Hostinger chuyển thẳng thành argument cho Artisan.

Khi đó Laravel dừng ở lớp đầu tiên với lỗi No arguments expected for "schedule:run" command, got ">>". Bỏ phần redirect là cần thiết, nhưng chưa đủ để kết luận scheduler đã khỏe. Cách kiểm tra chắc hơn là đi qua ba lớp: command có chạy đúng ứng dụng, Laravel có nhìn thấy task, và task có tạo ra kết quả quan sát được hay không.

Vì sao dòng cron đúng ở VPS lại sai trên Hostinger?

Tài liệu cronjob của Botble đưa ra lệnh phổ biến cho server có shell:

* * * * * /usr/local/bin/php /path-to-your-project/artisan schedule:run >> /dev/null 2>&1

Hai dấu >> và 2>&1 là cú pháp chuyển hướng output của shell. Vấn đề là trường cron của Hostinger không chạy command qua shell theo cách đó. Phần redirect bị đưa nguyên vào danh sách argument của schedule:run, nên Artisan xem >> như một đối số không hợp lệ và thoát trước khi xét bất kỳ lịch nào.

Botble vì thế hướng dẫn nhập command không có redirect trên Hostinger:

/usr/local/bin/php /path-to-your-project/artisan schedule:run

Hostinger tự lưu output và cho xem bằng nút View Output, nên không cần ném output vào /dev/null. Đường dẫn PHP và đường dẫn artisan ở trên chỉ là ví dụ; hãy dùng giá trị do control panel hoặc cấu hình hosting của site cung cấp. Không công khai username, home path hay hostname thật khi gửi ảnh lỗi cho người khác.

Trong fixture cô lập trên Windows 11, PHP 8.4.24 và Laravel 13.38.0, tiến trình được gọi trực tiếp với ba argument literal >>, /dev/null, 2>&1 đã thoát mã 1 và trả đúng thông báo nêu trên. Cùng ứng dụng đó, schedule:run -v không có redirect thoát mã 0 và chạy heartbeat. Đây là phép kiểm cho cơ chế truyền argument, không phải mô phỏng giao diện Hostinger hay test trên tài khoản production.

Sơ đồ ba lớp command, schedule và outcome với kết quả fixture Laravel 13.38
Fixture cô lập: argument literal >> làm Artisan thoát 1; command đúng chạy heartbeat, còn task 23:59 chưa tạo marker.

Lớp 1: command có chạy đúng ứng dụng không?

Đầu tiên, chạy một phép thử vô hại trong staging hoặc terminal mà hosting cung cấp. Mục tiêu là xác nhận binary PHP có tồn tại, file artisan thuộc đúng site và ứng dụng boot được. Hai lệnh ngắn sau thường đủ để tách lỗi môi trường khỏi lỗi lịch:

/duong-dan/php /duong-dan/site/artisan --version
/duong-dan/php /duong-dan/site/artisan schedule:run -v

Nếu lệnh đầu đã thất bại, chưa cần bàn tới cron expression. Kiểm lại phiên bản PHP mà website dùng, quyền đọc file, working directory và đường dẫn tuyệt đối. Không giải quyết bằng chmod 777; quyền quá rộng vừa không sửa được sai binary vừa tạo thêm rủi ro.

Nếu --version chạy nhưng schedule:run lỗi, đọc output trước khi xóa cache hay thay đổi production. Fixture của bài này cũng cho một negative control hữu ích: Laravel 13 mới dùng cache để theo dõi trạng thái pause. Khi app cấu hình cache database nhưng database/cache table chưa sẵn sàng, scheduler dừng vì lỗi cache trước khi chạy task. Chuyển fixture sang file cache đã chuẩn bị sẵn làm schedule:list và schedule:run hoạt động. Trên site thật, giải pháp là sửa đúng cache backend hoặc migration, không đổi store tùy tiện chỉ để dấu lỗi.

View Output trên Hostinger nên giữ lại ít nhất cho giai đoạn chẩn đoán. Một dòng “cron đã lưu” chỉ chứng minh control panel nhận cấu hình; exit code và output mới cho biết command có đến được Artisan hay không.

Lớp 2: Laravel có nhìn thấy task không?

Khi command đã boot được app, chạy php artisan schedule:list. Theo tài liệu Task Scheduling của Laravel 13, lệnh này hiển thị các scheduled task và thời điểm chạy kế tiếp. Nếu task cần tìm không nằm trong danh sách, cron bên ngoài có chạy mỗi phút cũng không thể gọi một lịch mà ứng dụng chưa đăng ký.

Fixture đăng ký hai command: heartbeat chạy mỗi phút và một task đặt lúc 23:59. schedule:list hiện cả hai cùng Next Due. Lượt schedule:run tại 10:02 UTC+7 chỉ chạy heartbeat; file marker của task 23:59 không xuất hiện. Negative control này nhắc một điều đơn giản: task chưa đến hạn không phải bằng chứng cron hỏng.

Cần đối chiếu timezone của ứng dụng, timezone gắn riêng cho task và giờ server. Laravel cho phép khai báo timezone() hoặc schedule_timezone; các lịch theo múi giờ có daylight saving còn có thể chạy hai lần hoặc bỏ một lượt vào ngày chuyển giờ. Với website dùng UTC+7, hãy ghi rõ mốc giờ đang đọc thuộc timezone nào thay vì chỉ chụp một con số.

Hai trạng thái khác cũng dễ làm task biến mất khỏi kết quả chạy. Mặc định, scheduled task không chạy khi ứng dụng ở maintenance mode, trừ task có evenInMaintenanceMode(). Laravel 13 còn có schedule:pause và schedule:continue; khi scheduler bị pause, không task nào chạy nếu chưa được đánh dấu ngoại lệ. Đây là trạng thái ứng dụng, không phải lỗi Hostinger.

Nếu site chạy nhiều node và task dùng lock phân tán, xem thêm bài chạy toàn bộ scheduler trên một server. Bài hiện tại chỉ tập trung vào parser, registry và outcome trên shared hosting; không lặp lại chiến lược multi-server.

Lớp 3: outcome nào chứng minh cron đã chạy?

schedule:run thoát mã 0 vẫn chưa chứng minh email đã gửi, queue đã xử lý hoặc backup đã hoàn tất. Scheduler có thể chỉ dispatch job vào queue; worker lại đang dừng. Task có thể chạy nhưng provider bên ngoài từ chối request. Vì thế cần một outcome nhỏ, độc lập và quan sát được.

Trong fixture, command heartbeat ghi file JSON chứa marker cố định và timestamp UTC. Sau lượt dương, marker xuất hiện với thời điểm 2026-10-23T03:02:03Z; task tương lai không tạo file riêng. Với production, có thể dùng một record heartbeat trong database test, log channel riêng hoặc timestamp trong trang quản trị nội bộ. Dữ liệu chỉ nên đủ để trả lời “task gần nhất đã đi qua scheduler lúc nào”, không chứa token, email khách hàng hay path nhạy cảm.

Nếu dùng file hoặc log, đặt retention và quyền đọc phù hợp. Nếu dùng database, tránh tăng một hàng vô hạn mỗi phút; cập nhật một record hoặc gom theo cửa sổ thời gian thường dễ quản lý hơn. Laravel cũng hỗ trợ sendOutputTo(), appendOutputTo() và hook onFailure() cho command/exec, nhưng output của task không thay thế bằng chứng ở queue hoặc dịch vụ ngoài.

Ví dụ một task scheduler dispatch mail job thành công nhưng queue worker không chạy. Lớp schedule sẽ xanh, còn outcome nghiệp vụ vẫn đỏ. Chỗ này nên tách hai heartbeat: một cho scheduler đã dispatch, một cho worker đã xử lý. Nếu mail provider là lớp cuối, cần thêm delivery event hoặc log phản hồi riêng; đừng gom tất cả thành một dấu “cron oke”.

Khi chạy thủ công được nhưng cron vẫn im

Khi manual command pass, chuyển sang chẩn đoán theo bằng chứng thay vì đổi hàng loạt cấu hình. Ma trận dưới đây rút gọn các nhánh thường gặp:

Ma trận chẩn đoán cron theo bằng chứng manual command, schedule list, thời điểm due và marker
Mỗi triệu chứng dẫn tới một lớp kiểm tra khác; dòng cron đã lưu chưa phải bằng chứng task hoàn tất.
Quan sátKết luận gần nhấtKiểm tiếp
Manual command lỗiSai binary, path, parser hoặc app không bootView Output, PHP path, working directory
Manual pass, schedule:list thiếu taskTask chưa được đăng ký trong environment nàyRoute console, bootstrap, condition theo environment
Danh sách có task nhưng chưa dueChưa đủ bằng chứng cron lỗiNext Due, timezone, maintenance/pause
Task được gọi nhưng outcome thiếuLỗi trong task hoặc lớp downstreamLock, queue worker, provider, quyền ghi
Marker đổi đúng chu kỳBa lớp scheduler đã thôngGiữ heartbeat và kiểm nghiệp vụ riêng

withoutOverlapping() là một nhánh đáng kiểm khi task từng bị dừng giữa chừng. Laravel dùng cache lock cho tính năng này và mặc định lock có thể tồn tại tới 24 giờ; chỉ dùng schedule:clear-cache khi đã xác định lock scheduler bị kẹt. Không clear toàn bộ cache của site theo thói quen, vì việc đó có thể che triệu chứng hoặc làm tăng tải.

Path tương đối cũng dễ khác giữa terminal và cron user. Dùng đường dẫn tuyệt đối cho binary và artisan, còn file task nên đi qua helper của Laravel như storage_path(). Kiểm user chạy cron có quyền với đúng thư mục cần ghi, nhưng không mở quyền toàn bộ project.

Nếu vấn đề xuất hiện sau deploy, bài Botble vẫn hiện giao diện cũ sau deploy có checklist tách cache, asset và quyền file. Với task backup, chỉ nên gắn lịch sau khi đã kiểm restore; phần đó được trình bày riêng trong bài kiểm tra restore Botble. Một cron chạy đều không biến backup chưa thử thành bản sao đáng tin.

Cấu hình tối thiểu nên giữ lại

Trên Hostinger, giữ command không có shell redirection và lịch gọi mỗi phút theo tài liệu Botble. Sau mỗi deploy hoặc thay đổi hosting, chạy lại schedule:list, một manual smoke test vô hại, chờ một chu kỳ heartbeat và đọc View Output. Ghi timestamp lần kiểm gần nhất để lần sau biết hệ thống hỏng từ bao giờ.

Trên VPS hoặc môi trường cron thực sự chạy qua shell, redirect vẫn có thể hợp lệ. Dù vậy, quy trình ba lớp vẫn giữ nguyên: command đúng chưa chắc task đã đăng ký, task đăng ký chưa chắc đến hạn, và scheduler đã gọi task chưa chắc nghiệp vụ downstream thành công.

Chốt lại

Nếu Botble cron không chạy trên Hostinger và output nhắc tới >>, hãy bỏ phần >> /dev/null 2>&1 khỏi trường command rồi xem View Output. Sau đó đừng dừng ở việc command thoát 0: dùng schedule:list để xác nhận registry và next due, cuối cùng theo dõi một marker hoặc heartbeat có timestamp.

Ba bằng chứng này đủ để khoanh vùng lỗi mà không cần chmod 777, clear cache rộng hay chạy một task phá dữ liệu trên production. Khi scheduler đã thông mà email, queue hoặc backup vẫn hỏng, chuyển sang kiểm đúng lớp downstream thay vì tiếp tục sửa cron.