
- March 17, 2026
- maulik.jadvani@gmail.com
- 0
Laravel Redis Integration — Complete Guide with Real-World Use Cases & Examples
Redis is the fastest in-memory data store on the planet. This guide covers everything — installation, configuration, caching, sessions, queues, pub/sub, rate limiting, leaderboards, and real-time counters — all with production-ready Laravel code examples and explanations of how Redis works under the hood.
What is Redis?
Redis (Remote Dictionary Server) is an open-source, in-memory data structure store. Unlike a traditional database that reads from disk, Redis keeps everything in RAM — making reads and writes blazing fast (sub-millisecond latency).
Redis supports rich data structures: strings, hashes, lists, sets, sorted sets, bitmaps, streams, and more. Each one unlocks a different real-world pattern — sorted sets power leaderboards, pub/sub powers real-time messaging, lists power job queues.
How Redis Works — Under the Hood
Redis runs as a single-threaded server process. All commands are executed sequentially in memory — no disk I/O on the hot path. Here is the request lifecycle for a Laravel cache read:
Redis data persistence
Despite being in-memory, Redis can persist data to disk using two strategies:
- RDB (snapshots) — saves a point-in-time snapshot at configured intervals. Fast restores, small files.
- AOF (Append Only File) — logs every write command. Slower but much more durable — can replay all operations on restart.
For Laravel cache and sessions, RDB is usually sufficient. For queues where job loss would be costly, enable AOF.
Redis data structures used by Laravel
| Structure | Redis Type | Laravel Feature |
|---|---|---|
| Key → Value | String | Cache, Sessions, Atomic Locks |
| Named fields | Hash | Cache tags (internal) |
| Ordered queue | List | Queue driver (jobs pushed to list) |
| Unique members | Set | Queue delayed jobs, deduplication |
| Scored ranking | Sorted Set | Leaderboards, rate limiting windows |
| Message channels | Pub/Sub | Broadcasting, real-time events |
Installation & Setup
1. Install Redis server
# Ubuntu / Debian sudo apt update && sudo apt install redis-server -y sudo systemctl enable redis-server sudo systemctl start redis-server # Verify it is running redis-cli ping # Output: PONG
2. Install PHP Redis client
Laravel supports two PHP clients — PhpRedis (C extension, fastest) and Predis (pure PHP, no extension needed). Predis is the easiest to get started with:
# Option A — Predis (recommended for getting started) composer require predis/predis # Option B — PhpRedis extension (recommended for production performance) sudo apt install php-redis # Then set REDIS_CLIENT=phpredis in .env
3. Install Laravel Horizon (optional but recommended)
Horizon gives you a beautiful real-time dashboard for Redis queues:
composer require laravel/horizon php artisan horizon:install php artisan migrate
Laravel Configuration
.env settings
REDIS_CLIENT=predis # or phpredis REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 REDIS_DB=0 # Tell Laravel to use Redis for cache, sessions, and queues CACHE_DRIVER=redis SESSION_DRIVER=redis QUEUE_CONNECTION=redis
config/database.php — Redis connection
'redis' => [
'client' => env('REDIS_CLIENT', 'predis'),
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
],
// Separate DB for cache to avoid key collisions
'cache' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_CACHE_DB', 1),
],
],REDIS_PREFIX in .env (e.g. myapp_) when multiple Laravel apps share the same Redis instance to avoid key collisions between apps.Real-World Use Cases Overview
Caching
Cache DB queries, API responses, and computed data to reduce load and latency.
Sessions
Store user sessions in Redis for fast reads and horizontal scaling across servers.
Queues & Jobs
Push background jobs to Redis lists — process emails, webhooks, reports asynchronously.
Rate Limiting
Throttle API endpoints per user or IP with atomic Redis counters and TTL windows.
Pub / Sub
Broadcast real-time events to connected clients via Redis channels and Laravel Echo.
Leaderboards
Build real-time sorted leaderboards using Redis Sorted Sets with O(log N) inserts.
Counters
Atomic page view, like, and click counters that never have race conditions.
Atomic Locks
Prevent duplicate job execution or concurrent writes with distributed Redis locks.
1. Caching with Redis
Caching is the most common Redis use case. Instead of hitting your MySQL database on every request, store the result in Redis and serve it instantly from memory on subsequent calls.
Basic cache operations
use Illuminate\Support\Facades\Cache;
// Store a value for 60 minutes
Cache::put('user:42:profile', $profileData, now()->addMinutes(60));
// Retrieve — returns null if expired or missing
$profile = Cache::get('user:42:profile');
// Store forever (no expiry)
Cache::forever('site:settings', $settings);
// Delete a key
Cache::forget('user:42:profile');
// Check existence
if (Cache::has('user:42:profile')) { ... }Remember pattern — the most useful cache helper
// Fetch from cache OR run the closure and store the result
$products = Cache::remember('homepage:products', now()->addHours(2), function () {
return Product::with('category')
->where('featured', true)
->orderBy('sort_order')
->get();
});
// rememberForever — no expiry
$countries = Cache::rememberForever('countries:all', fn () => Country::all());Cache tags — group and flush related keys
// Store tagged cache entries
Cache::tags(['products', 'homepage'])->put('featured', $featured, 3600);
Cache::tags(['products', 'category:5'])->put('cat5_items', $items, 3600);
// Flush ALL product-related caches at once (e.g., after product update)
Cache::tags('products')->flush();Cache with model observers — auto-invalidate on update
// app/Observers/ProductObserver.php
class ProductObserver
{
public function updated(Product $product): void
{
// Invalidate all product-related caches when any product changes
Cache::tags('products')->flush();
Cache::forget("product:{$product->id}");
}
public function deleted(Product $product): void
{
Cache::tags('products')->flush();
}
}
// Register in AppServiceProvider::boot()
Product::observe(ProductObserver::class);2. Sessions in Redis
By default Laravel stores sessions in files. Switching to Redis makes sessions faster and allows multiple application servers to share session state — essential for horizontal scaling.
Enable Redis sessions
# .env SESSION_DRIVER=redis SESSION_LIFETIME=120 # minutes SESSION_CONNECTION=default # matches config/database.php redis connection name
No code changes are needed — Laravel handles everything transparently. Sessions are stored as Redis strings with automatic TTL expiry.
Working with sessions in controllers
// Store data in session
session(['cart_id' => $cart->id]);
request()->session()->put('last_viewed', $product->id);
// Retrieve
$cartId = session('cart_id');
// Flash — available only for the next request
session()->flash('success', 'Order placed successfully!');
// Forget
session()->forget('cart_id');
session()->flush(); // clear entire sessionlaravel_session:{session_id} with a TTL equal to your SESSION_LIFETIME. Redis automatically removes expired sessions — no garbage collection needed.3. Queues & Background Jobs
Redis queues let you defer slow work — sending emails, generating PDFs, calling third-party APIs — to background workers so your HTTP response returns instantly.
How it works
When you dispatch() a job, Laravel pushes a serialised payload onto a Redis List. Queue workers continuously BLPOP (blocking left-pop) from the list and process jobs one by one.
Create and dispatch a job
php artisan make:job SendOrderConfirmation
// app/Jobs/SendOrderConfirmation.php
namespace App\Jobs;
use App\Models\Order;
use App\Mail\OrderConfirmed;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Mail;
class SendOrderConfirmation implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public int $tries = 3; // retry up to 3 times on failure
public int $backoff = 60; // wait 60 seconds between retries
public function __construct(public Order $order) {}
public function handle(): void
{
Mail::to($this->order->user->email)
->send(new OrderConfirmed($this->order));
}
}// Dispatch from a controller — returns instantly to the user
SendOrderConfirmation::dispatch($order);
// Dispatch to a specific queue
SendOrderConfirmation::dispatch($order)->onQueue('emails');
// Delay by 10 minutes
SendOrderConfirmation::dispatch($order)->delay(now()->addMinutes(10));Start queue workers
# Process the default queue php artisan queue:work redis # Process a specific named queue php artisan queue:work redis --queue=emails # Process with concurrency using Horizon php artisan horizon
queue:work under Supervisor so workers restart automatically on crash. Never use queue:listen in production — it re-boots the framework on every job.4. Rate Limiting with Redis
Redis is perfect for rate limiting because of its atomic INCR and EXPIRE commands. Laravel's built-in rate limiter uses Redis internally.
Define rate limiters in AppServiceProvider
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
// Global API limiter — 60 requests per minute per user/IP
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)
->by($request->user()?->id ?: $request->ip());
});
// Strict OTP limiter — 5 requests per 10 minutes per IP
RateLimiter::for('otp', function (Request $request) {
return Limit::perMinutes(10, 5)
->by($request->ip())
->response(function () {
return response()->json([
'message' => 'Too many OTP attempts. Try again later.'
], 429);
});
});
// Per-second burst limiter (Laravel 11+)
RateLimiter::for('uploads', function (Request $request) {
return [
Limit::perSecond(2)->by($request->user()->id),
Limit::perMinute(30)->by($request->user()->id),
];
});
}Apply to routes
// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
Route::post('/orders', OrderController::class);
});
Route::middleware('throttle:otp')->post('/auth/otp', OtpController::class);Manual rate limiting in controllers
use Illuminate\Support\Facades\RateLimiter;
$key = 'login-attempts:' . request()->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'message' => "Too many attempts. Retry in {$seconds} seconds."
], 429);
}
RateLimiter::hit($key, 300); // increment, expires in 300s
// ... authenticate user
RateLimiter::clear($key); // reset on successful login5. Pub / Sub — Real-time Events
Redis Pub/Sub lets one part of your application publish messages to a channel and any number of subscribers receive them instantly. Laravel uses this under the hood for its redis broadcast driver.
Broadcasting setup
# .env BROADCAST_DRIVER=redis # Install Laravel Echo and Pusher JS (acts as Redis bridge via Soketi or Laravel Reverb) npm install laravel-echo pusher-js
Define a broadcastable event
php artisan make:event OrderShipped
// app/Events/OrderShipped.php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
class OrderShipped implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets;
public function __construct(public Order $order) {}
// Broadcast on a private channel for the order's user
public function broadcastOn(): Channel
{
return new \Illuminate\Broadcasting\PrivateChannel(
'orders.' . $this->order->user_id
);
}
// Control exactly what data goes to the frontend
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'status' => $this->order->status,
'message' => 'Your order has been shipped!',
];
}
}Fire the event from a controller or job
// When order status changes $order->update(['status' => 'shipped']); broadcast(new OrderShipped($order))->toOthers();
Listen in JavaScript with Laravel Echo
// resources/js/app.js
window.Echo.private(`orders.${userId}`)
.listen('OrderShipped', (event) => {
console.log('Order shipped!', event.order_id);
showNotification(event.message);
});Direct Redis Pub/Sub (without broadcasting)
use Illuminate\Support\Facades\Redis;
// Publisher — send a message to a channel
Redis::publish('notifications', json_encode([
'user_id' => 42,
'message' => 'You have a new follower!',
]));
// Subscriber — run this in a long-running artisan command
Redis::subscribe(['notifications'], function (string $message) {
$data = json_decode($message, true);
// Process notification...
echo "Received: {$data['message']}\n";
});6. Real-time Leaderboards with Sorted Sets
Redis Sorted Sets (ZADD, ZRANGE, ZRANK) are purpose-built for leaderboards. Each member has a floating-point score. Redis keeps them sorted automatically on every insert in O(log N) time.
Building a game leaderboard
use Illuminate\Support\Facades\Redis;
class LeaderboardService
{
protected string $key = 'leaderboard:global';
// Add or update a player's score
public function addScore(int $userId, float $score): void
{
// ZADD key score member — updates if member already exists
Redis::zadd($this->key, $score, "user:{$userId}");
}
// Increment score (e.g., +10 points per kill)
public function incrementScore(int $userId, float $points): void
{
Redis::zincrby($this->key, $points, "user:{$userId}");
}
// Get top N players (highest score first)
public function getTopPlayers(int $limit = 10): array
{
// ZREVRANGE returns highest-score members first
// WITHSCORES includes the score alongside the member
return Redis::zrevrange($this->key, 0, $limit - 1, 'WITHSCORES');
}
// Get a specific player's rank (0-indexed, lower = better)
public function getRank(int $userId): int|null
{
$rank = Redis::zrevrank($this->key, "user:{$userId}");
return $rank !== null ? $rank + 1 : null; // convert to 1-indexed
}
// Get player's score
public function getScore(int $userId): float
{
return (float) Redis::zscore($this->key, "user:{$userId}");
}
}Usage in a controller
$lb = app(LeaderboardService::class); // Player scores 50 points $lb->incrementScore(auth()->id(), 50); // Get top 10 $top10 = $lb->getTopPlayers(10); // Get current player's rank $myRank = $lb->getRank(auth()->id());
SELECT … ORDER BY score DESC LIMIT 10 on a 10 million row table requires a full index scan. Redis ZREVRANGE on the same data is O(log N + M) — the top 10 is returned in microseconds regardless of total player count.7. Atomic Real-time Counters
Redis INCR is atomic — even with thousands of concurrent requests, it increments exactly once per call with zero race conditions. No transactions, no locking.
Page view counter
use Illuminate\Support\Facades\Redis;
class ViewCounter
{
// Increment view count for a post
public function increment(int $postId): int
{
return Redis::incr("post:{$postId}:views");
}
// Get current count
public function get(int $postId): int
{
return (int) Redis::get("post:{$postId}:views") ?? 0;
}
// Flush to database periodically (e.g., every hour via scheduled job)
public function syncToDatabase(int $postId): void
{
$views = $this->get($postId);
Post::where('id', $postId)->increment('view_count', $views);
// Reset Redis counter after syncing
Redis::set("post:{$postId}:views", 0);
}
}Like / reaction counter with deduplication
// Use a Redis Set to track who liked — prevents duplicate likes
public function like(int $postId, int $userId): bool
{
$key = "post:{$postId}:likes";
// SADD returns 1 if added (new like), 0 if already existed (duplicate)
$added = Redis::sadd($key, $userId);
if ($added) {
Redis::incr("post:{$postId}:like_count");
}
return (bool) $added;
}
public function unlike(int $postId, int $userId): void
{
Redis::srem("post:{$postId}:likes", $userId);
Redis::decr("post:{$postId}:like_count");
}
public function getLikeCount(int $postId): int
{
return (int) Redis::get("post:{$postId}:like_count");
}
public function hasLiked(int $postId, int $userId): bool
{
return (bool) Redis::sismember("post:{$postId}:likes", $userId);
}8. Atomic Locks — Prevent Duplicate Execution
Redis locks let you ensure that a critical section of code (like processing a payment or sending an email) only runs once, even if multiple servers or workers try to execute it simultaneously.
Laravel Cache lock (recommended)
use Illuminate\Support\Facades\Cache;
// Attempt to acquire a lock for 10 seconds
$lock = Cache::lock('process-order:' . $order->id, 10);
if ($lock->get()) {
try {
// Only ONE server/worker executes this block at a time
$this->processPayment($order);
$this->sendConfirmation($order);
} finally {
$lock->release(); // always release, even on exception
}
} else {
// Another process is already handling this order
throw new \RuntimeException('Order is already being processed.');
}Block until lock is available
// Wait up to 5 seconds to acquire the lock, then execute
Cache::lock('generate-report', 30)->block(5, function () {
// Generate report — guaranteed to run exclusively
ReportService::generate();
});Owner-token locks (for queue workers)
// Acquire lock and get owner token
$lock = Cache::lock('send-invoice:' . $invoiceId, 60);
$token = $lock->acquire();
// Pass token to a job that will release it
dispatch(new ProcessInvoice($invoiceId, $token));
// Inside the job — release using the token
Cache::restoreLock('send-invoice:' . $this->invoiceId, $this->token)->release();Redis CLI Commands Cheatsheet
Useful commands for debugging Redis in your Laravel application:
# Connect to Redis CLI redis-cli # List all keys matching a pattern KEYS laravel_cache:* # Check a cached value GET laravel_cache:homepage_products # Check TTL remaining (in seconds, -1 = no expiry, -2 = key gone) TTL laravel_cache:homepage_products # Delete a specific key DEL laravel_cache:homepage_products # Check memory usage of a key MEMORY USAGE laravel_cache:homepage_products # Monitor all commands in real-time (debug only!) MONITOR # Get Redis server info and stats INFO # See all connected clients CLIENT LIST # Flush entire database (DANGER — removes everything) FLUSHDB
KEYS * or FLUSHDB on a production Redis server. KEYS blocks the server while scanning and FLUSHDB wipes all data. Use SCAN for safe key iteration in production.Production Checklist
- Security Always set a strong
requirepassinredis.confand restrict Redis to localhost or a private network — never expose port 6379 publicly. - Persistence Enable AOF persistence for queues (
appendonly yesin redis.conf) so jobs survive Redis restarts without data loss. - Prefix Set
REDIS_PREFIXin.envwhen multiple apps share the same Redis instance to prevent key collisions. - Separate DBs Use different Redis databases (
database: 0,1,2) for cache, sessions, and queues — makes flushing cache safe without affecting sessions. - Horizon Run Laravel Horizon for queue monitoring — it shows throughput, wait times, failed jobs, and worker health in a beautiful dashboard.
- Memory limit Set
maxmemoryandmaxmemory-policy allkeys-lruin redis.conf so Redis evicts least-recently-used cache keys instead of crashing when memory is full. - Supervisor Run queue workers under Supervisor in production so they restart automatically after crashes or deploys.
- No KEYS in prod Use
SCANinstead ofKEYS *for key iteration —KEYSblocks all other operations during execution. - Monitoring Track Redis memory usage, hit/miss ratio, and connected clients via Laravel Horizon, Redis INFO, or tools like RedisInsight.
Conclusion
Redis transforms Laravel applications. What starts as a simple cache layer quickly becomes the backbone of your entire application — handling sessions, job queues, real-time pub/sub, rate limiting, leaderboards, and atomic counters, all from a single blazing-fast in-memory store.
The key to using Redis well is choosing the right data structure for each use case: strings for cache and counters, lists for queues, sorted sets for leaderboards, sets for deduplication, and pub/sub for real-time events. Laravel's Redis facade and cache abstractions make all of these patterns clean and testable.
Start with caching your slowest queries, switch sessions to Redis, and add queue workers — those three changes alone will make your application dramatically faster and more scalable. Build from there.
If you need help architecting or implementing a Redis-backed Laravel application — including Horizon setup, Redis Cluster for high availability, or advanced caching strategies — Contact Zeus Technocrats.
