The Features Nobody Tweets About
There’s a category of framework features that will never get a hype tweet. They don’t have flashy names, don’t make it into conference keynote demos, and won’t get a Medium article with 10,000 claps.
But they are the features you use every single day.
Laravel 13 ships three of them in Part 4 of this series: `Cache::touch()`, `Queue::route()`, and enhanced `DELETE…JOIN` support. Individually, each one is a small quality-of-life win. Together, they quietly shave hours off your development week and eliminate entire categories of bugs I’ve seen bite teams in production.
Let’s dig in.
Enhanced Developer Experience
Cache Optimization: Deep Dive Into `Cache::touch()`
Picture this. You have a user dashboard that loads a complex, pre-computed analytics report. That computation takes 4 seconds. You cache it with a 15-minute TTL. Sensible.
But here’s the catch: your active users check their dashboard every 2 minutes. For a user who is actively working, the cache expires while they’re mid-session, triggers a full recompute, and they wait 4 seconds. Not great.
The solution is a sliding expiry — extend the TTL whenever the user accesses the data. Before Laravel 13, doing this in Laravel looked like this:
// Old approach — three operations, one wasted round-trip
$report = Cache::get('report:user:' . $userId);
Cache::forget('report:user:' . $userId);
Cache::put('report:user:' . $userId, $report, now()->addMinutes(15));
You fetch the value (one network round-trip), delete it, then re-store it. For a large cached payload — say, a JSON blob of 100KB — this transfers data over the wire unnecessarily just to reset a timer.
The Laravel 13 Solution
// New approach — one operation, zero data transfer
$extended = Cache::touch('report:user:' . $userId, now()->addMinutes(15));
if (!$extended) {
// Key doesn't exist — regenerate the cache
$this->regenerateReport($userId);
}
| Scenario | Old Approach | Cache::touch() |
|---|---|---|
| Operations per cache extend | 3 | 1 |
| Data transferred | Full payload size | 0 bytes |
| Redis commands | GET + DEL + SET | EXPIRE |
Real-World Patterns Where This Shines
// 1. Sliding session expiry
public function heartbeat(Request $request): JsonResponse
{
$sessionKey = 'active_session:' . $request->user()->id;
Cache::touch($sessionKey, now()->addMinutes(30));
return response()->json(['status' => 'alive']);
}
// 2. API rate limit windows (sliding window pattern)
public function checkRateLimit(string $userId): bool
{
$key = 'rate_limit:' . $userId;
if (Cache::has($key)) {
Cache::touch($key, now()->addMinute()); // Slide the window
return true;
}
return false;
}
// 3. Hot dashboard data — keep warm while user is active
public function getDashboard(int $userId): array
{
$key = 'dashboard:' . $userId;
Cache::touch($key, now()->addMinutes(15)); // Extend TTL on every read
return Cache::get($key) ?? $this->buildDashboard($userId);
}
For business leaders
Queue Control
The Old Problem — Queue Config Scattered Everywhere
Option A
class ProcessPayment implements ShouldQueue
{
public string $connection = 'redis-priority';
public string $queue = 'payments';
}
Option B
ProcessPayment::dispatch()->onQueue('payments')->onConnection('redis-priority');
ProcessPayment::dispatch()->onQueue('payments')->onConnection('redis-priority'); // Again...
ProcessPayment::dispatch()->onQueue('payments')->onConnection('redis-priority'); // And again...
In a large codebase, you’d find `onQueue(‘payments’)` scattered across a dozen controllers. Change the queue name? Find and replace every occurrence. Miss one? That job silently runs on the wrong queue in production.
The Laravel 13 Solution
Laravel 13 adds queue routing by class via `Queue::route(…)`, allowing you to define default queue and connection routing rules for specific jobs in a central place.
Define it once in a Service Provider, and you’re done:
// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Queue;
use App\Jobs\ProcessPayment;
use App\Jobs\SendInvoiceEmail;
use App\Jobs\GenerateReport;
public function boot(): void
{
// Central routing config — one place, one source of truth
Queue::route(ProcessPayment::class, connection: 'redis', queue: 'payments-high');
Queue::route(SendInvoiceEmail::class, connection: 'redis', queue: 'emails');
Queue::route(GenerateReport::class, connection: 'database', queue: 'reports-low');
}
// In any controller, anywhere in the app
ProcessPayment::dispatch($order); // Always routes to redis / payments-high
SendInvoiceEmail::dispatch($user); // Always routes to redis / emails
GenerateReport::dispatch($params); // Always routes to database / reports-low
Why This Matters at Scale
// You can also use Queue::route() with attributes for extra clarity (Laravel 13)
#[OnQueue('payments-high')]
#[OnConnection('redis')]
class ProcessPayment implements ShouldQueue
{
// Still works! Queue::route() takes precedence when both are defined.
}
For managers
Database Enhancements
What Changed
// Old approach — raw SQL, no type safety, harder to test
DB::statement("
DELETE orders
FROM orders
JOIN customers ON orders.customer_id = customers.id
WHERE customers.status = 'deleted'
ORDER BY orders.created_at ASC
LIMIT 1000
");
// New approach — Laravel 13 query builder
DB::table('orders')
->join('customers', 'orders.customer_id', '=', 'customers.id')
->where('customers.status', 'deleted')
->orderBy('orders.created_at', 'asc')
->limit(1000)
->delete();
Practical Use Cases
// 1. Clean up orphaned records in batches
DB::table('sessions')
->join('users', 'sessions.user_id', '=', 'users.id')
->where('users.deleted_at', '!=', null)
->orderBy('sessions.last_activity', 'asc')
->limit(500)
->delete();
// 2. Remove expired notifications for inactive accounts
DB::table('notifications')
->join('users', 'notifications.notifiable_id', '=', 'users.id')
->where('users.is_active', false)
->where('notifications.created_at', '<', now()->subDays(30))
->orderBy('notifications.created_at')
->limit(1000)
->delete();
A Real Middleware Stack
// DashboardController.php
public function show(Project $project): JsonResponse
{
$cacheKey = 'project_summary:' . $project->id;
// Cache::touch() — slide the expiry window on every read
if (!Cache::touch($cacheKey, now()->addMinutes(10))) {
$summary = $this->buildProjectSummary($project);
Cache::put($cacheKey, $summary, now()->addMinutes(10));
}
return response()->json(Cache::get($cacheKey));
}
// AppServiceProvider.php
Queue::route(RegenerateProjectSummary::class, queue: 'high-priority');
Queue::route(CleanupArchivedProjects::class, queue: 'maintenance');
// CleanupArchivedProjects.php
public function handle(): void
{
// DELETE...JOIN to remove stale data efficiently
DB::table('tasks')
->join('projects', 'tasks.project_id', '=', 'projects.id')
->where('projects.archived_at', '<', now()->subMonths(6))
->orderBy('tasks.created_at')
->limit(2000)
->delete();
}
Explore project snapshots or discuss custom web solutions.
Simplicity is prerequisite for reliability.
Thank You for Spending Your Valuable Time
I truly appreciate you taking the time to read blog. Your valuable time means a lot to me, and I hope you found the content insightful and engaging!
Frequently Asked Questions
Yes. You may provide a `DateTimeInterface`, `DateInterval`, or Carbon instance to specify an exact expiration time. It works with Redis, Memcached, and the database cache driver. The underlying command varies by driver (Redis uses `EXPIRE`, Memcached uses `TOUCH`, database uses `UPDATE`) but the Laravel API is identical.
It returns `false`. This is your signal to regenerate the cached data. Build your caching layer to handle this gracefully — always check the return value when using `touch()` for sliding expiry.
Yes. `Queue::route()` sets the *default* routing. You can still override it at the dispatch call with `->onQueue()` or `->onConnection()` when you need an exception to the rule.
Use the `limit()` clause to batch deletions. Deleting millions of rows in one query locks tables. The recommended pattern for large cleanups is a scheduled job that runs in batches with a `limit(1000)` or similar cap.
Learn core Laravel routing, Eloquent, and controllers first. But once you start building real apps — even small ones — `Cache::touch()` and `Queue::route()` become immediately useful. They're not advanced topics; they're just cleaner ways to write patterns you'll encounter in your first production app. The official docs at laravel.com/docs/13.x/cache are the best starting point.
Comments are closed