In this post, Iβll walk you through how I built a real-time traffic visualization system that displays live user interactions across the globe. We'll cover the backend setup with Laravel and Redis, and the frontend implementation using Vue.js, MapLibre GL, and Deck.gl.

Demo of the Interactive traffic map we will be building
The goal was to create a system that captures and visualizes real-time traffic data from my applications worldwide. Each user interaction would be represented as a connection between the client and server locations on a global map.
Technologies Used:
In high-traffic scenarios, multiple processes might attempt to access or modify shared resources simultaneously, leading to race conditions. This can result in inconsistent data or system crashes.
To prevent race conditions, I utilized Laravel's "Cache::lock()" method, which provides atomic locks using Redis. This ensures that only one process can execute a critical section of code at a time.
Cache::lock('traffic_batch_lock', 10)->block(5, function () {
// Critical section code
});In this example, the lock named "traffic_batch_lock" is acquired for 10 seconds. If the lock is already held, the process will wait up to 5 seconds to acquire it.
Instead of broadcasting each traffic event individually, I implemented a batching mechanism. This collects multiple traffic events and broadcasts them together, reducing the load on the broadcasting system.
Middleware Implementation:
public function handle(Request $request, Closure $next): Response
{
if (!app()->runningInConsole()) {
$this->batchingService->queueEvent();
$this->batchingService->processBatch();
}
return $next($request);
}In this middleware, each incoming request queues its traffic data, and the "processBatch" method handles broadcasting the batched events.
Event Batching Service Implementation:
// app/services/EventBatchingService.php
<?php
namespace App\Services;
use App\Events\ApplicationTrafficBatchEvent;
use App\Events\ApplicationTrafficRecorderEvent;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class EventBatchingService
{
/** @var string The cache key used to store queued events */
protected const CACHE_KEY = 'application_traffic_events';
/** @var string The cache key used to store the last event emit time */
protected const LAST_EMIT_KEY = 'application_traffic_last_emit';
/** @var string The Atomic Lock key for queue cache handling */
protected const QUEUE_LOCK_KEY = 'application_traffic_lock';
/** @var int The minimum time (in seconds) between event emissions */
protected const THROTTLE_SECONDS = 2;
/** @var int The maximum number of events to include in a single batch
* This can be adjusted based on Pusher's message size limits */
protected const MAX_BATCH_SIZE = 10;
/**
* Queue an event to be processed in the next batch
*/
public function queueEvent(): void
{
try {
// Create the event data
$event = new ApplicationTrafficRecorderEvent();
$eventData = $event->trafficData;
// Use a lock to prevent race conditions when multiple processes update the queue
Cache::lock(self::QUEUE_LOCK_KEY, 10)->get(function () use ($eventData) {
// Get current queued events
$queuedEvents = Cache::get(self::CACHE_KEY, []);
// Add new event to the queue
$queuedEvents[] = $eventData;
// If we've reached the maximum batch size, trim the oldest events
if (count($queuedEvents) > self::MAX_BATCH_SIZE) {
$queuedEvents = array_slice($queuedEvents, -self::MAX_BATCH_SIZE);
}
// Store updated queue back in cache
Cache::put(self::CACHE_KEY, $queuedEvents, now()->addMinutes(10));
});
} catch (\Exception $e) {
Log::error('Failed to queue application traffic event: ' . $e->getMessage());
}
}
/**
* Process and broadcast all queued events
*/
public function processBatch(): void
{
try {
// Use a lock to prevent race conditions when multiple processes process the batch
Cache::lock(self::QUEUE_LOCK_KEY, 10)->get(function () {
// Check if enough time has passed since the last emit
$lastEmitTime = Cache::get(self::LAST_EMIT_KEY);
$now = now();
if ($lastEmitTime && abs($now->diffInSeconds($lastEmitTime)) < self::THROTTLE_SECONDS) {
// Not enough time has passed, skip this batch
return;
}
// Get all queued events
$queuedEvents = Cache::get(self::CACHE_KEY, []);
// If there are no events, do nothing
if (empty($queuedEvents)) {
return;
}
// Create a batch event with all queued events
$batchEvent = new ApplicationTrafficBatchEvent($queuedEvents);
// Broadcast the batch event
event($batchEvent);
// Update the last emit time
Cache::put(self::LAST_EMIT_KEY, $now, now()->addHours(1));
// Clear the queue
Cache::forget(self::CACHE_KEY);
});
} catch (\Exception $e) {
Log::error('Failed to process application traffic event batch: ' . $e->getMessage());
}
}
}Events Implementation:
// app/events/ApplicationTrafficRecorderEvent.php
<?php
namespace App\Events;
use App\Services\IpApiService;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class ApplicationTrafficRecorderEvent implements ShouldBroadcastNow
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
public $trafficData;
/**
* Create a new event instance.
*/
public function __construct()
{
$ipApi = new IpApiService();
if (app()->environment('local')) {
// handle running on local testing env.
$serverIp = '156.202.149.252'; //EG
$clientIp = '1.178.4.100'; //US
} else {
$serverIp = $_SERVER['SERVER_ADDR'] ?? request()->server('SERVER_ADDR') ?? gethostbyname(gethostname());
$clientIp = $ipApi->getRealIpAddr();
}
// Use any IP location service to convert from IP address to location
$this->trafficData = [
'server_ip' => substr(md5($serverIp), 0, 5),
'server' => array_values($ipApi->getLocationCodeByIp($serverIp)),
'client' => array_values($ipApi->getLocationCodeByIp($clientIp)),
];
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, Channel>
*/
public function broadcastOn(): array
{
return [
new Channel('application-traffic'),
];
}
}// app/events/ApplicationTrafficBatchEvent.php
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class ApplicationTrafficBatchEvent implements ShouldBroadcastNow
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/**
* Array of traffic data from multiple events
*/
public $trafficDataBatch;
/**
* Create a new event instance.
*
* @param array $trafficDataBatch Array of traffic data from multiple events
*/
public function __construct(array $trafficDataBatch)
{
$this->trafficDataBatch = $trafficDataBatch;
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, Channel>
*/
public function broadcastOn(): array
{
return [
new Channel('application-traffic'),
];
}
}To bring the traffic data to life, I used Vue 3, MapLibre GL JS for rendering the interactive map, and deck.gl (via @deck.gl/mapbox) to overlay animated arcs and points representing real-time traffic events.
Below is the full Vue component that powers the real-time visualization. It:
// TrafficMap.vue
<script setup>
import {onMounted, ref, watch, onBeforeUnmount} from 'vue'
import maplibregl from 'maplibre-gl'
import {MapboxOverlay as DeckOverlay} from '@deck.gl/mapbox';
import {ArcLayer, ScatterplotLayer} from '@deck.gl/layers';
// Set up refs
const mapContainer = ref(null)
let map = null
let deckOverlay = null
let trafficBuffer = ref([]);
// Active traffic data that will be displayed
const activeTrafficData = ref([])
// Store unique server locations (will be displayed as blue points)
const serverLocations = ref([])
const colors = [
[120, 176, 250],
[163, 240, 173],
[246, 250, 120]
];
// Function to add a server location to the serverLocations array
// Only adds if the server_ip doesn't already exist in the array
const addServerLocation = (serverIp, location) => {
// Check if this server IP already exists in our array
const exists = serverLocations.value.some(server => server.server_ip === serverIp)
if (!exists) {
serverLocations.value.push({
server_ip: serverIp,
location: location,
id: `server-${serverIp}`
})
}
TrafficStatsStore.servers = serverLocations.value.length
}
// Function to simulate traffic coming in
const simulateTraffic = (trafficDataBatch) => {
// Add a new random traffic data point
if(Array.isArray(trafficDataBatch) && trafficDataBatch.length){
trafficDataBatch.forEach((item) => {
TrafficStatsStore.requests += 1;
const existingIndex = activeTrafficData.value.findIndex(
(data) => Array.isArray(data.client) &&
data.client[0] === item.client?.[0] &&
data.client[1] === item.client?.[1] &&
data.server_ip === item.server_ip
)
if (existingIndex !== -1) {
let colorIdx = activeTrafficData.value[existingIndex].colorIdx;
if (colorIdx + 1 > colors.length - 1) {
activeTrafficData.value[existingIndex].colorIdx = 0;
} else {
activeTrafficData.value[existingIndex].colorIdx += 1;
}
}else{
activeTrafficData.value.push({
...item,
colorIdx: 0,
shouldChangeColor: true,
id: Date.now(),
})
}
// Add server location to our server locations array
addServerLocation(item.server_ip, item.server)
})
TrafficStatsStore.connections = activeTrafficData.value.length;
// Update the deck overlay with new data
if (deckOverlay) {
updateDeckOverlay()
}
}
}
//Function to update the deck overlay with new data
const updateDeckOverlay = () => {
if (!deckOverlay) return;
deckOverlay.setProps({
layers: [
// Animated arcs for traffic
new ArcLayer({
id: `traffic-arc${Date.now()}`, // Unique ID to force update
data: activeTrafficData.value,
getSourcePosition: d => d.client,
getTargetPosition: d => d.server,
getSourceColor: d => colors[d.colorIdx],
getTargetColor: d => [248, 55, 104],
getWidth: d => {
return 1.5;
},
}),
// Blue points for server locations
new ScatterplotLayer({
id: `server-locations${Date.now()}`,
data: serverLocations.value,
getPosition: d => d.location,
getRadius: 8,
radiusMinPixels: 6,
pickable: true,
stroked: true,
lineWidthMinPixels: 1,
lineWidthScale: 1,
getFillColor: [248, 55, 104], // Servers Color
getLineColor: [248, 55, 104], // Servers Color
})
]
});
};
onMounted(() => {
// Initialize the map
map = new maplibregl.Map({
container: mapContainer.value,
style: 'https://api.maptiler.com/maps/dataviz-dark/style.json?key=<ADD API TOKEN HERE>',
center: [0.45, 14.47],
zoom: 2,
pitch: 20,
bearing: 0,
});
// Initialize deck.gl overlay with empty data
deckOverlay = new DeckOverlay({
interleaved: true,
layers: []
});
// Set up map controls
map.on('load', () => {
map.addControl(deckOverlay);
map.addControl(new maplibregl.NavigationControl());
const ele = document.getElementsByClassName('maplibregl-control-container')
while (ele.length > 0) {
ele[0].parentNode.removeChild(ele[0]);
}
});
Echo.channel(`application-traffic`)
.listen('ApplicationTrafficBatchEvent', (e) => {
if (e?.trafficDataBatch && Array.isArray(trafficBuffer.value)) {
trafficBuffer.value.push(...e.trafficDataBatch);
}
});
setInterval(() => {
if (Array.isArray(trafficBuffer.value) && trafficBuffer.value.length > 0) {
const batch = [...trafficBuffer.value];
trafficBuffer.value = [];
simulateTraffic(batch); // draw batched data
}
}, 2000); // tune this interval based on performance
});
// Clean up resources when component is unmounted
onBeforeUnmount(() => {
// Remove the map
if (map) {
map.remove();
}
});
</script>
<template>
<div class="traffic-map-container">
<div ref="mapContainer" class="map-container"></div>
</div>
</template>
<style scoped>
.traffic-map-container {
position: relative;
width: 100%;
height: 100vh;
}
.map-container {
height: 100%;
width: 100%;
}
</style>
In action, each client-server connection is visualized as a smooth arc across the globe. Repeated traffic to the same server is color-cycled for better visual differentiation. Server nodes are rendered as persistent red points.
This system scales well with thousands of connections, thanks to the batching mechanism and GPU-accelerated rendering from deck.gl.
Tip: Adjust the update interval and color cycling strategy based on the volume and frequency of traffic data for optimal UX and performance.
You can find the complete frontend code in the TrafficMap folder of my Website GitHub repository.
The frontend code is organized in the following structure:
By combining Laravel's backend capabilities with Redis for caching and atomic locks, and leveraging Vue.js with MapLibre GL and Deck.gl on the frontend, I built a robust and high-performance real-time traffic visualization system. This setup efficiently handles high traffic volumes and provides an interactive and informative visualization of global user interactions.
Feel free to explore the code and adapt it to your own projects!