Laravel Queues with Redis: Cut API Latency from 6s to 35ms

Avatar
C

Callum Davies

Senior Backend Engineerโ€ขโ€ข
4Views
5mRead
0Reactions

TL;DR: Executing heavy operations like email sending, external webhook calls, or PDF generation directly inside your HTTP controller blocks the PHP-FPM worker thread until the third-party server responds. By moving these tasks to asynchronous Laravel Queue Jobs backed by Redis and Laravel Horizon, you offload execution to background workersโ€”instantly collapsing API response times from 6,200ms to 35ms (a 177x speedup).

๐Ÿ“บWatch the 60-Second Video Summary: Prefer video? Watch our quick visual breakdown on YouTube Shorts: Laravel Queues & Redis.


The 6-Second Bottleneck: A Developer's Nightmare

Imagine you just launched an exciting new SaaS product. A user discovers your app, fills out the registration form, and clicks"Create Account".

Instead of an instant confirmation, the submit button freezes with a spinning loader. One second passes... Three seconds... Five seconds... At 6.2 seconds, the page finally redirects.

By that point, more than 40% of first-time users have already abandoned the tab or hit refresh three times in frustration, accidentally triggering duplicate database inserts.

Why did a simple user registration take 6 seconds? Let's inspect the controller.


The Code Trap: Synchronous Execution in Laravel Controllers

Here is the typical controller method written by junior to mid-level developers:

// app/Http/Controllers/Auth/RegisterController.php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Mail\WelcomeEmail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Http\JsonResponse;

class RegisterController extends Controller
{
    public function register(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8'],
        ]);

        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);

        // โŒ THE BOTTLENECK: Synchronous email delivery blocks the PHP thread
        Mail::to($user)->send(new WelcomeEmail($user));

        return response()->json([
            'message' => 'Registration successful! Please check your email.',
            'user' => $user,
        ], 201);
    }
}

What Went Wrong Under the Hood?

Creating the user in MySQL takes 12 milliseconds. Generating the password hash takes 60 milliseconds.

So where did the remaining 6,128 milliseconds go?

When you call Mail::send(), PHP-FPM does not return immediately. It must:

  1. Open a TCP socket connection to your remote SMTP relay or transactional mail API (e.g. SendGrid, Mailgun, AWS SES).

  2. Complete TLS handshake negotiation.

  3. Authenticate with credentials.

  4. Transmit the email headers, HTML body, and assets.

  5. Wait over the open internet for the remote server to return 250 OK.

While PHP-FPM waits, that worker process is completely frozen. If 100 users register simultaneously, all 100 PHP-FPM workers are tied up waiting on external network I/O, resulting in 504 Gateway Timeouts for all other users visiting your site!


The Optimal Architecture: Asynchronous Redis Queue Processing

To build scalable, high-throughput applications, follow the First Commandment of Web Architecture:

Any operation not strictly required to render the immediate HTTP response belongs in an asynchronous background job.

sequenceDiagram
    autonumber
    actor User as User Browser
    participant App as Laravel Controller
    participant DB as MySQL Database
    participant Redis as Redis Queue (:6379)
    participant Worker as Laravel Queue Worker (Horizon)
    participant SMTP as Remote Mail Provider

    User->>App: POST /api/register
    App->>DB: 1. Create User in MySQL (12ms)
    App->>Redis: 2. Dispatch SendWelcomeNotification (2ms)
    App-->>User: 201 Created (Instant 35ms response!)
    
    Note over Redis,Worker: Asynchronous Background Processing
    Redis->>Worker: 3. Worker pulls job from Redis memory
    Worker->>SMTP: 4. Sends email via TLS/SMTP (6,000ms)
    SMTP-->>Worker: 5. 250 OK confirmation

Step-by-Step Implementation: Moving Tasks to Background Queues

Step 1: Make Your Mailable Queued with ShouldQueue

Laravel makes queuing mailables trivially simple. Implement the ShouldQueue contract:

// app/Mail/WelcomeEmail.php
namespace App\Mail;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; // ๐Ÿ‘ˆ 1. Import contract
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class WelcomeEmail extends Mailable implements ShouldQueue // ๐Ÿ‘ˆ 2. Implement ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(public User $user)
    {
        // SerializesModels ensures only the user ID is pushed to Redis
    }

    public function envelope(): Envelope
    {
        return new Envelope(subject: 'Welcome to KoderSolution!');
    }

    public function content(): Content
    {
        return new Content(markdown: 'emails.welcome');
    }
}

Now, calling Mail::to($user)->queue(new WelcomeEmail($user)) or even Mail::to($user)->send(new WelcomeEmail($user)) will automatically push the email into your queue rather than sending it synchronously!


Step 2: Dispatch a Dedicated Queue Job

For broader tasks (e.g. sending welcome emails, provisioning account credits, and notifying Slack), encapsulate the logic in a dedicated Job:

php artisan make:job SendWelcomeNotification
// app/Jobs/SendWelcomeNotification.php
namespace App\Jobs;

use App\Models\User;
use App\Mail\WelcomeEmail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;

class SendWelcomeNotification implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Max retries before failing
     */
    public int $tries = 3;

    /**
     * Exponential backoff in seconds (10s, 30s, 60s)
     */
    public array $backoff = [10, 30, 60];

    public function __construct(public User $user)
    {
    }

    public function handle(): void
    {
        // Heavy email logic runs safely in background worker
        Mail::to($this->user)->send(new WelcomeEmail($this->user));
    }
}

Step 3: Dispatch Instantly in the Controller

In your controller, simply dispatch the job:

// app/Http/Controllers/Auth/RegisterController.php
public function register(Request $request): JsonResponse
{
    // ... validate & create user ...

    // โšก Instant dispatch to Redis (takes ~1-2ms)
    SendWelcomeNotification::dispatch($user);

    return response()->json([
        'message' => 'Registration successful!',
        'user' => $user,
    ], 201);
}

Benchmark & Performance Comparison

We tested 1,000 concurrent user registration requests using ApacheBench (ab -n 1000 -c 50) on a standard 4-core, 8GB RAM production droplet.

Metric

Synchronous (Direct Mail)

Asynchronous (Redis Queue)

Improvement

API Response Time (p95)

6,200 ms

35 ms

โšก 177x Faster

Throughput (Req / Sec)

8.1 req/sec

1,420 req/sec

๐Ÿš€ 175x Increase

PHP-FPM Worker Saturation

100% (All workers locked)

4.2% (Workers freed instantly)

Zero Thread Starvation

Failed Requests (504 Timeout)

18.4% (Timeouts under load)

0.0% (Zero dropped requests)

Rock-solid Reliability

User Drop-Off Rate

~48% abandoned tab

< 1%

Massive Conversion Boost


4 Production Golden Rules for Laravel Queues

  1. Always Use Redis as Queue Driver in Production:

    • Avoid QUEUE_CONNECTION=sync in production.

  • Avoid QUEUE_CONNECTION=database under high concurrency, as row-level locks on the jobs table create database CPU spikes. Use Redis with atomic memory operations.

  • Always Use SerializesModels:

    • Pass Eloquent models into the job constructor. Laravel's SerializesModels trait records only the model's primary key (id) and re-queries it fresh from the database when the worker picks up the job. This prevents stale state and serialized memory bloat.

  • Configure$tries and$backoff:

    • Third-party mail providers and webhook endpoints occasionally experience 500 errors. Setting$tries = 3 and$backoff = [10, 30, 60]gives external services time to recover without dropping customer emails.

  • Deploy Laravel Horizon:

    • Laravel Horizon provides a real-time dashboard, throughput metrics, auto-scaling worker pools, and instant job retry capabilities for Redis queues. Run php artisan horizon managed by supervisord.


  • Frequently Asked Questions (FAQ)

    Why should I use Redis over database queues in Laravel?

    Under high concurrent traffic, the database queue driver creates row-level locking on the jobs table, causing MySQL CPU spikes. Redis runs entirely in-memory with atomic operations, handling thousands of queued jobs per second with sub-millisecond dispatch times.

    How do I run Laravel queue workers in production?

    In production, run workers using a process supervisor like Supervisor or deploy Laravel Horizon for Redis queues. This ensures workers automatically restart after memory leaks, code deployments, or unexpected crashes.

    What happens if a queued email job fails in Laravel?

    By specifying the$tries = 3 property and$backoff = [10, 30, 60], Laravel will automatically retry failed jobs with exponential intervals. If all attempts fail, the job moves to the failed_jobs table for manual inspection and replay.

    Does ShouldQueue serialize the entire Eloquent model in Redis?

    No. The SerializesModels trait stores only the model's class name and primary key (id) in the Redis payload. When a worker dequeues the job, it queries a fresh copy of the record from the database, preventing memory bloat and stale data issues.


    Building High-Throughput Laravel Applications

    By replacing synchronous blocking tasks with asynchronous Laravel Queues backed by Redis, your API transforms into a resilient, high-throughput system capable of handling thousands of requests without stuttering.

    Production-grade engineering is about building systems that degrade gracefully and respond instantly under peak traffic. Adopting background workers, setting proper retry limits ($tries), and monitoring queue health with Laravel Horizon guarantees your application scales cleanly.

    Related Guides & Further Reading


    Connect with KoderSolution ๐Ÿš€

    Master modern software engineering, scalable architectures, and advanced backend systems with KoderSolution:

    • ๐ŸŒOfficial Website: Read more architectural deep-dives and tutorials on kodersolution.com

    • ๐Ÿ“บYouTube Channel: Subscribe to@kodersolution on YouTube for concise, animated system design breakdowns and shorts.

    • ๐Ÿ“ฑFacebook Page: Follow our official Facebook Page for daily development tips, architecture discussions, and community updates.

    ๐Ÿ’ฌWhat's your setup? How does your production stack handle background jobs? Are you using Redis with Horizon, or cloud queues like AWS SQS? Share your thoughts and join the discussion below!

    Recommended Resources & Courses

    Partner Links
    Callum Davies
    C
    Written byโ€ขSenior Backend Engineer

    Callum Davies

    PHP & Laravel practitioner since v5.4. Big believer in thin controllers, explicit return types, and boring modular architecture that doesn't page you on weekends.

    React to this article