Laravel 11 quietly introduced one of the most practical helpers in years: defer(). It lets you run code after the HTTP response has been sent to the user, with absolutely zero queue infrastructure. No Redis. No worker processes. No Supervisor config.
But it does not replace queues. They're designed for different jobs. This guide explains how both work, the exact tradeoffs, and a clear decision framework for every scenario you'll encounter in production.
How Laravel Queues Work
A queued job is serialized and pushed into a persistent store — Redis, a database table, Amazon SQS, or Beanstalkd. A separate long-running PHP process (the queue worker) picks jobs from that store and executes them, independently of any HTTP request.
app/Jobs/SendWelcomeEmail.phpThe controller returns the HTTP response right away. The job runs later, in a separate process. If the job fails, Laravel automatically retries it according to your $tries setting and records it in the failed_jobs table.
How Laravel defer() Works
defer() was introduced in Laravel 11.0 (released March 2024) and is also available in Laravel 12 and 13. It takes a closure and defers its execution until after the HTTP response has been sent to the browser — all within the same PHP process.
The user gets the JSON response in milliseconds. The Metrics::recordOrder() call happens after, without blocking the response. No worker, no Redis, no extra config needed.
4xx or 5xx response will skip them. Chain ->always() to override this.
Version Support
defer() is available starting from Laravel 11. It does not exist in Laravel 10 or any earlier version. All currently supported versions of Laravel include it:
| Laravel Version | defer() Available? | Queue Jobs Available? |
|---|---|---|
| Laravel 13 (current) | ✅ Yes | ✅ Yes |
| Laravel 12 | ✅ Yes | ✅ Yes |
| Laravel 11 | ✅ Yes (introduced here) | ✅ Yes |
| Laravel 10 | ❌ No | ✅ Yes |
| Laravel 9 / 8 | ❌ No | ✅ Yes |
Queue vs defer() — Full Comparison
| Feature | Queue Job | defer() |
|---|---|---|
| Runs in same PHP process | ❌ No — separate worker | ✅ Yes |
| Needs queue worker (Artisan) | ✅ Required | ❌ Not needed |
| Needs Redis / DB queue driver | ✅ Required | ❌ Not needed |
| Persisted to storage | ✅ Yes — survives restarts | ❌ No — lost on crash |
| Automatic retries on failure | ✅ Yes (configurable) | ❌ No |
| Delayed / scheduled execution | ✅ Yes — any delay | ❌ No |
| Monitoring (Horizon / Telescope) | ✅ Yes | ❌ No |
| Cross-server execution | ✅ Yes | ❌ No — same server |
| Run after response is sent | ⚠️ Different process, not tied to response | ✅ Yes |
| Setup complexity | ⚠️ Medium — driver + worker + Supervisor | ✅ Zero — just call defer() |
| Laravel version required | Any version | Laravel 11+ |
When to Use defer()
defer() is the right tool when:
- The task is non-critical — losing it on a crash is acceptable
- The task is lightweight — a quick database write, counter increment, or cache update
- You want to avoid queue infrastructure entirely (small apps, staging environments)
- The task only makes sense on a successful response
- You don't need retries if it fails
Good use cases for defer()
- Recording analytics or metrics after an order (e.g.,
Metrics::reportOrder($order)) - Writing a non-critical audit trail entry
- Incrementing a page view or "last seen" counter
- Warming up a cache entry after a resource is created
- Sending a non-critical Slack notification to an internal channel
- Firing a webhook to a monitoring service (where loss is tolerable)
When to Use Queue Jobs
Queue jobs are the right tool when:
- The task must not be lost — it needs to survive server restarts and crashes
- The task must retry on failure (sending emails, calling payment APIs)
- The task should run after a delay (e.g., a follow-up email 24 hours later)
- The task is CPU or memory heavy (image processing, report generation, PDF rendering)
- You need visibility and monitoring via Horizon or Telescope
- The task should run on a dedicated worker server separately from the web server
Good use cases for Queue Jobs
- Sending transactional emails (welcome email, password reset, invoice)
- Processing file uploads (image resizing, video transcoding, CSV imports)
- Calling third-party APIs that may fail or be slow (payment gateway, SMS, shipping)
- Sending push notifications or bulk WhatsApp/FCM messages
- Generating and storing PDF reports
- Scheduled/delayed tasks (e.g., "send reminder 3 days after sign-up")
- Syncing data to external systems (CRM, ERP, analytics platform)
defer() in Laravel 13: What Changed?
Laravel 13 did not change the defer() API — it works identically to Laravel 11 and 12. The function signature, behaviour, ->always(), named deferral, and test helpers are all the same across all three versions.
What did change in Laravel 13 is the surrounding ecosystem. The new Reverb database driver makes it possible to run real-time broadcasts without Redis, and PHP Attributes replace the repetitive boilerplate in queue jobs — but the defer() function itself is unchanged.
Practical Decision Guide
Ask yourself these questions when choosing:
| Question | If YES → Use |
|---|---|
| Can I tolerate losing this task if the server crashes? | defer() |
| Must this task retry if it fails? | Queue Job |
| Is this task longer than ~1 second? | Queue Job |
| Does this task run after a time delay (minutes/hours)? | Queue Job |
| Does this task call an external API (email, payment, SMS)? | Queue Job |
| Is this a quick write (counter, metric, cache bust)? | defer() |
| Am I on Laravel 10 or earlier? | Queue Job |
| Do I want zero infrastructure for this? | defer() |
Testing Deferred Functions
By default, deferred functions execute asynchronously after the response. In tests this can cause timing issues. Laravel provides withoutDefer() to run them synchronously during tests:
To disable defer for all tests in a test case, override setUp() in your base TestCase class:
Swoole / FrankenPHP Warning
If you're using the Swoole PHP extension or FrankenPHP, be aware that Swoole has its own global defer() function that conflicts with Laravel's. Always import Laravel's helper explicitly to avoid a hard-to-debug collision:
use function Illuminate\Support\defer; at the top of any file that calls defer(), regardless of your server environment. It makes the code self-documenting and safe across all configurations.
Frequently Asked Questions
What is the difference between Laravel Queue and defer()?
A Queue Job is serialized and pushed into a persistent store (Redis, database, SQS) processed by a separate worker. defer() runs a PHP closure in the same process, after the HTTP response has been sent — no worker needed, no persistence, no retries. Use defer() for lightweight fire-and-forget tasks. Use queues for anything that must be reliable or retried on failure.
Which Laravel versions support defer()?
defer() was introduced in Laravel 11.0 (March 2024). It is available in Laravel 11, 12, and 13. It is NOT available in Laravel 10 or earlier.
Does Laravel defer() need a queue worker?
No. defer() does not need a queue worker, Redis, or any additional infrastructure. The deferred closure runs in the same PHP process, after the HTTP response has been sent — making it ideal for simple apps that don't want to manage queue infrastructure.
What happens to deferred functions if the response fails?
By default, deferred functions only execute if the HTTP response completes successfully. A 4xx or 5xx response will skip them. To force them to always run, chain ->always(): defer(fn () => doWork())->always();
Can defer() replace queues in Laravel?
No. defer() is a lightweight complement to queues, not a replacement. It lacks retries, persistence, monitoring, delayed execution, and cross-server processing. For anything that must succeed reliably — emails, payment webhooks, file processing — always use a queue job. See also: Why Your Laravel Queue Workers Die (and How to Fix It).