
August 21, 2026
10 min read
By Kokil Thapa | Last reviewed: September 2026
Choosing the wrong Kubernetes batch primitive wastes cluster resources and hides failures. The k8s job vs cronjob decision is straightforward once you know what each controller owns. A Job runs finite work once; a CronJob creates Jobs on a recurring schedule. If you ship Laravel applications in production, that split maps directly to migrations, nightly reports, and the minute-by-minute scheduler. This guide covers YAML you can deploy today, failure handling, and monitoring patterns I use on real clusters.
schedule:run, backups, and cleanup scripts.What Is the Difference Between a k8s Job and a CronJob?
A Kubernetes Job is a batch controller. It creates pods and tracks them until a target number complete successfully. The Job stops when that count is met or retries are exhausted. Use it for work that should run once: database migrations after deploy, cache warming, or a CSV import triggered from CI.
A CronJob sits one level higher. It does not run your container directly. At each tick of its cron schedule, the CronJob controller creates a fresh Job object. That Job then creates pods. Every scheduled run gets its own Job name, status, and logs. Debugging a failed Tuesday 2 AM run is far easier than parsing one long-lived process log.
On a traditional VPS, one crontab line runs php artisan schedule:run every minute. In Kubernetes, the same pattern becomes a CronJob that starts a fresh pod each minute. If one run hangs, it does not block the next. That isolation is why teams evaluating cloud hosting for batch workloads often pick managed Kubernetes over single-server PaaS. For background on cluster fundamentals, see the guide to deploying your first app to Kubernetes.
The official Kubernetes Job documentation defines completion semantics. The CronJob documentation covers schedule fields and concurrency policies. Read both before copying YAML from a tutorial.
How Do You Configure a Kubernetes CronJob for Laravel Scheduler?
Laravel expects schedule:run every minute. Individual tasks define their own frequency inside the scheduler. A naive CronJob without concurrency controls will stack overlapping pods during slow tasks. That drains node memory fast.
Production YAML baseline
This manifest targets Laravel 13.x on PHP 8.3 or higher. It includes resource limits, timezone support, and strict overlap prevention. Omitting limits is a common cause of node instability during heavy batch cycles.
apiVersion: batch/v1
kind: CronJob
metadata:
name: laravel-scheduler
namespace: production
spec:
schedule: "* * * * *"
timeZone: "Asia/Kathmandu"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
startingDeadlineSeconds: 60
jobTemplate:
spec:
activeDeadlineSeconds: 55
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: scheduler
image: registry.example.com/app:2026.09.14
command: ["php", "artisan", "schedule:run"]
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
envFrom:
- secretRef:
name: laravel-secrets
- configMapRef:
name: laravel-config
serviceAccountName: app-worker
automountServiceAccountToken: false Fields that matter most
- concurrencyPolicy: Forbid — Blocks a new Job while the previous one runs. Mandatory for minute-level schedulers.
- activeDeadlineSeconds: 55 — Kills a run before the next minute starts. Prevents overlap even with clock skew.
- backoffLimit: 0 —
schedule:runexits 0 even when no tasks fire. Retrying empty runs wastes quota. - startingDeadlineSeconds: 60 — Skips missed windows instead of firing catch-up Jobs in rapid succession.
- timeZone: Asia/Kathmandu — Available since Kubernetes 1.27. Without it, schedules default to UTC. Billing and notification jobs drift by 5 hours 45 minutes for Nepal-based apps.
I've deployed this pattern across sister sites sharing a Deployer 7 and GitLab CI pipeline. Timezone mismatches and overlapping processes disappear as a class of bugs. For the VPS equivalent before you migrate, read Laravel scheduled tasks in production and cron versus queue workers. The Laravel scheduling docs define what runs inside each minute.
One-off migrations belong in a Job, not a CronJob. Trigger them from your deploy pipeline after the image rolls out. Pair this with zero-downtime Laravel deployment and GitLab CI for Laravel so migrations run exactly once per release.
How Do You Handle Failures and Retries in Kubernetes Jobs?
Kubernetes retries at the pod level, not the application level. The backoffLimit field sets how many pod restarts occur before the Job is marked Failed. Each retry waits with exponential backoff: 10 seconds, then 20, then 40, capped at six minutes.
Pod exit code 0 means success. Any non-zero code triggers a retry until the limit is hit. Your application must return meaningful exit codes. A catch block that logs an error but still exits 0 will look green in Kubernetes while data is wrong.
Set backoffLimit: 0 for scheduler CronJobs. For dedicated batch Jobs—importing 50,000 product rows for an eCommerce client—use backoffLimit: 2 or 3. Network timeouts and temporary database locks deserve retries. Validation errors and missing files should fail immediately with a clear log line.
On legal-tech portals I maintain, document processing distinguishes retryable exceptions (HTTP 429, connection timeout) from fatal ones (invalid PDF, missing signature). Only retryable errors bubble up. That keeps Kubernetes retries meaningful. For API batch patterns, see Laravel API best practices on idempotency keys. Validate log output with a JSON formatter during development.
When pods crash-loop, the troubleshooting steps in debugging CrashLoopBackOff apply to failed Jobs too. Set resource requests and limits before blaming the application.
When Should You Use Kubernetes Jobs Versus Laravel Queues?
The k8s job vs cronjob question is only half the picture. The bigger choice is Kubernetes batch primitives versus Laravel queues. They solve different problems. Conflating them creates fragile systems.
| Criteria | Kubernetes Job / CronJob | Laravel Queue (Redis / SQS) |
|---|---|---|
| Trigger | Time-based or manual (kubectl, CI hook) | Event-driven: user action, webhook, API call |
| Latency | Minutes to hours acceptable | Seconds to minutes expected |
| State | Stateless per run; no shared memory | Redis supports rate limits and deduplication |
| Scaling | parallelism field; vertical via CPU/memory limits | Independent worker count per queue |
| Failure handling | Pod restart; coarse-grained Job retry | Per-job retry, delay, dead-letter queue |
| Best for | Nightly reports, DB maintenance, scheduled syncs | Email, image processing, payment callbacks |
| Cost | Zero between runs; bursty compute | Baseline cost for always-on workers |
Most production Laravel systems use both. CronJobs handle the temporal dimension: every night at 2 AM NPT, generate invoice summaries. Queues handle the reactive dimension: when a user uploads a document, validate and store it. A CronJob can dispatch queue jobs—it runs php artisan invoices:generate-nightly, which enqueues hundreds of ProcessInvoice jobs for parallel workers.
Read background jobs versus cron jobs for the design framing. For queue setup, see Laravel queues with Redis in production. On a booking platform like Adventure Third Pole Trek, scheduled inventory snapshots run as CronJobs. Real-time booking confirmations flow through queues.
Containerize the app first if you have not already. The guide to containerizing a Laravel app covers the image your Job or CronJob will reference.
How Do You Monitor and Debug Kubernetes CronJobs in Production?
CronJobs fail quietly. A missed schedule leaves no application log. The LAST SCHEDULE column in kubectl get cronjobs only shows when a Job was created—not whether it succeeded or produced correct output.
Build three complementary signals:
- Metric alerts —
kube-state-metricsexposeskube_cronjob_status_last_successful_time. Alert when that timestamp exceeds twice your expected interval. This catches controller failures, image pull errors, and quota exhaustion. - Application heartbeats — POST to an external dead man's switch after every successful run. A pod can exit 0 while skipping tasks due to a missing env var. Only an app-level ping catches that.
- Structured logs — Inject Job name and UID via the Downward API. Include both in every log line. Filter by exact Job instance instead of interleaved pod output.
For the full stack, read Prometheus and Grafana monitoring setup. Teams running multiple client sites on one cluster need one dashboard with last-success timestamps per namespace. That prevents the out-of-sight failure mode that plagues distributed batch systems. If you need hands-on cluster help, see Linux system administration services.
Key Takeaways
- A Job runs finite work once; a CronJob creates Jobs on a cron schedule—know which side of the k8s job vs cronjob split you need.
- Laravel schedulers need
concurrencyPolicy: Forbid,activeDeadlineSecondsunder 60, andtimeZone: Asia/Kathmandufor NPT. - Set
backoffLimit: 0for schedulers; use 2–3 for batch imports with transient failure modes. - Use CronJobs for time-based work and Laravel queues for event-driven work—combine them when a nightly command dispatches queue jobs.
- Monitor with Prometheus metrics, application heartbeats, and structured logs—never rely on
kubectlalone. - Trigger one-off migrations as Jobs from CI, not as CronJobs, to guarantee exactly-once execution per deploy.
People Also Ask
What is the difference between a Kubernetes Job and a CronJob?
A Job creates pods that run until completion for a single execution. A CronJob wraps that behaviour in a schedule and spawns a new Job at each interval. Think of CronJob as the timer and Job as the worker it hires each time.
Can you run a Kubernetes CronJob only once?
Not directly. CronJobs are designed for recurrence. For one-time work, create a Job resource instead. You can manually trigger a CronJob with kubectl create job --from=cronjob/name, but a plain Job is cleaner for migrations and imports.
How do you debug a failed Kubernetes CronJob?
Start with kubectl get jobs to find the child Job, then kubectl logs job/name for application output. Check kubectl describe cronjob for schedule misses and startingDeadlineSeconds skips. Cross-reference Prometheus metrics for silent failures where pods never started.
Should Laravel schedule:run use a CronJob or a long-running Deployment?
Use a CronJob that runs schedule:run every minute with concurrencyPolicy: Forbid. A long-running Deployment with an internal sleep loop hides failures, complicates deploys, and wastes resources between ticks. The CronJob model matches Laravel's intended one-minute invocation pattern.
Deploy Batch Workloads With Confidence
The k8s job vs cronjob choice is simple once you map workloads to triggers. Jobs for one-off and CI-driven work. CronJobs for recurring schedules with strict concurrency and timezone settings. Queues for everything that must react in seconds. Audit your crontab and scheduler definitions before migrating. Test overlap, timeout, and failure paths in staging. Instrument metrics and heartbeats before production data depends on the schedule.
Need help migrating Laravel schedulers from a VPS to Kubernetes, or designing batch architecture for a growing app? Contact us to discuss your workload. You can also reach out directly about your cluster setup. Getting batch primitives right upfront saves months of debugging silent failures and resource leaks.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

