Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

GraphQL Subscriptions with Laravel Lighthouse

By Kokil Thapa | Last reviewed: August 2026

Implementing GraphQL Subscriptions with Laravel Lighthouse transforms a standard request-response API into a reactive system where clients receive instant updates without polling. While Lighthouse provides the schema directives and subscription engine, the actual real-time delivery depends entirely on your Laravel broadcasting infrastructure and WebSocket server configuration. This guide covers the complete production setup, moving beyond basic tutorials to address the Redis, queue, and deployment realities required for stable subscriptions in 2026.

Before diving into subscription specifics, ensure your foundation is solid. Real-time features add operational complexity that compounds existing architectural issues. If you are still structuring your core API, review Laravel API best practices first to avoid building subscriptions on top of an unscalable base. Subscriptions amplify both good and bad design decisions; they work reliably only when your underlying event system and queue infrastructure are already production-grade.

How do GraphQL Subscriptions with Laravel Lighthouse actually work?

Understanding the internal mechanism prevents the most common debugging failures. GraphQL Subscriptions with Laravel Lighthouse do not maintain a direct PHP process per client. Instead, they rely on a pub/sub pattern decoupled from the HTTP lifecycle.

Client AppSubscription QueryWebSocket ServerReverb / Laravel WSRedis Pub/SubBroadcast DriverLaravel WorkerQueue + TriggerWS ConnectPublish EventConsumeGraphQL Subscriptions with Laravel Lighthouse Architecture Flow
Data flow for GraphQL Subscriptions with Laravel Lighthouse: clients connect via WebSockets, Laravel publishes to Redis, and the WebSocket server pushes updates back to subscribed clients.

The sequence operates as follows:

  1. Initial Handshake: The client sends a GraphQL subscription query over a WebSocket connection. Lighthouse validates the query, resolves any initial arguments, and registers the subscription in storage (typically Redis).
  2. Event Trigger: A mutation or background job in your Laravel application fires an event. This is usually done via Broadcast::event() or by returning a subscription trigger from a mutation resolver.
  3. Pub/Sub Distribution: Laravel serializes the event payload and publishes it to a Redis channel. This step is non-blocking and handled by the queue worker if configured correctly.
  4. Delivery: The WebSocket server subscribes to the Redis channel. When a message arrives, it matches the event against active subscription registrations and pushes the updated GraphQL payload to all matching connected clients.

This architecture means your PHP application never holds a socket open. The WebSocket server (Reverb or Laravel WebSockets) handles persistence, while Laravel remains stateless. Misconfiguring any link in this chain—Redis connectivity, queue workers, or channel naming—silently breaks subscriptions without throwing HTTP errors.

How do you configure broadcasting for GraphQL Subscriptions with Laravel Lighthouse?

Lighthouse does not replace Laravel’s broadcasting system; it consumes it. Your broadcasting configuration must be correct before Lighthouse subscriptions will function. In 2026, with Laravel 12.x and PHP 8.4, the recommended stack uses Redis 7.x as the broadcast driver and either Laravel Reverb (first-party) or Laravel WebSockets (community-maintained) as the socket server.

Redis Configuration

Redis serves dual purposes: caching/broadcasting and subscription storage. Ensure your config/database.php Redis configuration includes a dedicated connection for broadcasting if your workload is heavy:

<?php
// config/database.php
'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'),
    'default' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD'),
        'port' => env('REDIS_PORT', 6379),
        'database' => 0,
    ],
    'broadcast' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD'),
        'port' => env('REDIS_PORT', 6379),
        'database' => 1, // Isolate broadcast traffic
    ],
],

In .env, set BROADCAST_CONNECTION=redis. Verify connectivity with php artisan tinker followed by Redis::connection('broadcast')->ping(). A common mistake in Nepal-hosted environments is firewall rules blocking Redis port 6379 between the application server and Redis instance when they are on separate VMs.

WebSocket Server Selection

FeatureLaravel ReverbLaravel WebSockets (beyondcode)
Maintenance StatusFirst-party (Laravel team)Community (archived but stable)
Laravel 12 SupportNativeRequires fork or compatibility patch
Horizontal ScalingBuilt-in Redis pub/subRequires manual Redis configuration
DashboardMinimalFull debug dashboard
Production Ready 2026YesLegacy projects only

For new projects in 2026, use Laravel Reverb. Install via composer require laravel/reverb and run php artisan reverb:install. For existing projects on Laravel WebSockets that cannot migrate immediately, ensure you are running version 2.x with PHP 8.2+ compatibility patches applied.

How do you implement a subscription class in Lighthouse?

With broadcasting configured, define your subscription in the Lighthouse schema and create the corresponding PHP class. This example assumes a legal-tech portal where clients need real-time updates when their case status changes—a pattern I have implemented across multiple Nepal law firm portals.

Schema Definition

# schema.graphql
type Subscription {
    caseStatusUpdated(caseId: ID!): CaseStatusUpdate!
        @subscription(class: "Subscriptions\\CaseStatusUpdated")
}

type CaseStatusUpdate {
    caseId: ID!
    status: String!
    updatedAt: DateTime!
    note: String
}

Subscription Class Implementation

Create app/GraphQL/Subscriptions/CaseStatusUpdated.php:

<?php

namespace App\GraphQL\Subscriptions;

use Nuwave\Lighthouse\Subscriptions\Subscription;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Illuminate\Support\Facades\Auth;

class CaseStatusUpdated extends Subscription
{
    /
     * Filter which subscribers should receive the update.
     */
    public function authorize($root, array $args, GraphQLContext $context): bool
    {
        // Only allow users who own this case or are assigned attorneys
        return Auth::check() && 
               ($root->user_id === Auth::id() || 
                $root->assigned_attorney_id === Auth::id());
    }

    /
     * Resolve the subscription payload.
     */
    public function resolve($root, array $args): array
    {
        return [
            'caseId' => $root->id,
            'status' => $root->status,
            'updatedAt' => $root->updated_at,
            'note' => $root->latest_note,
        ];
    }

    /**
     * Define the channel name for filtering.
     */
    public static function topicName(array $args): string
    {
        return 'case.' . $args['caseId'];
    }
}

The authorize method is critical. Without it, every subscriber to caseStatusUpdated receives every case update, creating a data leak. Lighthouse calls authorize both at subscription registration and at delivery time. Always validate ownership or permissions here, never assume the client filtered correctly.

Event TriggeredFind Subscribersauthorize() CheckSkip Deliveryresolve() PayloadPush to ClientDeniedAllowed
Authorization and resolution flow for GraphQL Subscriptions with Laravel Lighthouse — every event passes through authorize() before resolve() executes.

Triggering the Subscription

Subscriptions can be triggered from mutations or event listeners. The mutation approach is explicit and traceable:

<?php

namespace App\GraphQL\Mutations;

use App\Models\CaseFile;
use Nuwave\Lighthouse\Subscriptions\BroadcastSubscription;

class UpdateCaseStatus
{
    public function __invoke($root, array $args): CaseFile
    {
        $case = CaseFile::findOrFail($args['id']);
        $case->update(['status' => $args['status']]);
        
        // Trigger subscription with the updated model
        BroadcastSubscription::trigger(
            'caseStatusUpdated',
            ['caseId' => $case->id],
            $case
        );
        
        return $case;
    }
}

Alternatively, use Laravel events for decoupled triggers. Create CaseStatusChanged event implementing ShouldBroadcastNow, then call Broadcast::event(new CaseStatusChanged($case)) from your service layer. Lighthouse automatically maps broadcast events to subscriptions when channel names match.

What are the production deployment requirements for GraphQL Subscriptions with Laravel Lighthouse?

Development setups mask production realities. On a real client project serving legal professionals across Nepal, subscriptions failed silently after deployment because the WebSocket server was not supervised and Redis connections were not persisted. Address these requirements before going live.

Process Supervision

The WebSocket server must run as a persistent daemon. Use systemd on Ubuntu 22/24:

# /etc/systemd/system/reverb.service
[Unit]
Description=Laravel Reverb WebSocket Server
After=network.target redis.service

[Service]
User=www-data
WorkingDirectory=/var/www/html
ExecStart=/usr/bin/php artisan reverb:start --host=0.0.0.0 --port=8080
Restart=always
RestartSec=5
Environment="APP_ENV=production"

[Install]
WantedBy=multi-user.target

Enable with sudo systemctl enable --now reverb.service. Never run the WebSocket server inside tmux or screen in production. Process crashes without supervision cause silent subscription failures that are difficult to diagnose.

Queue Workers

Subscription broadcasting should be queued to avoid blocking HTTP responses. Configure a dedicated queue for broadcasts in config/queue.php and run workers with --queue=broadcasts,default. Monitor queue depth; broadcast jobs piling up indicate Redis contention or insufficient workers. For high-traffic systems, I typically allocate 2–4 dedicated broadcast workers separate from general application queues.

Nginx Reverse Proxy

Proxy WebSocket connections through Nginx to handle SSL termination and load balancing:

server {
    listen 443 ssl http2;
    server_name api.example.com;

    location /graphql {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 86400s; # Keep WS alive 24h
        proxy_send_timeout 86400s;
    }
}

The proxy_read_timeout value is critical. Default Nginx timeouts (60s) kill idle WebSocket connections. Set this to match your expected maximum idle period. For legal-tech portals where attorneys may leave dashboards open during court sessions, 24-hour timeouts prevent unnecessary reconnections.

NginxSSL + WS ProxyPHP-FPMLaravel AppReverbPort 8080Redis 7.xPub/Sub + CacheQueue WorkersBroadcast QueueProduction Topology for GraphQL Subscriptions with Laravel Lighthouse
Required production components: Nginx proxies both HTTP and WebSocket traffic, PHP-FPM handles requests, Reverb manages sockets, Redis coordinates pub/sub, and dedicated queue workers process broadcast jobs.

How do you debug failing GraphQL Subscriptions with Laravel Lighthouse?

Subscription failures are notoriously silent. The client receives no error; updates simply stop arriving. Follow this diagnostic sequence when subscriptions work locally but fail in staging or production.

  • Verify WebSocket connectivity: Open browser DevTools → Network → WS tab. Confirm the connection establishes and stays open. A 404 indicates misconfigured Nginx proxy paths. A 101 Switching Protocols response confirms successful upgrade.
  • Check Redis pub/sub: Run redis-cli MONITOR on the server while triggering an event. You should see PUBLISH commands with your channel name. Absence indicates the broadcast job never executed or Redis connection failed.
  • Inspect queue failures: Check failed_jobs table and Laravel logs for BroadcastException. Common causes include serialization errors in the event payload or missing authentication context in queued jobs.
  • Validate authorization: Add temporary logging in your subscription’s authorize method. If authorize returns false, the subscription silently drops. This is the most frequent cause of "subscriptions work for admin but not regular users" issues.
  • Test with Lighthouse debug mode: Enable LIGHTHOUSE_DEBUG=true in development. Lighthouse adds subscription debug information to responses, including registration IDs and filter criteria. Never enable this in production.

For complex debugging scenarios involving multiple services, refer to building real-time features in Laravel using WebSockets and Redis for deeper coverage of the underlying broadcasting mechanics that Lighthouse abstracts.

When should you avoid GraphQL Subscriptions with Laravel Lighthouse?

Subscriptions are not universally appropriate. They introduce persistent connections, stateful infrastructure, and operational overhead that simpler alternatives avoid. Consider these trade-offs before committing:

  • High-frequency updates (>10/sec per client): WebSocket throughput becomes the bottleneck. Use Server-Sent Events (SSE) or short-polling instead. GraphQL subscriptions excel at low-to-medium frequency state changes, not streaming telemetry.
  • Public anonymous feeds: Without authentication, subscription channels become attack vectors for resource exhaustion. Implement rate limiting and connection caps at the WebSocket server level. For public content like blog updates, RSS or SSE with CDN caching is more scalable.
  • Simple notification-only use cases: If clients only need "something changed" signals without payload data, use Pusher/Ably native notifications or Firebase Cloud Messaging. Reserve GraphQL subscriptions for cases where the client needs the full updated GraphQL object graph.
  • Teams without DevOps capacity: Subscriptions require managing persistent processes, monitoring connection counts, and handling Redis scaling. If your team cannot commit to this operational burden, evaluate managed services like Ably or Pusher that integrate with Lighthouse via custom broadcast drivers.

For teams evaluating whether to invest in real-time infrastructure at all, hiring a web developer in Nepal with proven Lighthouse and WebSocket experience can bridge the gap between prototype and production reliability without building internal expertise from scratch.

Deploying GraphQL Subscriptions with Laravel Lighthouse Reliably

GraphQL Subscriptions with Laravel Lighthouse deliver genuine real-time value when the full stack is configured correctly: Redis broadcasting, supervised WebSocket servers, queued event processing, and proper authorization filtering. The implementation details matter more than the schema elegance. Test your subscription pipeline end-to-end in a staging environment that mirrors production infrastructure before shipping to clients. Monitor connection counts, queue depths, and Redis memory usage as primary health indicators.

If you are implementing real-time features for a Laravel application and need hands-on guidance or production-ready setup, reach out to discuss your specific requirements. I have deployed Lighthouse subscriptions across legal-tech platforms, booking systems, and eCommerce dashboards in Nepal and internationally, and can help you avoid the pitfalls that only surface under real user load.

Frequently Asked Questions

Server-sent events over WebSocket allowing clients to receive real-time data updates from a Laravel backend using Lighthouse's @subscription directive.

Expect Rs 80,000–150,000 (USD 600–1,100) for setup, testing, and deployment on an existing Laravel app with Lighthouse already configured.

Use subscriptions when multiple clients need simultaneous real-time updates; prefer webhooks for server-to-server notifications or low-frequency events.

Soketi is my current recommendation for 2026 deployments. It is a lightweight, open-source alternative to Pusher that integrates natively with Laravel Echo and Lighthouse without vendor lock-in. On client projects where budget allows managed infrastructure, Pusher remains viable, but self-hosted Soketi on Ubuntu 24 reduces monthly costs significantly for Nepal-based teams managing their own EC2 or VPS instances.

Install the predis/predis package and set BROADCAST_CONNECTION=redis in your .env file. Configure Redis as both cache and queue driver. Ensure phpredis extension is enabled if using native performance. In lighthouse.php, set the broadcaster to redis. I have found that misconfigured Redis permissions or missing PHP extensions cause silent subscription failures during deployment, so always verify connectivity via artisan tinker before going live.

Yes, Lighthouse supports authorization for subscriptions using standard Laravel policies. Define an authorize method in your subscription class returning true or false based on user permissions. Private channels prevent unauthorized users from receiving sensitive real-time data. On legal-tech portals handling case updates, this pattern ensures only assigned attorneys receive notifications. Always test authorization logic thoroughly, as subscription auth errors often fail silently without proper logging configured.

Apollo Client and urql both support Lighthouse subscriptions via WebSocket transport. Configure the split link to route subscription operations through ws:// while queries use HTTP. Vue.js applications pair well with these clients. Alpine.js can consume subscriptions via vanilla JS WebSocket wrappers for lighter implementations. Ensure your frontend handles reconnection logic, as network interruptions in Nepal require resilient client-side recovery patterns.

Enable Lighthouse debug mode and check storage/logs/laravel.log for broadcast exceptions. Use Laravel Telescope to inspect queued jobs and Redis activity. Verify WebSocket server is running and accessible on the expected port. Browser DevTools Network tab shows WebSocket frames. Common issues include CORS misconfiguration, missing APP_URL in .env, or queue workers not processing broadcast events. I regularly add temporary logging in subscription classes during development to trace execution flow.

Yes, but you must use Redis Pub/Sub as the broadcast driver instead of local array or log drivers. All application servers connect to the same Redis instance, which coordinates message distribution. Soketi or Pusher handle fan-out automatically. Without shared Redis, subscribers connected to different servers miss events. On multi-server deployments, also ensure session and cache drivers use the same Redis cluster to maintain consistent authentication state across nodes.

Subscriptions bypass traditional HTTP middleware, requiring explicit authorization checks within each subscription class. Rate limit subscription creation to prevent resource exhaustion attacks. Validate all input arguments to prevent injection. Use TLS for WebSocket connections in production. Implement subscription timeouts to clean up abandoned connections. On projects handling sensitive data, audit which fields are exposed via subscriptions, as they may leak information not present in query resolvers due to different authorization contexts.

Each active subscription maintains a persistent WebSocket connection consuming memory and CPU. Budget approximately 2GB RAM per 1,000 concurrent subscribers on a standard Laravel setup. Self-hosted Soketi adds minimal overhead compared to managed Pusher fees. Monitor connection counts via Redis INFO or Soketi metrics. For Nepal-based projects with limited hosting budgets, implement connection limits and idle timeouts aggressively. Consider horizontal scaling only after optimizing subscription payload size and filtering unnecessary broadcasts.

Active WebSocket connections drop during PHP-FPM reloads unless using a separate WebSocket server like Soketi. Clients must implement automatic reconnection with exponential backoff. With Deployer 7 symlinked releases, the new codebase activates instantly, but existing connections terminate. Notify users of maintenance windows when possible. Store critical state outside WebSocket sessions so reconnecting clients can resynchronize. Test deployment impact in staging first, as subscription recovery behavior varies significantly between frontend libraries and network conditions.

Unit test subscription authorization and resolve methods directly without WebSocket overhead. For integration tests, use Laravel's Broadcast::fake() to assert events are dispatched correctly. End-to-end subscription testing requires spinning up actual WebSocket infrastructure, which is slow and flaky in CI. I typically mock the broadcaster in pipeline tests and reserve full subscription E2E tests for critical paths only. Document expected subscription payloads separately so frontend teams can develop against contracts without waiting for backend infrastructure.

Nuwave Lighthouse remains the most mature option with native subscription support. Laravel Reverb offers first-party WebSocket broadcasting but lacks GraphQL-specific features. Sileria provides GraphQL subscriptions but has smaller community adoption. For simple real-time needs without GraphQL complexity, consider Laravel Livewire's polling or Alpine.js with Server-Sent Events. Evaluate whether full GraphQL subscriptions justify the operational overhead versus simpler alternatives, especially for projects with small teams maintaining production systems long-term.

Forgetting to run queue workers causes broadcasts to never execute. Missing CORS headers block browser WebSocket connections. Broadcasting excessive data overwhelms clients and wastes bandwidth. Neglecting subscription cleanup creates memory leaks over time. Hardcoding WebSocket URLs breaks environment portability. Assuming subscriptions replace proper database indexing for real-time feeds. Always validate that subscription resolvers remain performant under load, as they execute on every broadcast rather than on-demand like queries. Profile resolver performance before launching to production.

Share this article

Quick Contact Options
Choose how you want to connect me: