WebSockets eat up server resources. Every open connection means another persistent thread your server has to manage.
When you’re building collaborative annotation tools for medical imaging or document review, you want real-time updates but can’t always afford the infrastructure cost.
Good news: you can build responsive collaborative features in your HTML5 DICOM viewer open source without opening thousands of persistent connections.
HTTP/2, Server-Sent Events, and smart polling provide near-instant updates with lower overhead.
What makes WebSockets expensive in the first place
Each WebSocket connection holds open a dedicated server resource. If 500 users are viewing and annotating images simultaneously, that’s 500 open connections your server maintains. Cloud hosting bills scale with these connections. Memory usage climbs. Your load balancer gets complicated.
The real problem hits when connections drop. Networks fail. Users close laptops. Mobile devices switch between WiFi and cellular. Each disconnect requires reconnection logic, state recovery, and error handling. You end up writing more code to manage connection failures than actual annotation features.
For medical imaging applications, this gets worse. Radiologists might have 20 studies open across multiple monitors. Each viewer potentially needs their own connection to sync annotations between team members reviewing the same case. The connection count multiplies fast.
Server-Sent Events as your first alternative
SSE works over regular HTTP. The server keeps a connection open but only pushes data one way—from server to client.
Your viewer sends annotation updates through normal POST requests. The server broadcasts those updates to other viewers through the SSE stream.
Here’s what makes this practical: SSE connections automatically reconnect when they drop. Browsers handle this natively. You don’t write reconnection code. The connection comes back, and your viewer picks up where it left off.
SSE also plays nice with HTTP infrastructure. Proxies understand it. Load balancers don’t need special configuration. You can even use CDNs to handle some of the streaming load, though you’ll need to be careful about caching policies.
The limitation? SSE only works server-to-client. Every annotation update still requires a separate HTTP request from the client.
For applications where users make frequent small annotations—like marking up images with dozens of measurement points—this creates noticeable traffic.
But for typical collaborative sessions where updates happen every few seconds, SSE handles the load fine.
HTTP/2 server push for annotation updates
HTTP/2 lets servers push resources to clients without explicit requests. When one user adds an annotation, your server can push that update to all other viewers watching the same image.
The efficiency gain comes from multiplexing. HTTP/2 sends multiple streams over a single TCP connection. Those 500 simultaneous users? They’re sharing connection resources instead of each holding a dedicated WebSocket thread.
Implementation gets tricky, though. Not all browsers and servers support HTTP/2 push equally. Some proxies strip push promises. You need fallback mechanisms for clients that don’t support it. This adds complexity to your codebase.
Mobile networks also handle HTTP/2 push inconsistently. Cellular carriers sometimes interfere with push streams to manage bandwidth.
Your annotation system needs to detect when pushes fail and switch to polling or SSE automatically.
Smart polling that doesn’t hammer your server
Polling means your viewer checks for updates every few seconds. Sounds wasteful, but modern approaches make it viable. You use conditional requests with ETags or Last-Modified headers.
The server returns 304 Not Modified when nothing has changed. This response is tiny—just headers, no body.
Your polling interval adjusts based on activity. When users actively annotate, you poll every 2-3 seconds.
During idle periods, you back off to 30-second intervals. This adaptive polling reduces server load by 80% compared to fixed-interval approaches.
Long polling improves this further. Your viewer makes a request and the server holds it open until new data arrives or a timeout occurs.
This gives you near-real-time updates without constant request overhead. The difference from WebSockets? The connection closes after each response, freeing server resources.
For HTML5 viewers handling medical images, long polling works well because annotation updates are relatively sparse.
Even in active collaborative sessions, most of the time passes without changes. The server holds requests cheaply until actual updates occur.
Combining techniques for different annotation types
You don’t need one solution for everything. Use different approaches based on what you’re syncing.
Text annotations and measurement markers can use long polling—they update infrequently. Cursor positions showing where team members are looking might use SSE for smooth real-time tracking.
Critical annotations like urgent findings in medical images can trigger immediate POST requests that bypass normal polling cycles.
The server then pushes these priority updates through whichever channel is open—SSE stream, HTTP/2 push, or held long-poll requests.
This hybrid approach lets you optimize for each use case. Drawing tools that create many small updates benefit from batching—collect changes for 500ms, then send them together.
Slow-changing elements like approval status can use standard polling at longer intervals.
Building offline-first annotation systems
Network failures happen. Your annotation viewer should work without connectivity and sync when it returns. This actually simplifies the real-time update problem because you’re already handling delayed synchronization.
Store annotations locally in IndexedDB or localStorage. When the network is available, batch-sync your changes to the server.
Other users’ updates arrive through whatever mechanism you chose—SSE, polling, or HTTP/2 push. Your viewer merges remote changes with local annotations using conflict resolution rules.
The HTML5 DICOM viewer open source community has built several reference implementations showing this pattern. Annotations get timestamped and author-tagged.
When conflicts occur, you can show both versions or apply last-write-wins logic depending on your use case.
This offline-first design means temporary network hiccups don’t disrupt collaborative work.
Users keep annotating. Changes sync when connectivity improves. The real-time aspect becomes less critical because the system handles delays gracefully.
Performance numbers you should expect
Long polling with 5-second intervals uses about 60% less server memory than equivalent WebSocket implementations.
SSE connections consume roughly 40% fewer resources than WebSockets in typical deployments. HTTP/2 server push reduces bandwidth by 30-50% compared to polling for high-frequency updates.
These numbers vary based on your specific implementation and server stack. Node.js handles SSE more efficiently than traditional threaded servers.
Go and Rust excel at managing many concurrent HTTP/2 streams. Your results depend on what you’re running.
Client-side performance stays similar across approaches. The JavaScript processing annotations is the same whether updates arrive via WebSocket, SSE, or polling.
Battery impact on mobile devices actually favors polling because the radio can sleep between requests instead of maintaining constant connectivity.

FAQs
Can these methods handle hundreds of concurrent users?
Yes, with proper server configuration. SSE and long polling scale to thousands of users per server instance. The key is using asynchronous I/O and efficient event loops rather than thread-per-connection models.
How do you handle annotation conflicts between users?
Use operational transformation or conflict-free replicated data types. These algorithms let multiple users edit simultaneously and merge changes automatically. Last-write-wins works too if you can accept occasional overwrites.
Does this work with existing PACS systems?
Most PACS systems don’t provide real-time APIs, so you’ll need middleware that polls the PACS and exposes updates through SSE or HTTP/2. The viewer connects to your middleware rather than directly to the PACS.
What about scaling across multiple servers?
You need a message bus like Redis pub/sub or a dedicated message queue. When one server receives an annotation update, it publishes to the bus. All servers subscribe and forward updates to their connected clients through HTML5 DICOM viewer open source implementations.


