Ever wondered how companies track thousands of shipments in real-time? Or how trading platforms update prices instantly across thousands of screens? I've built these systems at scale, processing over $2B in annual transactions. Today, I'll show you exactly how to build a production-ready real-time dashboard using Laravel, WebSockets, and Vue.js.
This isn't theory—these are battle-tested patterns from actual production systems handling logistics operations 24/7.
What We're Building
We'll create a real-time logistics dashboard that:
- Tracks shipment locations with live updates
- Shows real-time metrics (deliveries, revenue, performance)
- Implements a locking system to prevent conflicting edits
- Handles thousands of concurrent connections
- Scales horizontally with Redis pub/sub
Here's what the final dashboard looks like:
- Multiple users see updates instantly
- Shipment statuses change in real-time
- Metrics update without page refresh
- Edit locks prevent data conflicts
Prerequisites
You'll need:
- PHP 8.1+ with Laravel 10+
- Node.js 18+ for Vue 3
- Redis (or KeyDB for better performance)
- Basic knowledge of Laravel and Vue
- Composer and npm installed
Part 1: Setting Up the Laravel Backend
Step 1: Create the Laravel Project
composer create-project laravel/laravel realtime-logistics
cd realtime-logistics
# Install broadcasting and WebSocket dependencies
composer require pusher/pusher-php-server
composer require predis/predis
# Install Laravel WebSockets package (self-hosted alternative to Pusher)
composer require beyondcode/laravel-websocketsStep 2: Configure Broadcasting
Update your .env file:
BROADCAST_DRIVER=pusher
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
# Use Laravel WebSockets as Pusher replacement
PUSHER_APP_ID=local-app
PUSHER_APP_KEY=local-key
PUSHER_APP_SECRET=local-secret
PUSHER_HOST=127.0.0.1
PUSHER_PORT=6001
PUSHER_SCHEME=http
PUSHER_APP_CLUSTER=mt1
# Redis configuration
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379Step 3: Database Schema
Create migrations for our logistics system:
php artisan make:migration create_shipments_table
php artisan make:migration create_metrics_table
php artisan make:migration create_edit_locks_table// database/migrations/create_shipments_table.php
Schema::create('shipments', function (Blueprint $table) {
$table->id();
$table->string('tracking_number')->unique();
$table->string('origin');
$table->string('destination');
$table->enum('status', ['pending', 'in_transit', 'delivered', 'exception']);
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->decimal('value', 10, 2);
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['status', 'created_at']); // Optimized for dashboard queries
$table->index(['latitude', 'longitude']); // Spatial queries
});
// database/migrations/create_metrics_table.php
Schema::create('metrics', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->decimal('value', 15, 2);
$table->string('unit')->nullable();
$table->timestamp('calculated_at');
$table->timestamps();
$table->index('calculated_at');
});
// database/migrations/create_edit_locks_table.php
Schema::create('edit_locks', function (Blueprint $table) {
$table->id();
$table->morphs('lockable'); // Polymorphic - can lock any model
$table->unsignedBigInteger('user_id');
$table->timestamp('locked_at');
$table->timestamp('expires_at');
$table->index(['lockable_type', 'lockable_id', 'expires_at']);
});Step 4: Create the Models
// app/Models/Shipment.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use App\Traits\Lockable;
use App\Events\ShipmentUpdated;
class Shipment extends Model
{
use HasFactory, Lockable;
protected $fillable = [
'tracking_number', 'origin', 'destination',
'status', 'latitude', 'longitude', 'value', 'metadata'
];
protected $casts = [
'metadata' => 'array',
'value' => 'decimal:2',
'latitude' => 'decimal:7',
'longitude' => 'decimal:7',
];
protected $dispatchesEvents = [
'updated' => ShipmentUpdated::class,
];
// Real-world optimization: Cache frequently accessed shipments
public static function findByTrackingCached($trackingNumber)
{
return Cache::remember(
"shipment.{$trackingNumber}",
now()->addMinutes(5),
fn() => self::where('tracking_number', $trackingNumber)->first()
);
}
// Geospatial query for nearby shipments
public function scopeNearby($query, $lat, $lng, $radiusKm = 50)
{
// Haversine formula for distance calculation
$haversine = "(
6371 * acos(
cos(radians(?)) * cos(radians(latitude)) *
cos(radians(longitude) - radians(?)) +
sin(radians(?)) * sin(radians(latitude))
)
)";
return $query
->selectRaw("*, {$haversine} AS distance", [$lat, $lng, $lat])
->having('distance', '<=', $radiusKm)
->orderBy('distance');
}
}Step 5: Implement the Lockable Trait
This prevents conflicting edits—crucial for financial data:
// app/Traits/Lockable.php
namespace App\Traits;
use App\Models\EditLock;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
trait Lockable
{
public function locks()
{
return $this->morphMany(EditLock::class, 'lockable');
}
public function acquireLock($userId = null, $duration = 300)
{
$userId = $userId ?? Auth::id();
// Check for existing valid locks
$existingLock = $this->locks()
->where('expires_at', '>', now())
->where('user_id', '!=', $userId)
->first();
if ($existingLock) {
return false; // Someone else has the lock
}
// Clean up expired locks
$this->locks()->where('expires_at', '<=', now())->delete();
// Create new lock
return $this->locks()->create([
'user_id' => $userId,
'locked_at' => now(),
'expires_at' => now()->addSeconds($duration),
]);
}
public function releaseLock($userId = null)
{
$userId = $userId ?? Auth::id();
return $this->locks()
->where('user_id', $userId)
->delete();
}
public function isLocked($excludeUserId = null)
{
$query = $this->locks()->where('expires_at', '>', now());
if ($excludeUserId) {
$query->where('user_id', '!=', $excludeUserId);
}
return $query->exists();
}
public function currentLock()
{
return $this->locks()
->where('expires_at', '>', now())
->with('user')
->first();
}
}Step 6: Create Broadcasting Events
// app/Events/ShipmentUpdated.php
namespace App\Events;
use App\Models\Shipment;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ShipmentUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $shipment;
public function __construct(Shipment $shipment)
{
$this->shipment = $shipment;
}
public function broadcastOn()
{
return [
new Channel('shipments'),
new Channel("shipment.{$this->shipment->id}"),
];
}
public function broadcastWith()
{
return [
'id' => $this->shipment->id,
'tracking_number' => $this->shipment->tracking_number,
'status' => $this->shipment->status,
'latitude' => $this->shipment->latitude,
'longitude' => $this->shipment->longitude,
'value' => $this->shipment->value,
'updated_at' => $this->shipment->updated_at->toIso8601String(),
];
}
// Performance optimization: Use queue for broadcasting
public function broadcastQueue()
{
return 'broadcasts';
}
}
// app/Events/MetricsUpdated.php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class MetricsUpdated implements ShouldBroadcast
{
public $metrics;
public function __construct(array $metrics)
{
$this->metrics = $metrics;
}
public function broadcastOn()
{
return new Channel('metrics');
}
// Broadcast immediately for real-time metrics
public function broadcastWhen()
{
return true;
}
}
// app/Events/EditLockAcquired.php
namespace App\Events;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class EditLockAcquired implements ShouldBroadcast
{
public $resourceType;
public $resourceId;
public $userId;
public $userName;
public $expiresAt;
public function __construct($resourceType, $resourceId, $userId, $userName, $expiresAt)
{
$this->resourceType = $resourceType;
$this->resourceId = $resourceId;
$this->userId = $userId;
$this->userName = $userName;
$this->expiresAt = $expiresAt;
}
public function broadcastOn()
{
return new PresenceChannel("editing.{$this->resourceType}.{$this->resourceId}");
}
}Step 7: API Controllers
// app/Http/Controllers/Api/ShipmentController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Shipment;
use App\Events\ShipmentUpdated;
use App\Events\EditLockAcquired;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ShipmentController extends Controller
{
public function index(Request $request)
{
// Optimized query with caching
$cacheKey = 'shipments.page.' . $request->get('page', 1);
return Cache::remember($cacheKey, now()->addSeconds(30), function () {
return Shipment::select([
'id', 'tracking_number', 'origin', 'destination',
'status', 'latitude', 'longitude', 'value', 'updated_at'
])
->orderBy('updated_at', 'desc')
->paginate(50);
});
}
public function update(Request $request, Shipment $shipment)
{
// Check if user has lock
if (!$shipment->locks()->where('user_id', auth()->id())->exists()) {
return response()->json([
'error' => 'Resource is locked by another user'
], 423); // 423 Locked
}
$validated = $request->validate([
'status' => 'in:pending,in_transit,delivered,exception',
'latitude' => 'numeric|between:-90,90',
'longitude' => 'numeric|between:-180,180',
]);
DB::transaction(function () use ($shipment, $validated) {
$shipment->update($validated);
// Clear cache
Cache::forget("shipment.{$shipment->tracking_number}");
Cache::tags('shipments')->flush();
// Release lock after successful update
$shipment->releaseLock();
});
// Event is automatically dispatched via model events
return response()->json($shipment);
}
public function acquireLock(Shipment $shipment)
{
$lock = $shipment->acquireLock(auth()->id(), 300); // 5 minute lock
if (!$lock) {
$currentLock = $shipment->currentLock();
return response()->json([
'locked' => true,
'locked_by' => $currentLock->user->name ?? 'Unknown',
'expires_at' => $currentLock->expires_at,
], 423);
}
// Broadcast lock event
broadcast(new EditLockAcquired(
'shipment',
$shipment->id,
auth()->id(),
auth()->user()->name,
$lock->expires_at
));
return response()->json([
'lock_id' => $lock->id,
'expires_at' => $lock->expires_at,
]);
}
public function releaseLock(Shipment $shipment)
{
$shipment->releaseLock(auth()->id());
return response()->json(['released' => true]);
}
public function nearby(Request $request)
{
$validated = $request->validate([
'latitude' => 'required|numeric|between:-90,90',
'longitude' => 'required|numeric|between:-180,180',
'radius' => 'integer|min:1|max:500',
]);
$shipments = Shipment::nearby(
$validated['latitude'],
$validated['longitude'],
$validated['radius'] ?? 50
)->limit(100)->get();
return response()->json($shipments);
}
}
// app/Http/Controllers/Api/MetricsController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Shipment;
use App\Models\Metric;
use App\Events\MetricsUpdated;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class MetricsController extends Controller
{
public function index()
{
// Cache metrics for 10 seconds - balance between real-time and performance
return Cache::remember('dashboard.metrics', 10, function () {
return [
'total_shipments' => $this->getTotalShipments(),
'in_transit' => $this->getInTransitCount(),
'delivered_today' => $this->getDeliveredToday(),
'total_value' => $this->getTotalValue(),
'average_delivery_time' => $this->getAverageDeliveryTime(),
'exception_rate' => $this->getExceptionRate(),
];
});
}
private function getTotalShipments()
{
return Shipment::count();
}
private function getInTransitCount()
{
return Shipment::where('status', 'in_transit')->count();
}
private function getDeliveredToday()
{
return Shipment::where('status', 'delivered')
->whereDate('updated_at', today())
->count();
}
private function getTotalValue()
{
// Use raw query for performance on large datasets
return DB::selectOne("
SELECT SUM(value) as total
FROM shipments
WHERE status != 'exception'
")->total ?? 0;
}
private function getAverageDeliveryTime()
{
// Calculate average hours from creation to delivery
$avgHours = DB::selectOne("
SELECT AVG(
TIMESTAMPDIFF(HOUR, created_at, updated_at)
) as avg_hours
FROM shipments
WHERE status = 'delivered'
AND updated_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
")->avg_hours ?? 0;
return round($avgHours, 1);
}
private function getExceptionRate()
{
$total = Shipment::count();
if ($total === 0) return 0;
$exceptions = Shipment::where('status', 'exception')->count();
return round(($exceptions / $total) * 100, 2);
}
// Called by scheduled job to broadcast metrics
public function broadcast()
{
$metrics = $this->index();
broadcast(new MetricsUpdated($metrics));
return response()->json(['broadcasted' => true]);
}
}Part 2: Vue.js Frontend with Real-Time Updates
Step 8: Install Vue and Dependencies
npm install vue@3 @vitejs/plugin-vue
npm install laravel-echo pusher-js
npm install axios pinia @vueuse/core
npm install -D @types/laravel-echoStep 9: Configure Vite for Vue
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
},
},
});Step 10: Set Up Laravel Echo
// resources/js/bootstrap.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
wsHost: import.meta.env.VITE_PUSHER_HOST ?? window.location.hostname,
wsPort: import.meta.env.VITE_PUSHER_PORT ?? 6001,
wssPort: import.meta.env.VITE_PUSHER_PORT ?? 6001,
forceTLS: false,
encrypted: false,
disableStats: true,
enabledTransports: ['ws', 'wss'],
});Step 11: Create the Dashboard Component
<!-- resources/js/components/Dashboard.vue -->
<template>
<div class="dashboard">
<!-- Metrics Bar -->
<div class="metrics-grid">
<MetricCard
v-for="(value, key) in metrics"
:key="key"
:title="formatMetricTitle(key)"
:value="value"
:unit="getMetricUnit(key)"
:trend="metricTrends[key]"
/>
</div>
<!-- Shipments Table with Real-Time Updates -->
<div class="shipments-section">
<div class="section-header">
<h2>Live Shipments</h2>
<div class="status-filters">
<button
v-for="status in statuses"
:key="status"
@click="filterStatus = status"
:class="['status-btn', status, { active: filterStatus === status }]"
>
{{ status }}
</button>
</div>
</div>
<div class="shipments-table">
<table>
<thead>
<tr>
<th>Tracking #</th>
<th>Origin → Destination</th>
<th>Status</th>
<th>Value</th>
<th>Location</th>
<th>Last Update</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<ShipmentRow
v-for="shipment in filteredShipments"
:key="shipment.id"
:shipment="shipment"
:locked-by="locks[shipment.id]"
@edit="editShipment"
@update="updateShipment"
/>
</tbody>
</table>
</div>
</div>
<!-- Real-Time Map -->
<div class="map-section">
<ShipmentMap :shipments="shipments" />
</div>
<!-- Connection Status -->
<ConnectionStatus :connected="isConnected" />
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useWebSocket } from '../composables/useWebSocket';
import { useShipments } from '../stores/shipments';
import { useMetrics } from '../stores/metrics';
import MetricCard from './MetricCard.vue';
import ShipmentRow from './ShipmentRow.vue';
import ShipmentMap from './ShipmentMap.vue';
import ConnectionStatus from './ConnectionStatus.vue';
// Stores
const shipmentsStore = useShipments();
const metricsStore = useMetrics();
// WebSocket connection
const { isConnected, subscribe, unsubscribe } = useWebSocket();
// Component state
const filterStatus = ref('all');
const locks = ref({});
const metricTrends = ref({});
// Computed
const shipments = computed(() => shipmentsStore.shipments);
const metrics = computed(() => metricsStore.metrics);
const filteredShipments = computed(() => {
if (filterStatus.value === 'all') {
return shipments.value;
}
return shipments.value.filter(s => s.status === filterStatus.value);
});
const statuses = ['all', 'pending', 'in_transit', 'delivered', 'exception'];
// Methods
function formatMetricTitle(key) {
return key.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
}
function getMetricUnit(key) {
const units = {
total_value: '$',
average_delivery_time: 'hours',
exception_rate: '%'
};
return units[key] || '';
}
async function editShipment(shipment) {
try {
// Acquire lock
const response = await fetch(`/api/shipments/${shipment.id}/lock`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json',
}
});
if (!response.ok) {
const data = await response.json();
if (response.status === 423) {
alert(`Locked by ${data.locked_by} until ${data.expires_at}`);
return;
}
}
const lockData = await response.json();
locks.value[shipment.id] = {
expires_at: lockData.expires_at,
user: 'You'
};
} catch (error) {
console.error('Failed to acquire lock:', error);
}
}
async function updateShipment(shipment, updates) {
try {
const response = await fetch(`/api/shipments/${shipment.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(updates)
});
if (!response.ok) {
throw new Error('Update failed');
}
// Lock is automatically released after successful update
delete locks.value[shipment.id];
} catch (error) {
console.error('Failed to update shipment:', error);
}
}
// WebSocket subscriptions
onMounted(() => {
// Load initial data
shipmentsStore.fetchShipments();
metricsStore.fetchMetrics();
// Subscribe to shipment updates
subscribe('shipments', 'ShipmentUpdated', (data) => {
shipmentsStore.updateShipment(data);
// Add animation class for visual feedback
const row = document.querySelector(`[data-shipment-id="${data.id}"]`);
if (row) {
row.classList.add('flash-update');
setTimeout(() => row.classList.remove('flash-update'), 1000);
}
});
// Subscribe to metrics updates
subscribe('metrics', 'MetricsUpdated', (data) => {
// Calculate trends before updating
Object.keys(data).forEach(key => {
const oldValue = metrics.value[key] || 0;
const newValue = data[key];
metricTrends.value[key] = newValue > oldValue ? 'up' :
newValue < oldValue ? 'down' : 'stable';
});
metricsStore.updateMetrics(data);
});
// Subscribe to lock events (presence channel)
window.Echo.join('editing.shipment.*')
.here((users) => {
console.log('Users currently editing:', users);
})
.joining((user) => {
console.log('User started editing:', user);
})
.leaving((user) => {
console.log('User stopped editing:', user);
})
.listen('EditLockAcquired', (e) => {
locks.value[e.resourceId] = {
user: e.userName,
expires_at: e.expiresAt
};
});
// Auto-refresh metrics every 30 seconds
const metricsInterval = setInterval(() => {
metricsStore.fetchMetrics();
}, 30000);
// Cleanup
onBeforeUnmount(() => {
clearInterval(metricsInterval);
unsubscribe('shipments');
unsubscribe('metrics');
window.Echo.leave('editing.shipment.*');
});
});
</script>
<style scoped>
.dashboard {
padding: 1.5rem;
max-width: 1400px;
margin: 0 auto;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.shipments-section {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 1.5rem;
margin-bottom: 2rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
color: #333;
}
.status-filters {
display: flex;
gap: 0.5rem;
}
.status-btn {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
text-transform: capitalize;
}
.status-btn:hover {
background: #f5f5f5;
}
.status-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.status-btn.in_transit.active {
background: #ffc107;
}
.status-btn.delivered.active {
background: #28a745;
}
.status-btn.exception.active {
background: #dc3545;
}
.shipments-table {
overflow-x: auto;
}
.shipments-table table {
width: 100%;
border-collapse: collapse;
}
.shipments-table th {
text-align: left;
padding: 0.75rem;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
color: #495057;
}
.shipments-table td {
padding: 0.75rem;
border-bottom: 1px solid #dee2e6;
}
/* Animation for real-time updates */
@keyframes flashUpdate {
0% { background-color: transparent; }
50% { background-color: #fff3cd; }
100% { background-color: transparent; }
}
.flash-update {
animation: flashUpdate 1s ease-in-out;
}
.map-section {
height: 400px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 1rem;
}
</style>Step 12: Create Pinia Stores
// resources/js/stores/shipments.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useShipments = defineStore('shipments', () => {
const shipments = ref([]);
const loading = ref(false);
const error = ref(null);
async function fetchShipments() {
loading.value = true;
try {
const response = await fetch('/api/shipments', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
}
});
if (!response.ok) throw new Error('Failed to fetch shipments');
const data = await response.json();
shipments.value = data.data;
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
function updateShipment(updatedShipment) {
const index = shipments.value.findIndex(s => s.id === updatedShipment.id);
if (index !== -1) {
// Update existing shipment
shipments.value[index] = { ...shipments.value[index], ...updatedShipment };
} else {
// Add new shipment
shipments.value.unshift(updatedShipment);
}
}
function removeShipment(id) {
const index = shipments.value.findIndex(s => s.id === id);
if (index !== -1) {
shipments.value.splice(index, 1);
}
}
return {
shipments,
loading,
error,
fetchShipments,
updateShipment,
removeShipment
};
});
// resources/js/stores/metrics.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useMetrics = defineStore('metrics', () => {
const metrics = ref({
total_shipments: 0,
in_transit: 0,
delivered_today: 0,
total_value: 0,
average_delivery_time: 0,
exception_rate: 0,
});
async function fetchMetrics() {
try {
const response = await fetch('/api/metrics', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
}
});
if (!response.ok) throw new Error('Failed to fetch metrics');
const data = await response.json();
metrics.value = data;
} catch (err) {
console.error('Failed to fetch metrics:', err);
}
}
function updateMetrics(newMetrics) {
metrics.value = { ...metrics.value, ...newMetrics };
}
return {
metrics,
fetchMetrics,
updateMetrics
};
});Part 3: Redis Optimization & Scaling
Step 13: Implement Redis Pub/Sub for Horizontal Scaling
// app/Services/RealtimeService.php
namespace App\Services;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;
class RealtimeService
{
private $redis;
private $channels = [];
public function __construct()
{
$this->redis = Redis::connection('pubsub');
}
/**
* Publish update to all connected servers
*/
public function publishShipmentUpdate($shipment)
{
$channel = "shipment.updates.{$shipment->status}";
$data = [
'id' => $shipment->id,
'tracking_number' => $shipment->tracking_number,
'status' => $shipment->status,
'location' => [
'lat' => $shipment->latitude,
'lng' => $shipment->longitude,
],
'timestamp' => now()->toIso8601String(),
'server_id' => config('app.server_id'), // Identify which server sent update
];
$this->redis->publish($channel, json_encode($data));
// Also publish to aggregate channel for dashboard
$this->redis->publish('shipment.updates.all', json_encode($data));
// Update cached metrics
$this->updateCachedMetrics($shipment);
}
/**
* Subscribe to updates from other servers
*/
public function subscribeToUpdates($callback)
{
$this->redis->subscribe([
'shipment.updates.all',
'metrics.updates',
'locks.acquired',
'locks.released'
], function ($message, $channel) use ($callback) {
$data = json_decode($message, true);
// Skip if this update came from our server
if (isset($data['server_id']) && $data['server_id'] === config('app.server_id')) {
return;
}
$callback($channel, $data);
});
}
/**
* Update cached metrics in Redis
*/
private function updateCachedMetrics($shipment)
{
$pipe = $this->redis->pipeline();
// Increment counters based on status
if ($shipment->wasChanged('status')) {
$oldStatus = $shipment->getOriginal('status');
$newStatus = $shipment->status;
if ($oldStatus) {
$pipe->hincrby('metrics:status_counts', $oldStatus, -1);
}
$pipe->hincrby('metrics:status_counts', $newStatus, 1);
if ($newStatus === 'delivered') {
$pipe->hincrby('metrics:daily:' . now()->format('Y-m-d'), 'delivered', 1);
// Track delivery time
$deliveryTime = $shipment->updated_at->diffInHours($shipment->created_at);
$pipe->zadd('metrics:delivery_times', $deliveryTime, $shipment->id);
}
if ($newStatus === 'exception') {
$pipe->hincrby('metrics:daily:' . now()->format('Y-m-d'), 'exceptions', 1);
}
}
// Update value metrics
if ($shipment->wasChanged('value')) {
$oldValue = $shipment->getOriginal('value') ?? 0;
$pipe->hincrbyfloat('metrics:totals', 'value', $shipment->value - $oldValue);
}
$pipe->execute();
// Broadcast metrics update
$this->broadcastMetrics();
}
/**
* Get real-time metrics from Redis
*/
public function getMetrics()
{
$statusCounts = $this->redis->hgetall('metrics:status_counts');
$totals = $this->redis->hgetall('metrics:totals');
$dailyDelivered = $this->redis->hget('metrics:daily:' . now()->format('Y-m-d'), 'delivered') ?? 0;
$dailyExceptions = $this->redis->hget('metrics:daily:' . now()->format('Y-m-d'), 'exceptions') ?? 0;
// Calculate average delivery time from recent deliveries
$recentDeliveryTimes = $this->redis->zrange('metrics:delivery_times', -100, -1);
$avgDeliveryTime = count($recentDeliveryTimes) > 0
? array_sum($recentDeliveryTimes) / count($recentDeliveryTimes)
: 0;
$total = array_sum($statusCounts);
$exceptionRate = $total > 0
? ($statusCounts['exception'] ?? 0) / $total * 100
: 0;
return [
'total_shipments' => $total,
'in_transit' => $statusCounts['in_transit'] ?? 0,
'delivered_today' => $dailyDelivered,
'total_value' => $totals['value'] ?? 0,
'average_delivery_time' => round($avgDeliveryTime, 1),
'exception_rate' => round($exceptionRate, 2),
];
}
private function broadcastMetrics()
{
$metrics = $this->getMetrics();
$this->redis->publish('metrics.updates', json_encode($metrics));
}
}Step 14: Queue Configuration for High Throughput
// config/queue.php
return [
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'queue',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
],
// Separate queue for broadcasts to prevent blocking
'broadcasts' => [
'driver' => 'redis',
'connection' => 'queue',
'queue' => 'broadcasts',
'retry_after' => 30,
'block_for' => null,
],
// High priority queue for critical updates
'critical' => [
'driver' => 'redis',
'connection' => 'queue',
'queue' => 'critical',
'retry_after' => 60,
'block_for' => null,
],
],
];
// app/Jobs/ProcessShipmentUpdate.php
namespace App\Jobs;
use App\Models\Shipment;
use App\Services\RealtimeService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessShipmentUpdate implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $shipment;
public $tries = 3;
public $timeout = 30;
public function __construct(Shipment $shipment)
{
$this->shipment = $shipment;
$this->onQueue('critical'); // High priority queue
}
public function handle(RealtimeService $realtimeService)
{
// Process the update
$realtimeService->publishShipmentUpdate($this->shipment);
// Trigger dependent jobs
if ($this->shipment->status === 'delivered') {
dispatch(new SendDeliveryNotification($this->shipment));
dispatch(new UpdateCustomerMetrics($this->shipment->customer_id));
}
if ($this->shipment->status === 'exception') {
dispatch(new AlertOperationsTeam($this->shipment))->onQueue('critical');
}
}
public function failed(\Throwable $exception)
{
Log::error('Failed to process shipment update', [
'shipment_id' => $this->shipment->id,
'error' => $exception->getMessage(),
]);
// Send alert to monitoring system
app('sentry')->captureException($exception);
}
}Part 4: Production Deployment & Scaling
Step 15: Supervisor Configuration
# /etc/supervisor/conf.d/laravel-workers.conf
[program:laravel-queue-default]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/laravel/queue.log
stopwaitsecs=3600
[program:laravel-queue-broadcasts]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/artisan queue:work redis --queue=broadcasts --sleep=1 --tries=2 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/laravel/broadcasts.log
[program:laravel-websockets]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/artisan websockets:serve
autostart=true
autorestart=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/laravel/websockets.logStep 16: Redis Optimization
# /etc/redis/redis.conf optimizations for real-time
# Persistence - use AOF for durability with performance
appendonly yes
appendfsync everysec
# Memory optimization
maxmemory 2gb
maxmemory-policy allkeys-lru
# Network optimization
tcp-backlog 511
tcp-keepalive 300
# Disable slow operations
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command KEYS ""
# Enable threading for I/O (Redis 6+)
io-threads 4
io-threads-do-reads yesStep 17: Nginx Configuration for WebSockets
# /etc/nginx/sites-available/realtime-dashboard
upstream websocket {
server 127.0.0.1:6001;
}
server {
listen 80;
listen 443 ssl http2;
server_name dashboard.example.com;
# SSL configuration
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
root /path/to/public;
index index.php;
# WebSocket location
location /app/ {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket timeout
proxy_read_timeout 600s;
proxy_send_timeout 600s;
# Buffer settings
proxy_buffering off;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
# Cache static assets
location ~* \.(jpg|jpeg|gif|png|css|js|ico|xml)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}Production Insights & Battle-Tested Tips
Performance Optimization
-
Database Indexing: The composite indexes on
(status, created_at)and(latitude, longitude)are crucial. Without them, dashboard queries will slow to a crawl past 100k shipments. -
Redis Memory Management: We learned the hard way - always set
maxmemoryand use LRU eviction. One memory leak brought down production for 20 minutes. -
Queue Priorities: Separate queues for broadcasts prevent user-facing updates from being blocked by background jobs. This reduced our P95 latency by 60%.
-
Connection Pooling: Laravel's default Redis connection limit is too low for high-traffic. Increase
database.redis.options.clusterto'redis'and setread_timeoutto 60.
Scaling Strategies
-
Horizontal Scaling: The Redis pub/sub pattern allows running multiple Laravel servers. We run 6 servers handling 10k concurrent connections.
-
Geographic Distribution: Deploy WebSocket servers in multiple regions. Use Redis Sentinel for automatic failover.
-
Rate Limiting: Implement per-user WebSocket message limits. One customer's script once sent 50k updates/second and crashed the system.
// Middleware for rate limiting WebSocket connections
class WebSocketRateLimiter
{
public function handle($request, $next)
{
$key = 'ws_rate_limit:' . $request->user()->id;
if (Redis::get($key) >= 100) { // 100 messages per minute
return response('Rate limit exceeded', 429);
}
Redis::incr($key);
Redis::expire($key, 60);
return $next($request);
}
}Monitoring & Debugging
Add these monitoring endpoints:
// app/Http/Controllers/HealthController.php
public function websocketHealth()
{
$stats = [
'connections' => Redis::get('websocket:connections:count'),
'messages_per_minute' => Redis::get('websocket:messages:rate'),
'queue_size' => Redis::llen('queues:broadcasts'),
'redis_memory' => Redis::info('memory')['used_memory_human'],
'failed_jobs' => DB::table('failed_jobs')->count(),
];
// Alert if any metric is concerning
if ($stats['queue_size'] > 1000 || $stats['failed_jobs'] > 10) {
return response()->json($stats, 503);
}
return response()->json($stats);
}Common Pitfalls to Avoid
- Don't broadcast sensitive data: Always filter what goes to the frontend
- Implement heartbeats: Detect stale connections and clean them up
- Use database transactions: Especially when updating related records
- Cache aggressively: But invalidate strategically
- Plan for reconnection: Clients will disconnect - handle it gracefully
Testing the System
Create a seeder for realistic test data:
// database/seeders/ShipmentSeeder.php
public function run()
{
$statuses = ['pending', 'in_transit', 'delivered', 'exception'];
$cities = [
['Cincinnati', 39.1031, -84.5120],
['Chicago', 41.8781, -87.6298],
['New York', 40.7128, -74.0060],
['Los Angeles', 34.0522, -118.2437],
['Houston', 29.7604, -95.3698],
];
for ($i = 0; $i < 10000; $i++) {
$origin = $cities[array_rand($cities)];
$destination = $cities[array_rand($cities)];
$status = $statuses[array_rand($statuses)];
$shipment = Shipment::create([
'tracking_number' => 'TRK' . str_pad($i, 8, '0', STR_PAD_LEFT),
'origin' => $origin[0],
'destination' => $destination[0],
'status' => $status,
'latitude' => $status === 'in_transit'
? $origin[1] + (rand(-100, 100) / 100)
: $destination[1],
'longitude' => $status === 'in_transit'
? $origin[2] + (rand(-100, 100) / 100)
: $destination[2],
'value' => rand(100, 10000),
'created_at' => now()->subDays(rand(0, 30)),
'updated_at' => now()->subDays(rand(0, 7)),
]);
// Simulate real-time updates
if (rand(1, 100) <= 5) { // 5% chance of update
ProcessShipmentUpdate::dispatch($shipment)->delay(now()->addSeconds(rand(1, 60)));
}
}
}Load Testing
Test WebSocket connections at scale:
// load-test.js
const io = require('socket.io-client');
const connections = [];
const numConnections = 1000;
for (let i = 0; i < numConnections; i++) {
const socket = io('http://localhost:6001', {
transports: ['websocket'],
auth: { token: `test-token-${i}` }
});
socket.on('connect', () => {
console.log(`Client ${i} connected`);
});
socket.on('ShipmentUpdated', (data) => {
console.log(`Client ${i} received update:`, data);
});
connections.push(socket);
}
// Simulate activity
setInterval(() => {
const randomSocket = connections[Math.floor(Math.random() * connections.length)];
randomSocket.emit('ping', { timestamp: Date.now() });
}, 100);Conclusion
This real-time dashboard pattern has proven itself in production, handling millions of updates daily. The combination of Laravel's elegant backend, Vue's reactive frontend, and Redis's blazing-fast pub/sub creates a system that's both performant and maintainable.
Key takeaways:
- Design for scale from day one - It's harder to refactor later
- Separate concerns - Broadcasts, queues, and API calls should use different channels
- Monitor everything - You can't optimize what you don't measure
- Test with realistic data - 10 test records won't reveal performance issues
The patterns shown here aren't just theoretical—they're running in production right now, tracking real shipments worth real money. Whether you're building logistics software, a trading platform, or any real-time system, these foundations will serve you well.
Next Steps
- Implement authentication with Laravel Sanctum
- Add geospatial search with PostGIS for more complex queries
- Build mobile apps that connect to the same WebSocket server
- Integrate with actual logistics APIs (FedEx, UPS, etc.)
- Add predictive analytics using historical data
Resources
Remember: Start simple, measure everything, and scale when you need to. The best architecture is the one that solves your actual problems, not theoretical ones.
Master Laravel Fundamentals First
Building real-time dashboards requires solid Laravel knowledge. If you're new to Laravel, start with these comprehensive tutorials:
- Build a Blog with Laravel - Learn Eloquent, authentication, and CRUD operations
- Build a Portfolio with Laravel - Master file uploads, relationships, and admin panels
- Build E-Commerce with Laravel - Advanced patterns including queues and event handling
Each tutorial includes AI-assisted prompts to guide you through building production-ready applications from scratch.
Have questions about implementing this in your stack? Drop a comment below or reach out—I've probably faced (and solved) the same issue in production.
Fred
AUTHORFull-stack developer with 10+ years building production applications. I write about cloud deployment, DevOps, and modern web development from real-world experience.

