- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Laravel Queues in Depth: Chains, Batches and Unique Jobs
About Post
Dispatching one job is the easy part. SendWelcomeEmail::dispatch($user), a worker picks it up, done.
Real background work is rarely one job. It's "generate the contract PDF, then send it for signature, then tell the leasing team". It's "build a statement for every tenant this month and tell me when they're all done". It's "the user clicked Recalculate four times, please don't run it four times". And it's "the SMS provider only accepts so many messages a minute".
Laravel has a tool for each of those, and they're some of the most underused parts of the framework. Let's go through them with the gotchas that only show up once real traffic arrives.
Chains: steps that must happen in order
A chain runs jobs one after another. The next job only starts when the previous one succeeded. If any job fails (after its retries), the rest of the chain is abandoned.
use Illuminate\Support\Facades\Bus;
Bus::chain([
new GenerateContractPdf($contract),
new SendForSignature($contract),
new NotifyLeasingTeam($contract),
])->catch(function (Throwable $e) {
report($e);
})->onQueue('contracts')->dispatch();
Why not just call all three steps inside one big job? Because each step can now retry on its own. If the e-signing provider has a bad minute, only SendForSignature retries. The PDF isn't generated again, and the team isn't notified twice.
The thing that surprises people: jobs in a chain don't pass return values to each other. There's no "output of step one becomes input of step two". Each job reads what it needs from the database. So GenerateContractPdf stores the file path on the contract, and SendForSignature reads it from there. That feels clumsy at first, but it's actually what you want: the state survives a crash, and you can see exactly how far a chain got.
Batches: many jobs, one finish line
A batch is the opposite shape. The jobs are independent and can run in parallel across workers, but you care about the group: how far along it is, and what happens when it's finished.
Batches need a table to track progress, so run this once:
php artisan make:queue-batches-table
php artisan migrate
Then dispatch the batch with the callbacks you need:
use Illuminate\Bus\Batch;
$batch = Bus::batch(
$tenants->map(fn (Tenant $tenant) => new GenerateStatement($tenant, $month))->all()
)->then(function (Batch $batch) {
// every job succeeded
})->catch(function (Batch $batch, Throwable $e) {
// the first failure happened
})->finally(function (Batch $batch) {
// every job has run, successfully or not
})->name("Statements {$month}")->allowFailures()->dispatch();
return $batch->id; // keep it to show progress later
Later, Bus::findBatch($id) gives you progress(), processedJobs() and failedJobs, which is all you need for a progress bar in an admin screen.
Each job in the batch uses the Batchable trait and should check whether the batch was cancelled:
class GenerateStatement implements ShouldQueue
{
use Batchable, Queueable;
public function __construct(public Tenant $tenant, public string $month) {}
public function handle(): void
{
if ($this->batch()?->cancelled()) {
return;
}
// build and store the statement PDF
}
}
Three batch gotchas
- Without
allowFailures(), one failure cancels the batch. But the remaining jobs are already on the queue, and workers will still pick them up. That cancelled check at the top ofhandle()is the only thing that stops them doing the work. Forget it and "cancelled" means nothing. thenonly runs if every job succeeded, even withallowFailures(). If you want "the run is over, send me a summary either way", that'sfinally.- Callbacks are serialized and run later on a worker. Don't use
$thisinside them, and don't capture big objects. Pass IDs and look things up.
One more practical point: the batch table grows forever unless you prune it. Schedule queue:prune-batches and forget about it.
Unique jobs: don't queue the same work twice
Users double-click. Observers fire more often than you think. A scheduled command and a button can trigger the same recalculation at the same moment. If the job is expensive and the result would be identical, you only want one in the queue.
use Illuminate\Contracts\Queue\ShouldBeUnique;
class RecalculateTenantBalance implements ShouldQueue, ShouldBeUnique
{
use Queueable;
public $uniqueFor = 300; // seconds; the lock expires even if a worker dies
public function __construct(public Tenant $tenant) {}
public function uniqueId(): string
{
return (string) $this->tenant->id;
}
}
On dispatch, Laravel takes a cache lock keyed by the job class and uniqueId(). While the lock exists, dispatching the same job again quietly does nothing. The lock is released when the job finishes or runs out of retries.
The details worth knowing:
- It needs a cache driver that supports atomic locks. Redis, Memcached, database, DynamoDB and file all do. If your queue workers run on several servers, use a shared cache, not
file. - Always set
$uniqueFor. If a worker is killed mid-job, the lock otherwise lingers and the job silently refuses to be dispatched. That's a confusing bug to chase. - Consider
ShouldBeUniqueUntilProcessingwhen new changes can arrive while the job is running. It releases the lock just beforehandle()starts, so a change that lands mid-run can queue one fresh recalculation instead of being dropped.
Also note what unique jobs don't do. They prevent duplicates in the queue. They don't stop two different jobs for the same tenant from running at the same time. For that, there's job middleware.
Job middleware: overlap and rate limits
WithoutOverlapping makes sure only one job with a given key runs at a time. Others are released back to the queue to try again later:
use Illuminate\Queue\Middleware\WithoutOverlapping;
public function middleware(): array
{
return [(new WithoutOverlapping($this->tenant->id))->releaseAfter(10)->expireAfter(180)];
}
Rate limiting is for the outside world: SMS gateways, email APIs, anything with a quota. Define the limiter once, then attach it to the job:
// AppServiceProvider::boot()
RateLimiter::for('sms', function (object $job) {
return Limit::perMinute(30);
});
// in the job
public function middleware(): array
{
return [new RateLimited('sms')];
}
public function retryUntil(): DateTime
{
return now()->addHour();
}
That retryUntil() is not decoration. When a job hits the limit, it's released back to the queue, and that counts as an attempt. With a normal $tries = 3, a busy hour can push perfectly healthy jobs into the failed table without a single real error. A time-based limit fits rate-limited work much better than a count.
A rule I like: a chain describes order, a batch describes a group, uniqueness describes the queue, and middleware describes execution. If you're reaching for sleep(), a lock you wrote by hand, or a flag column called is_processing, one of these four probably fits better.
Which tool for which problem
| You need… | Use |
|---|---|
| Steps that must run in order, each retrying on its own | Bus::chain() |
| Many independent jobs with progress and a finish line | Bus::batch() |
| The same work queued only once | ShouldBeUnique |
| Never two at once for the same record | WithoutOverlapping |
| Respect an external API's quota | RateLimited + retryUntil() |
They also combine. A batch can contain chains (put an array inside the batch array), and any job in either can carry middleware. The Laravel 12 queue docs cover the full set of options.
Which of these have you used in production, and which one bit you first? For me, the cancelled-batch check is the one I see forgotten most often.

Be first to comment it...