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
<?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

php
// 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+)

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

php
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 Version defer() Available? Queue Jobs Available?
Laravel 13 (current) ✓ Yes ✓ Yes
Laravel 12 ✓ Yes ✓ Yes
Laravel 11 ✓ Yes (introduced) ✓ 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 Separate process ✓ Yes
Setup complexity Medium (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)

Real-world defer() examples

php
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

php
// 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:

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:

PHPUnit test

php
<?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

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

javascript
// 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));

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

Work with me

Need help with your Laravel architecture?

I build clean, maintainable Laravel backends for SaaS products — REST APIs, queue systems and production patterns like the one in this article.