skip to content

In a Laravel app using Echo, a bidder sees their own bid appear twice; how does broadcast()->toOthers() fix it, and what must be in place?

level: middleimportance: should knowfreq 34%

answer

  1. response plus broadcast echo
  2. X-Socket-ID header
  3. InteractsWithSockets trait
  4. Echo interceptors add the header
  5. fetch needs Echo.socketId()

basics

~20 s

The page adds the bid from its own HTTP response and again from the broadcast. broadcast(new BidPlaced($bid))->toOthers() excludes the sender's socket, read from the X-Socket-ID header; the event needs the InteractsWithSockets trait and the request must carry that header.

solid answer

~40 s

The bidder's page inserts the bid from the POST response and then receives the same `BidPlaced` broadcast, so it renders twice. `broadcast(new BidPlaced($bid))->toOthers()` calls the event's `dontBroadcastToCurrentUser()`, which stores `Broadcast::socket()`, the value of the request's `X-Socket-ID` header, in the event's `socket` property; the driver then skips that one connection. Two things must hold on your side: the event uses the `InteractsWithSockets` trait (otherwise `toOthers()` silently does nothing), and the request carries `X-Socket-ID` (Echo's interceptors add it for a global Axios instance, jQuery, Turbo and Vue's HTTP client; `fetch` needs `Echo.socketId()` added by hand). A custom `broadcastWith()` is safe, because Laravel merges `socket` back into it. It excludes the connection, not the user, so the bidder's other tabs still receive it.

code

php · 23 lines
php
<?php

namespace App\Http\Controllers;

use App\Events\BidPlaced;
use App\Models\Auction;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class AuctionBidController
{
    public function store(Request $request, Auction $auction): JsonResponse
    {
        $bid = $auction->bids()->create([
            'user_id' => $request->user()->id,
            'amount_cents' => $request->integer('amount_cents'),
        ]);

        broadcast(new BidPlaced($bid))->toOthers();

        return response()->json($bid);
    }
}

go deeper

for a junior

Recall that broadcast()->toOthers() stops the sender's own page from receiving the event it caused.

for a middle

Explain the X-Socket-ID header, the InteractsWithSockets trait and which HTTP clients Echo intercepts.

for a senior

Debug a toOthers() that silently fails, and decide between sender exclusion, broadcast-only rendering and client-side de-duplication.

for a principal

Define how the frontend reconciles optimistic updates with realtime events so duplicates and gaps are handled consistently across features.

## The duplicate On an auction page the bidder submits a bid with an HTTP request. A typical frontend does two things: 1. it appends the bid returned in the response, so the bidder sees it at once 2. it listens on `auctions.{id}` for `BidPlaced` so it also sees everyone else's bids The server broadcasts `BidPlaced` for every bid, including the bidder's own, so the bidder's page receives its own bid a second time over the WebSocket and renders it twice. ## What toOthers() does ```php broadcast(new BidPlaced($bid))->toOthers(); ``` - The `broadcast()` helper returns a `PendingBroadcast`; the event is dispatched when that object is destroyed at the end of the statement. - `toOthers()` calls `dontBroadcastToCurrentUser()` on the event if the method exists. That method comes from the **`InteractsWithSockets`** trait and sets the event's public `$socket` property to `Broadcast::socket()`. - `Broadcast::socket()` reads the **`X-Socket-ID`** header of the current request. - When the broadcast job runs, the socket ID travels with the payload and the driver tells the WebSocket server to deliver to every subscriber **except that connection**. The anonymous-event API has the same switch: `Broadcast::on('auctions.42')->toOthers()->send()`. ## What must be in place | Requirement | What goes wrong without it | |---|---| | Event uses `InteractsWithSockets` | `toOthers()` checks for the method and silently does nothing | | Request sends `X-Socket-ID` | the socket is null and everyone, including the sender, receives it | | Echo connected before the request | there is no socket ID yet to send | | Payload keeps the `socket` key | with `broadcastWith()`, Laravel merges `socket` back in, so this holds by default | Echo registers request interceptors when it is created, unless `withoutInterceptors` is set, that add the header automatically for: - a global Axios instance - jQuery's Ajax - Turbo requests - Vue's HTTP client A frontend that uses `fetch` or its own HTTP wrapper must add `X-Socket-ID: Echo.socketId()` itself; Echo's React, Vue and Svelte packages also expose a `useSocketId` hook for this. ## What it does not do - It excludes one **connection**, not a user. The same bidder in another tab has a different socket ID and still receives the event, which is usually what you want. - It does not stop ordinary listeners of the event from running on the server. - It does not affect other channels or other events. - If the client reconnects, it gets a new socket ID; a request sent with the old one excludes nothing. ## How the exclusion reaches the WebSocket server When the broadcast job runs, the broadcaster removes the `socket` value from the payload before sending, so it never reaches browsers, and passes it to the WebSocket server as the Pusher protocol's `socket_id` parameter. The server, Reverb included, then delivers the message to every connection on the channel except the one with that ID. That is why exclusion works only when the ID in the request matches a live connection on the same server cluster. ## Alternatives - **Rely only on the broadcast.** The page does not insert from the response and waits for the event. That removes duplicates without `toOthers()`, but the bidder's own update now depends on the queue and the WebSocket being healthy. - **De-duplicate on the client** by bid ID. It is robust against every duplicate source, including reconnect replays, and pairs well with `toOthers()`. ## Checklist when toOthers() "does not work" 1. Does the event class use `InteractsWithSockets`? 2. Does the request that triggers the broadcast carry `X-Socket-ID`? Check the browser's network tab. 3. Was Echo connected when that request was made? 4. Is the event sent through `broadcast(...)->toOthers()` rather than `BidPlaced::dispatch()`, which has no `toOthers()` step?

  • Why does toOthers() do nothing on an event that lacks InteractsWithSockets?
    `PendingBroadcast::toOthers()` only calls `dontBroadcastToCurrentUser()` if the event has that method, and the method comes from `InteractsWithSockets`. Without the trait there is no method, no error and no socket stored, so the broadcast goes to everyone.
  • Does toOthers() stop the bidder's second browser tab from receiving the event?
    No. Exclusion is by socket ID, and each tab has its own WebSocket connection. Only the connection whose ID came in the request's `X-Socket-ID` header is skipped.

saying these in an interview costs you the question

  • toOthers() excludes every connection belonging to the authenticated user.
  • toOthers() works on any broadcast event without extra traits.
  • Echo adds the X-Socket-ID header to fetch requests automatically.
  • BidPlaced::dispatch($bid)->toOthers() is how you exclude the sender.
  • Defining broadcastWith() always breaks toOthers() because the socket key is lost.