Caio-Tera's avatar

Caio-Tera started a new conversation+100 XP

5mos ago

I recently made a decision to use FrankenPHP + Mercure for a real-time dashboard project (Laravel 12 + Octane), and I wanted to share the architectural reasoning behind it.

I see a lot of people suggesting PHP-native SSE (like Laravel Wave) or defaulting to WebSockets (Reverb), but there is a critical distinction often missed regarding Connection Holding and Protocol Integration.

I analyzed the options based on "Who holds the TCP connection?":

1. The "Hold the Line" Model (Native PHP / Swoole / Standard RoadRunner) In this model, the PHP application layer (or the worker process) handles the connection.

  • The Mechanism: The application state is tied to the network I/O.
  • The Cost: If you have 10,000 users waiting for a status update (idle connections), you essentially have 10,000 workers/coroutines occupied. You are using application memory to manage network silence.
  • Recoverability: You often have to manually handle reconnection logic and state management within PHP.

2. The "Fire and Forget" Model (FrankenPHP + Mercure) This is where the architecture shifts. We use mvanduijker/laravel-mercure-broadcaster as the driver.

  • The Mechanism: The Client connects to Caddy (Go), not PHP.
  • The Optimization: With recent FrankenPHP updates, we can leverage native bindings. PHP dispatches the update directly to the Caddy Hub (often bypassing the local network stack entirely via mercure_publish internal calls).
  • The Benefit: Zero PHP RAM usage for idle connections.

The Code Reality (Standard Laravel Broadcasting): The beauty of this stack is that I don't need to change how I write Laravel code. I don't write loop logic or stream responses manually. I just dispatch a standard Event.

// 1. The Event
class ExamQueueUpdated implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function broadcastOn()
    {
        // Maps to the Mercure Topic
        return new Channel('clinic.1.queue');
    }
}

// 2. The Controller
public function update(Request $request, Exam $exam)
{
    $exam->update($request->validated());

    // PHP fires this to Caddy memory and dies instantly.
    // The worker is free in milliseconds.
    ExamQueueUpdated::dispatch($exam); 

    return response()->noContent();
}

Conclusion The DX is identical to using Reverb or Pusher (it's just ShouldBroadcast), but the infrastructure footprint is drastically smaller because the Go Web Server holds the connections, not the PHP Runtime.

The Discussion: I admit I might be falling for the 'Shiny New Toy' syndrome with FrankenPHP. But considering Kévin Dunglas (Symfony Core Team) is behind it, this feels less like a gamble and more like the natural evolution of PHP deployment.

My Question to the Community:

  1. The 'Safe' Choice vs. The Efficient Choice: Are most of you defaulting to Reverb simply because it's the official first-party solution? Or are you considering that for read-heavy dashboards, a WebSocket server might be unnecessary overhead?

  2. Production Readiness: Is anyone else running the FrankenPHP + Mercure stack in production yet? I am betting on this architecture because offloading connection management to Go makes perfect sense to me, but I'd love to hear if anyone has 'battle scars' or success stories to share regarding stability.

Let's discuss: Is Reverb the new standard, or is Native SSE (via FrankenPHP) the sleeper hit for 2025?