How I Built a Real-Time Precious Metals Price Feed for 30,000 Concurrent Users in Laravel This article describes how a developer built a real-time precious metals price feed using Laravel 11, Laravel Reverb, and Redis to handle 30,000 concurrent users. The system uses WebSocket broadcasting through Laravel Reverb to push gold and silver price updates with sub-second latency, bypassing traditional polling methods. Key architectural decisions include using Redis for both price caching and queue backend, implementing a `broadcastWhen()` filter to eliminate ~80% of redundant price updates, and keeping the database out of the subscriber hot path by only writing once per tick rather than reading 30,000 times. How I Built a Real-Time Precious Metals Price Feed for 30,000 Concurrent Users in Laravel Architecture walkthrough: Laravel Reverb, WebSocket broadcasting, Redis pub/sub, and the tricks that make it hold under real load. The Problem A live precious metals trading platform needs to push gold and silver price updates to every connected user the moment a tick arrives — not every 5 seconds via polling, not via a REST endpoint they hammer on a timer. Real-time. Sub-second latency. And it needs to handle 30,000 simultaneous connections without melting. This post walks through exactly how I built this using Laravel 11, Laravel Reverb, Redis, and a few architectural decisions that made the difference between a demo and something production-ready. Stack - Laravel 11 + PHP 8.2 - Laravel Reverb — self-hosted WebSocket server replaces Pusher - Laravel Echo — frontend WebSocket client - Redis — pub/sub backbone + cache - Laravel Horizon — queue worker monitoring - MySQL 8 — OHLCV candle storage with covering indexes Full source: github.com/Hafiz-M-Subhan/laravel-precious-metals-platform https://github.com/Hafiz-M-Subhan/laravel-precious-metals-platform Architecture Overview Price Feed external API ↓ PriceService::ingestTick ↓ DB update + Redis cache 5s TTL ↓ broadcast new PriceUpdated $asset ↓ Laravel Reverb WebSocket server ↓ prices.XAU channel → 30k subscribers The key insight: the database is not in the hot path for subscribers. Price data flows through Redis and directly out via WebSocket. The DB only gets written once per tick, not read 30,000 times. Step 1 — The Event The PriceUpdated event is what gets broadcast. The most important decisions are: Which channel? Public, so unauthenticated visitors on the live page receive ticks too. What payload? As small as possible. Every extra byte is multiplied by 30,000. class PriceUpdated implements ShouldBroadcast { public function broadcastOn : array { return new Channel "prices.{$this- asset- symbol}" , new PresenceChannel 'live-event' , // carries viewer count ; } public function broadcastWith : array { // 7 fields — deliberately minimal return 'symbol' = $this- asset- symbol, 'spot' = float $this- asset- spot price, 'bid' = float $this- asset- bid price, 'ask' = float $this- asset- ask price, 'change pct' = float $this- asset- daily change pct, 'direction' = $this- asset- spot price $this- previousPrice ? 'up' : 'down', 'ts' = now - toIso8601String , ; } // Skip broadcast if price moved less than 0.001% — kills ~80% of noise public function broadcastWhen : bool { if $this- previousPrice == 0 return true; $change = abs $this- asset- spot price - $this- previousPrice / $this- previousPrice ; return $change = 0.00001; } } The broadcastWhen gate is underused in most Laravel projects. In a metals feed, prices sometimes tick the same value repeatedly. Without filtering, you're broadcasting thousands of no-op messages to 30,000 clients. With it, you cut ~80% of queue messages. Step 2 — The WebSocket Server Laravel Reverb Laravel Reverb is Laravel's official self-hosted WebSocket server, released in 2024. Before Reverb, you either paid for Pusher or ran a separate Node.js server Soketi, etc. . Reverb runs as a native PHP process: php artisan reverb:start --host=0.0.0.0 --port=8080 In config/broadcasting.php : js 'reverb' = 'driver' = 'reverb', 'app id' = env 'REVERB APP ID' , 'app key' = env 'REVERB APP KEY' , 'app secret' = env 'REVERB APP SECRET' , 'options' = 'host' = env 'REVERB HOST', '0.0.0.0' , 'port' = env 'REVERB PORT', 8080 , 'scheme' = env 'REVERB SCHEME', 'http' , , , Reverb uses a non-blocking event loop under the hood ReactPHP . It handles thousands of concurrent connections on a single process — no thread-per-connection model like traditional PHP. Step 3 — Redis as the Backbone Redis does two jobs here: 1. Price cache — every tick writes to Redis with a 5-second TTL. API responses read from Redis, not MySQL. Under a burst of requests live event with 30k viewers all hitting /api/v1/assets , the DB sees exactly 0 extra reads. // PriceService::ingestTick Cache::put "asset:price:{$symbol}", 'spot' = $spot, 'bid' = $bid, 'ask' = $ask, 'ts' = now - toIso8601String , , 5 ; // 5 seconds 2. Queue backend — the broadcast job goes through Redis queues, not the database. This is critical. QUEUE CONNECTION=redis in .env . Database queues serialize and will struggle under a price feed that fires every 2 seconds across 4 metals. Horizon monitors all of this with a real dashboard at /horizon . You can see queue throughput, failed jobs, and worker load in real time. Step 4 — OHLCV Candle Storage Every tick needs to update the current 1-minute candle open, high, low, close . Naive approach: SELECT + UPDATE. At 30 ticks/minute across 4 metals, that's 120 roundtrips per minute, plus locking issues. Better approach: upsert — one query, atomic, no SELECT needed: js PriceHistory::upsert 'asset id' = $assetId, 'resolution' = '1m', 'open' = $price, // only set on INSERT 'high' = $price, 'low' = $price, 'close' = $price, 'volume' = 0, 'recorded at' = now - startOfMinute , , uniqueBy: 'asset id', 'resolution', 'recorded at' , update: 'high' = DB::raw "GREATEST high, {$price} " , 'low' = DB::raw "LEAST low, {$price} " , 'close' = $price, , ; The unique index on asset id, resolution, recorded at makes this both fast and idempotent — if the same tick somehow arrives twice, nothing breaks. Step 5 — The Frontend Laravel Echo python import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; // Echo uses Pusher protocol even with Reverb window.Pusher = Pusher; const echo = new Echo { broadcaster: 'reverb', key: import.meta.env.VITE REVERB APP KEY, wsHost: import.meta.env.VITE REVERB HOST, wsPort: import.meta.env.VITE REVERB PORT, forceTLS: false, enabledTransports: 'ws', 'wss' , } ; // Subscribe to gold price updates echo.channel 'prices.XAU' .listen '.price.updated', data = { updatePriceTicker data.symbol, data.spot, data.direction ; } ; // Presence channel — get viewer count for live event page echo.join 'live-event' .here users = setViewerCount users.length .joining = setViewerCount prev = prev + 1 .leaving = setViewerCount prev = prev - 1 ; The presence channel gives you live viewer count for free — the same mechanism Kettner uses for their live event page that peaks at 30,000 simultaneous viewers. Performance Numbers | Scenario | Without optimization | With optimization | |---|---|---| | API under burst 1k req/s | MySQL: 1000 reads/s | MySQL: ~0 reads Redis cache | | Queue messages per minute | ~120 raw ticks | ~25 broadcastWhen filters | | Candle upserts | 2 queries SELECT+UPDATE | 1 query upsert | | Connection overhead | 30k × polling interval | 1 persistent connection per user | Running It Locally git clone https://github.com/Hafiz-M-Subhan/laravel-precious-metals-platform.git cd laravel-precious-metals-platform docker compose up -d MySQL + Redis + Elasticsearch + Reverb + Horizon php artisan migrate --seed php artisan reverb:start php artisan horizon php artisan prices:simulate --interval=2 simulates live price feed The simulator uses Geometric Brownian Motion to generate realistic price movements — the same model used in Black-Scholes options pricing. What I'd Do Differently at True Scale - Horizontal Reverb scaling — multiple Reverb nodes behind a load balancer, using Redis pub/sub to sync broadcasts across nodes - Separate read replicas for candle chart queries — OHLCV history doesn't need the primary DB - Message compression — at 30k subscribers, gzip on WebSocket frames saves significant bandwidth - Backpressure handling — slow consumers should be detected and disconnected before they cause memory pressure on the Reverb process Source Code Full project on GitHub: github.com/Hafiz-M-Subhan/laravel-precious-metals-platform https://github.com/Hafiz-M-Subhan/laravel-precious-metals-platform Includes: models, events, jobs, services, Filament 3 admin panel, Docker Compose, migrations, and a price simulator. Tags: laravel php websocket redis architecture