{"slug": "how-i-built-a-real-time-precious-metals-price-feed-for-30000-concurrent-users-in", "title": "How I Built a Real-Time Precious Metals Price Feed for 30,000 Concurrent Users in Laravel", "summary": "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.", "body_md": "Architecture walkthrough: Laravel Reverb, WebSocket broadcasting, Redis pub/sub, and the tricks that make it hold under real load.\nA 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.\nThis 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.\nFull source: github.com/Hafiz-M-Subhan/laravel-precious-metals-platform\nPrice Feed (external API)\n↓\nPriceService::ingestTick()\n↓\nDB update + Redis cache (5s TTL)\n↓\nbroadcast(new PriceUpdated($asset))\n↓\nLaravel Reverb (WebSocket server)\n↓\nprices.XAU channel → 30k subscribers\nThe 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.\nThe PriceUpdated\nevent is what gets broadcast. The most important decisions are:\nWhich channel? Public, so unauthenticated visitors on the live page receive ticks too.\nWhat payload? As small as possible. Every extra byte is multiplied by 30,000.\nclass PriceUpdated implements ShouldBroadcast\n{\npublic function broadcastOn(): array\n{\nreturn [\nnew Channel(\"prices.{$this->asset->symbol}\"),\nnew PresenceChannel('live-event'), // carries viewer count\n];\n}\npublic function broadcastWith(): array\n{\n// 7 fields — deliberately minimal\nreturn [\n'symbol' => $this->asset->symbol,\n'spot' => (float) $this->asset->spot_price,\n'bid' => (float) $this->asset->bid_price,\n'ask' => (float) $this->asset->ask_price,\n'change_pct' => (float) $this->asset->daily_change_pct,\n'direction' => $this->asset->spot_price > $this->previousPrice ? 'up' : 'down',\n'ts' => now()->toIso8601String(),\n];\n}\n// Skip broadcast if price moved less than 0.001% — kills ~80% of noise\npublic function broadcastWhen(): bool\n{\nif ($this->previousPrice == 0) return true;\n$change = abs(($this->asset->spot_price - $this->previousPrice) / $this->previousPrice);\nreturn $change >= 0.00001;\n}\n}\nThe broadcastWhen()\ngate 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.\nLaravel 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:\nphp artisan reverb:start --host=0.0.0.0 --port=8080\nIn config/broadcasting.php\n:\n'reverb' => [\n'driver' => 'reverb',\n'app_id' => env('REVERB_APP_ID'),\n'app_key' => env('REVERB_APP_KEY'),\n'app_secret' => env('REVERB_APP_SECRET'),\n'options' => [\n'host' => env('REVERB_HOST', '0.0.0.0'),\n'port' => env('REVERB_PORT', 8080),\n'scheme' => env('REVERB_SCHEME', 'http'),\n],\n],\nReverb 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.\nRedis does two jobs here:\n1. 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\n), the DB sees exactly 0 extra reads.\n// PriceService::ingestTick()\nCache::put(\"asset:price:{$symbol}\", [\n'spot' => $spot, 'bid' => $bid, 'ask' => $ask,\n'ts' => now()->toIso8601String(),\n], 5); // 5 seconds\n2. Queue backend — the broadcast job goes through Redis queues, not the database. This is critical. QUEUE_CONNECTION=redis\nin .env\n. Database queues serialize and will struggle under a price feed that fires every 2 seconds across 4 metals.\nHorizon monitors all of this with a real dashboard at /horizon\n. You can see queue throughput, failed jobs, and worker load in real time.\nEvery 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.\nBetter approach: upsert()\n— one query, atomic, no SELECT needed:\nPriceHistory::upsert(\n[[\n'asset_id' => $assetId,\n'resolution' => '1m',\n'open' => $price, // only set on INSERT\n'high' => $price,\n'low' => $price,\n'close' => $price,\n'volume' => 0,\n'recorded_at' => now()->startOfMinute(),\n]],\nuniqueBy: ['asset_id', 'resolution', 'recorded_at'],\nupdate: [\n'high' => DB::raw(\"GREATEST(high, {$price})\"),\n'low' => DB::raw(\"LEAST(low, {$price})\"),\n'close' => $price,\n],\n);\nThe unique index on (asset_id, resolution, recorded_at)\nmakes this both fast and idempotent — if the same tick somehow arrives twice, nothing breaks.\nimport Echo from 'laravel-echo';\nimport Pusher from 'pusher-js'; // Echo uses Pusher protocol even with Reverb\nwindow.Pusher = Pusher;\nconst echo = new Echo({\nbroadcaster: 'reverb',\nkey: import.meta.env.VITE_REVERB_APP_KEY,\nwsHost: import.meta.env.VITE_REVERB_HOST,\nwsPort: import.meta.env.VITE_REVERB_PORT,\nforceTLS: false,\nenabledTransports: ['ws', 'wss'],\n});\n// Subscribe to gold price updates\necho.channel('prices.XAU')\n.listen('.price.updated', (data) => {\nupdatePriceTicker(data.symbol, data.spot, data.direction);\n});\n// Presence channel — get viewer count for live event page\necho.join('live-event')\n.here((users) => setViewerCount(users.length))\n.joining(() => setViewerCount(prev => prev + 1))\n.leaving(() => setViewerCount(prev => prev - 1));\nThe 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.\ngit clone https://github.com/Hafiz-M-Subhan/laravel-precious-metals-platform.git\ncd laravel-precious-metals-platform\ndocker compose up -d # MySQL + Redis + Elasticsearch + Reverb + Horizon\nphp artisan migrate --seed\nphp artisan reverb:start\nphp artisan horizon\nphp artisan prices:simulate --interval=2 # simulates live price feed\nThe simulator uses Geometric Brownian Motion to generate realistic price movements — the same model used in Black-Scholes options pricing.\nFull project on GitHub: github.com/Hafiz-M-Subhan/laravel-precious-metals-platform\nIncludes: models, events, jobs, services, Filament 3 admin panel, Docker Compose, migrations, and a price simulator.\nTags: #laravel #php #websocket #redis #architecture", "url": "https://wpnews.pro/news/how-i-built-a-real-time-precious-metals-price-feed-for-30000-concurrent-users-in", "canonical_source": "https://dev.to/rana_subhan/how-i-built-a-real-time-precious-metals-price-feed-for-30000-concurrent-users-in-laravel-4gjg", "published_at": "2026-05-23 10:02:22+00:00", "updated_at": "2026-05-23 10:34:52.958107+00:00", "lang": "en", "topics": ["developer-tools", "data", "enterprise-software"], "entities": ["Laravel", "Laravel Reverb", "Redis", "WebSocket", "GitHub", "Hafiz-M-Subhan"], "alternates": {"html": "https://wpnews.pro/news/how-i-built-a-real-time-precious-metals-price-feed-for-30000-concurrent-users-in", "markdown": "https://wpnews.pro/news/how-i-built-a-real-time-precious-metals-price-feed-for-30000-concurrent-users-in.md", "text": "https://wpnews.pro/news/how-i-built-a-real-time-precious-metals-price-feed-for-30000-concurrent-users-in.txt", "jsonld": "https://wpnews.pro/news/how-i-built-a-real-time-precious-metals-price-feed-for-30000-concurrent-users-in.jsonld"}}