Laravel Queue vs defer(): When to Use Each (Laravel 11, 12 & 13)

Short answer: they solve different problems Use defer() for lightweight fire-and-forget tasks that run in the same request lifecycle — no worker, no Redis needed. Use a Queue Job when you need retries on failure, delayed execution, heavy processing, or reliable delivery. defer() is available from Laravel 11 onward.

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.php
<?php namespace App\Jobs; use App\Models\User; use App\Mail\WelcomeEmail; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Facades\Mail; class SendWelcomeEmail implements ShouldQueue { use Queueable; public int $tries = 3; // retry up to 3 times on failure public int $timeout = 60; // kill the job after 60 seconds public function __construct(public User $user) {} public function handle(): void { Mail::to($this->user)->send(new WelcomeEmail($this->user)); } }
Dispatching the job from a controller
// Dispatches to queue — controller returns immediately SendWelcomeEmail::dispatch($user); // Dispatch with a 5-minute delay SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5)); // Dispatch to a specific queue SendWelcomeEmail::dispatch($user)->onQueue('emails');

The 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.

Basic defer() usage (Laravel 11+)
use function Illuminate\Support\defer; Route::post('/orders', function (Request $request) { $order = Order::create($request->validated()); // This runs AFTER the response is sent to the user defer(fn () => Metrics::recordOrder($order)); return response()->json($order, 201); // returned immediately });

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.

Important: By default, deferred functions only run if the response completes successfully. A 4xx or 5xx response will skip them. Chain ->always() to override this.
defer() options
use function Illuminate\Support\defer; // Run only on successful responses (default) defer(fn () => Metrics::recordOrder($order)); // Always run, even if the response is a 4xx or 5xx defer(fn () => AuditLog::write($event))->always(); // Named defer — so you can cancel it later if needed defer(fn () => Cache::warmUp($user), 'warm-cache'); defer()->forget('warm-cache'); // cancel before it runs

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 Versiondefer() 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

FeatureQueue Jobdefer()
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 requiredAny versionLaravel 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)
Real-world defer() examples
use function Illuminate\Support\defer; // 1. Record analytics after an API response public function store(OrderRequest $request) { $order = $this->orderService->create($request->validated()); defer(fn () => Analytics::track('order_created', [ 'user_id' => $order->user_id, 'amount' => $order->total, 'currency' => $order->currency, ])); return OrderResource::make($order); } // 2. Update a cached leaderboard after a score is saved public function submitScore(ScoreRequest $request) { $score = Score::create($request->validated()); defer(fn () => Cache::forget("leaderboard:{$score->game_id}")); return response()->json(['message' => 'Score saved']); } // 3. Log a "last active" timestamp without slowing the request public function show(User $user) { defer(fn () => $user->update(['last_active_at' => now()])); return UserResource::make($user); }

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)
Never use defer() for emails or payments. If your PHP process crashes between sending the response and executing the deferred closure, the task is silently lost. For anything that must be delivered — use a queue job with retries.

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.

Laravel 13: PHP Attribute syntax for queue jobs
// Laravel 13 — use PHP Attributes instead of class properties use Illuminate\Queue\Attributes\WithoutOverlapping; use Illuminate\Queue\Attributes\OnQueue; #[OnQueue('emails')] #[WithoutOverlapping] class SendWelcomeEmail implements ShouldQueue { use Queueable; public function __construct(public User $user) {} public function handle(): void { Mail::to($this->user)->send(new WelcomeEmail($this->user)); } }

Practical Decision Guide

Ask yourself these questions when choosing:

QuestionIf 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:

PHPUnit test
<?php class OrderControllerTest extends TestCase { public function test_order_records_metrics(): void { // Make deferred functions execute immediately in this test $this->withoutDefer(); Metrics::shouldReceive('recordOrder')->once(); $this->postJson('/api/orders', ['product_id' => 1, 'qty' => 2]) ->assertStatus(201); } }

To disable defer for all tests in a test case, override setUp() in your base TestCase class:

tests/TestCase.php
abstract class TestCase extends BaseTestCase { protected function setUp(): void { parent::setUp(); $this->withoutDefer(); // All deferred functions run immediately } }

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:

Always use the explicit import when running Swoole
// WRONG on Swoole — calls Swoole's defer(), not Laravel's defer(fn () => Metrics::record($data)); // CORRECT — explicitly use Laravel's namespace use function Illuminate\Support\defer; defer(fn () => Metrics::record($data));
Best practice: Always use 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).

Need Help with Laravel Performance?

I've implemented background processing — queues, defer, Horizon, Supervisor — across production SaaS platforms and e-commerce backends. Let's get your app running faster.

Fixed price · Reply within 4 hours · Remote worldwide

About the Author

Kamruzzaman Polash — Software Engineer specialising in Laravel, REST APIs, and scalable backend systems. 10+ projects delivered for clients worldwide.