One question decides it
Reduce this decision to a single question. Does your client need to send messages over the same channel, often?
If it does not, server-sent events wins on nearly every operational axis, and the set of genuinely two-way features is smaller than product specs make it sound.
Tally what you get for free. It is plain HTTP, so it travels through balancers and proxies with no protocol-specific configuration, and any platform that can stream a response can serve it. Reconnecting with resume is built into the browser. Your server code is a loop writing to a response.
Compare what WebSocket asks for. Upgrade support at every hop, heartbeats you write yourself, reconnect and resume you write yourself, and its own security review, because it steps outside normal HTTP rules.
Resist overweighting client-to-server traffic, because that is where people get this wrong. A chat user sends a message every few dozen seconds, and a plain POST alongside the stream handles that perfectly well, riding the same underlying connection on HTTP/2 anyway.
When WebSocket earns it
Reserve WebSocket for genuinely high-frequency upstream traffic. Cursor positions in a shared editor at 20 a second, game inputs, audio. There the overhead of a request per message becomes real and two-way frames earn their complexity. Binary payloads and sub-100 millisecond round trips point the same way.
Recognise the tell that a team chose wrong, because it almost always points one direction. A WebSocket deployment where the client sends nothing but heartbeats is a server-sent events workload paying WebSocket's operational bill.
Look at how token streaming settled this in public. The large language model products stream their completions over server-sent events. That is pure server-to-client push at high frequency, and none of them found a reason to upgrade it.
Worked example
A fintech team specs real-time notifications and defaults to WebSocket because the ticket said real-time. Rohan, reviewing the design, asks what the client ever sends over the channel: the answer is mark-as-read, which happens maybe five times a day per user and already exists as a REST endpoint. He reworks the design as one SSE endpoint fed by their existing Kafka topic, roughly 200 lines of server code, shipped in three days against the two sprints estimated for the WebSocket version with its sticky sessions and custom reconnect protocol. Six months in, the stream serves 60,000 concurrent connections from four pods, notifications land in under 400 ms end to end, and the mark-as-read POST works exactly as it always did.