Skip to main content
Saved
Guide strategy Intermediate 18 min read

Choosing a Networking Style for Realtime Apps

How to choose between polling, Server-Sent Events, WebSockets, and CRDTs, depending on how realtime your app needs to be and whether users edit together.

Realtime can mean a lot of different things, from a dashboard that refreshes every thirty seconds to a collaborative editor where each keystroke appears straight away on a colleague’s screen. The networking approach you choose affects what your users experience, and it also affects how complex your infrastructure gets and how much it costs to run.

None of these technologies is the best choice in every case. WebSockets aren’t automatically better than polling, and CRDTs aren’t always the right choice for collaboration. You’re looking for the approach that fits your own requirements and your team.

Over the years, I’ve built notification systems that needed sub-second delivery and collaborative tools where several users edited the same document at the same time. Each of those projects taught me that the right networking choice depends on what “realtime” means for your users, and on how much complexity your team can realistically maintain.

Clarify What Realtime Means for Your Application

Before I compare technologies, I find it helps to pin down what kind of realtime behavior the application needs, because different use cases need very different things.

With periodic freshness, users need reasonably current data, but a delay of seconds or even minutes is acceptable. Social media feeds and analytics dashboards often fall into this category. Users expect the data to update, but they don’t expect it to stay in sync instantly.

With live updates, users should see changes shortly after they happen, usually within a second or two. Notification systems, live sports scores, auction bidding interfaces, and order tracking fall into this group. Users notice a delay of more than a few seconds, but a short one is fine.

With realtime collaboration, several users work on the same data at the same time and expect to see each other’s changes as they happen. Collaborative document editors, shared whiteboards, multiplayer games, and live coding environments all need this level of synchronization. Even a few hundred milliseconds of latency can make them feel sluggish.

Offline-capable collaboration adds another requirement. Users need to keep working while they’re disconnected, and their changes have to merge correctly when the connection comes back. Field service applications and note-taking tools often need this.

If you get your requirements wrong, you can end up over-engineering, such as building WebSocket infrastructure for a dashboard that would work fine with polling. Or you might under-engineer, by building polling for a collaborative editor and then hearing users complain about lag and lost work.

Understand the Core Options

Polling

Polling is the simplest approach. The application makes an HTTP request at a regular interval to fetch the current data, the server responds with the latest state, and the client updates the UI.

Choose polling when:

  • Data changes infrequently, perhaps every few minutes or hours
  • Users are fine with a few seconds of latency
  • You want the simplest possible implementation
  • Your infrastructure makes persistent connections hard to support

What to watch for: Polling wastes server work when the data hasn’t changed. If you poll every five seconds but the data only changes once an hour, 99.9% of your requests return the same response. Each user you add sends another stream of requests, however rarely the data changes.

Improving efficiency: With long polling, the server keeps each request open until the data changes or a timeout passes, so fewer requests are wasted. The Stale-While-Revalidate pattern can show cached data straight away while it fetches updates in the background, which makes polling feel more responsive.

I start with polling for any realtime requirement. If it works well enough, you avoid the complexity of persistent connections completely, and you only need something more sophisticated once polling causes problems you can measure.

Server-Sent Events

With Server-Sent Events (SSE), the client opens a persistent HTTP connection, and the server pushes data down it whenever there’s an update. Unlike polling, the server starts the communication when something changes, so the client doesn’t have to keep asking.

Choose SSE when:

  • Updates mostly go from the server to the client
  • You need lower latency than polling gives you
  • You want simpler infrastructure than WebSockets require
  • Your updates fit naturally into a stream of separate events

What to watch for: SSE only sends data from the server to the client. If your application needs to send messages from the client to the server often, you’ll need separate HTTP requests for that direction, and those can take away much of the simplicity you chose SSE for.

Infrastructure considerations: SSE works over standard HTTP, so it goes through existing proxies and CDNs without any special configuration. In many environments, that makes SSE much easier to run than WebSockets. Some older proxies might still buffer SSE responses or time out long connections, though, and you’ll need workarounds for those.

SSE is a good fit for many applications, because it gives you push-based updates with much less complexity than WebSockets. Notification feeds and progress indicators for long-running operations both work well with it.

WebSockets

WebSockets open a persistent, bidirectional connection between the client and the server. Either side can send a message at any time, without the overhead of a separate HTTP request and response for each one.

Choose WebSockets when:

  • You need bidirectional communication with low latency
  • Messages go back and forth often in both directions
  • Users need sub-second latency
  • You’re building interactive features such as chat or live collaboration

What to watch for: WebSockets need more infrastructure work. Your servers and load balancers have to be configured for persistent connections. The client also has to manage the connection’s lifecycle, because connections can drop without warning, so you need reconnection logic that backs off between attempts.

Scaling considerations: Each WebSocket connection uses server memory and a socket for as long as it’s open, even when it’s idle. With polling, the limit is how many requests a server can handle per second. With WebSockets, the limit is how many connections the server can keep open at once, and each user who has the page open adds one, whether they’re active or not. To scale across several servers, you need sticky sessions or a message broker that routes each message to the right server.

WebSockets are the right choice when you need bidirectional, low-latency communication. I’ve also seen teams adopt them for applications that would have worked fine with SSE or even polling, and then struggle with operational complexity they didn’t need.

CRDTs and Operational Transforms

When several users edit the same data at the same time, you need a way to handle those concurrent edits. The two common approaches are Operational Transformation (OT) and Conflict-free Replicated Data Types (CRDTs).

Operational Transformation records each change as an operation, such as inserting a character at position 5 or deleting characters 10-15. When two edits happen at once, the algorithm adjusts each operation to take the other one into account. Google Docs is a well-known example of this approach. OT usually needs a central server to put the operations in order. That makes conflict resolution simpler, but each client then depends on that server being available, and on how quickly it responds.

CRDTs are data structures designed, with mathematical guarantees, so that concurrent changes always merge without conflicts. Each client can apply changes locally and sync with the others in its own time, and all the clients are guaranteed to end up with the same data eventually. CRDTs work well for offline-capable applications, because changes merge correctly however late they arrive and in whatever order.

Choose CRDTs or OT when:

  • Multiple users edit the same document or data structure simultaneously
  • Users expect to see each other’s changes in near real-time
  • Merge conflicts have to be resolved automatically, without asking the user
  • You need character-by-character or fine-grained synchronization

What to watch for: Both approaches add a lot of complexity. OT is notoriously hard to implement correctly, and subtle bugs can make documents drift apart in ways that are hard to detect and repair. CRDTs are easier to reason about mathematically, but they can produce merge results that surprise users, and they can grow in size over time because they keep track of history.

For collaborative text editing, I recommend an established library instead of implementing these algorithms yourself, such as Yjs, which is a CRDT library, or ShareDB, which uses OT. There are a lot of edge cases, and debugging them is painful.

Choose a Conflict Handling Strategy

When several users can edit the same data, conflicts are going to happen. However good your networking infrastructure is, two users will eventually make changes at the same time, and the way you handle those conflicts affects the user experience as much as the networking technology does.

If you know when each of the three strategies below fits, you can avoid losing data without adding complexity you don’t need.

Last-Write-Wins

The simplest way to resolve a conflict is to accept the most recent change and discard any earlier changes made at the same time. The server timestamps each update, and when two updates conflict, the one with the latest timestamp wins.

Choose last-write-wins when:

  • The data is low-stakes and easy to recreate
  • Changes are infrequent, so conflicts are rare
  • Users are unlikely to edit the same record simultaneously
  • You need the simplest possible implementation

What to watch for: Last-write-wins silently discards work. If two users spend ten minutes editing the same document and submit at nearly the same time, one of them loses their changes completely. When a user has put effort into the content, that feels like a bug to them, even though the system is working as designed.

Improving the experience: When the server rejects a change because there’s a newer version, it can return the current content so the client can show what changed. Users can then reconcile their work manually, instead of finding out later that their changes have disappeared.

Last-write-wins works surprisingly well for many applications, because things like user profile settings and metadata fields rarely get edited by two people at the same time. When conflicts are that rare, its limitations don’t cause many problems, and it’s simple to build.

Show Conflicts for User Resolution

When the data is valuable enough that you can’t let one change silently overwrite another, you can detect conflicts and ask users to resolve them manually. That way you keep every version, and people decide for themselves how to merge the changes.

When a client submits changes based on an outdated version, the server rejects the update and returns both versions. The client then shows a comparison where the user can choose which changes to keep, or merge them manually.

Choose conflict dialogs when:

  • The data is valuable and took a lot of effort to create
  • Users would be upset to lose their changes silently
  • Automatic merging would produce confusing results
  • The content is complex enough that only a person can merge it sensibly

What to watch for: Conflict dialogs interrupt the user’s work, and if conflicts happen often, users might get frustrated by the constant interruptions. The dialog should explain clearly what happened and what each option does. A vague message such as “a conflict occurred” leaves users unsure what to do.

Design considerations: It helps to show a meaningful comparison where you can. For text, the dialog can highlight what changed between the versions, and for structured data, it can list which fields are different. If the content is important enough, you can also give users the option to keep both versions.

Git’s merge conflict interface is a well-known example of this pattern. It works because conflicts are fairly rare, and code is valuable enough that resolving conflicts automatically would be risky. If your data is similar, this approach makes sense.

Local Lock While Editing

Instead of resolving conflicts after they happen, you can prevent them by making sure only one user can edit a resource at a time. When a user starts editing, they get a lock that stops anyone else from making changes until they’re done.

Before a user enters edit mode, the client requests a lock from the server. If the server grants it, other users see the document as read-only, or see a message saying someone else is editing it. The lock is released when the editing user saves or abandons their changes.

Choose locking when:

  • Edits take a while, minutes instead of seconds
  • The editing experience doesn’t work well with interruptions
  • Users expect exclusive access while working
  • Merge conflicts would be hard or impossible to resolve

What to watch for: A lock can go stale if a user abandons their session without releasing it, so locks need to expire automatically. If you show who has the lock and when it expires, other users know roughly how long they’ll have to wait, and a “break lock” option for administrators helps when a lock gets stuck.

Improving the experience: The client can send heartbeats to extend the lock while the user is still editing, so the lock doesn’t expire too early, but an abandoned lock still expires on schedule. If waiting users can see the lock status update live, they know as soon as they’re able to edit.

Locking can feel old-fashioned next to real-time collaboration, but it’s still the right choice in many situations. Complex forms, and workflows where a half-finished change would cause problems, both work well with only one person editing at a time.

Choosing Between Conflict Strategies

The right conflict strategy depends on what the data is and how users work with it.

Consider last-write-wins when edits are quick and independent. A user changing their display name doesn’t conflict with someone else changing the email notification preferences. For settings and metadata, where the fields don’t depend on each other, last-write-wins works well when you apply it field by field.

Consider user-resolved conflicts when the data took the user real effort to create. If users spend time writing content, they should be able to see and control what happens to it in a conflict. Long-form text and complex configurations are both better off with a person deciding how each conflict gets resolved.

Consider locking when the editing experience is immersive or time-consuming. If users spend a long time in a focused editing mode, interrupting them with conflict dialogs would be jarring. Visual editors and multi-step wizards often work better with a lock, so conflicts can’t happen in the first place.

You can combine strategies within the same application. A project management tool might use last-write-wins for task status changes and locking for the visual project timeline editor.

Data CharacteristicsRecommended StrategyExample
Quick edits, low stakes, rarely concurrentLast-write-winsUser settings, status fields
Valuable content, users would notice lost changesUser resolutionComments, descriptions, documents
Immersive editing, extended sessionsLockingForm builders, visual editors
Real-time collaboration expectedCRDTs/OTShared documents, whiteboards

Whatever strategy you choose, users need to know what’s going on. They should get a warning when their changes might be discarded, and after a conflict, they should be told what happened and what their options are. The worst experience is when a user’s work disappears and they don’t understand why.

Factor in Offline Requirements

If users need to work offline, the architecture of a realtime application changes a lot.

If your application has to work while it’s disconnected, you need local storage for pending changes, and a conflict resolution strategy for when the connection comes back. The UI also has to show the sync status clearly. CRDTs or similar eventually consistent approaches usually fit best here, because they were designed for this situation.

If you don’t need offline support, you have more options. An approach where the server is always the source of truth is simpler, because there aren’t different versions of the data to reconcile, and clients just show what the server sends them.

I’ve seen teams add offline capability as an afterthought and struggle with it. If your users need to work offline, it’s much easier to design for that from the beginning. If they don’t, there’s no reason to add complexity for a requirement you don’t have.

Plan for How Each Approach Fails

Each networking approach fails in its own way, and if you know how, you can decide how much error handling to build.

Polling failures are straightforward. A request fails and you retry until one succeeds or you give up. In the meantime, the user sees stale data. That simplicity is an advantage that’s easy to underrate.

SSE failures interrupt the event stream. The browser’s EventSource API reconnects automatically, but the client misses any events sent while it was disconnected. To recover them, the client usually sends a last-event-id, and the server uses it to replay the events the client missed.

WebSocket failures can be harder to spot. A connection can look open when it’s actually broken, which is called a “zombie connection.” Heartbeats can detect this, but how they’re implemented varies. When a connection drops, the client needs reconnection logic with exponential backoff, so that clients reconnecting all at once don’t overwhelm the servers during an outage.

CRDT failures work differently. Each operation always succeeds locally, but it might not sync with other clients straight away, or at all. Clients that are cut off from each other might drift apart for a long time, so the UI needs to show the sync status clearly enough that users know whether other people have their changes yet.

How much error handling you build depends on what your users can put up with. For a casual social feature, briefly showing stale data is fine. For a collaborative document where users might lose work, you need careful reconnection and recovery, with clear feedback to the user.

Evaluate Your Infrastructure Constraints

Your existing infrastructure might limit your choices more than you’d like.

Serverless environments such as AWS Lambda or Vercel Functions handle HTTP requests well, but they don’t support persistent connections. If you’re committed to serverless, you’ll need a managed service for WebSocket support, or you’ll be limited to polling or short-lived SSE connections.

Corporate networks sometimes block WebSocket connections, or have proxies that close long-lived HTTP connections after a short timeout. If most of your users work in enterprise environments, it’s worth testing your approach on realistic network conditions.

Mobile networks are unreliable, and their latency varies a lot, so persistent connections will drop often. Whichever approach you choose needs to reconnect cleanly, and it’s worth thinking about what users see while the connection is poor.

Global distribution makes realtime architectures more complicated. If your users are all over the world but your servers are in one region, the extra latency starts to affect them. Polling copes with that reasonably well, but interactive collaboration feels noticeably slower.

I recommend testing your chosen approach under realistic conditions early in development. It’s an unpleasant surprise to find out that your corporate users can’t open WebSocket connections after you’ve built your whole architecture around them.

Match Complexity to Team Capability

How sophisticated your networking approach is should depend on how well your team can debug and run it.

Polling only needs skills your team already has, such as making HTTP requests and managing state, so there’s nothing new to learn or debug.

SSE is a small step up. The EventSource API is straightforward, but you need to handle reconnection and missed events. The server side is simple to build if you’ve worked with streaming responses before.

With WebSockets, your team has to understand the connection lifecycle, implement heartbeats, handle reconnection, and possibly run a message broker so the app can scale across several servers. Teams that are new to WebSockets often underestimate how much operational work they need.

CRDTs and OT are the most complex of these approaches. Even with an existing library, you need to understand the data structures well enough to debug sync problems. Building them from scratch can take months, even for experienced teams.

I’d start simpler than you think you need, because it’s much easier to add complexity when your requirements call for it than to remove it later.

Quick Decision Guide

Your SituationRecommended ApproachReason
Data changes every few minutes, brief staleness is acceptablePollingSimplest implementation, easy to debug
Server pushes updates, client rarely sends dataServer-Sent EventsPush-based without WebSocket complexity
Frequent bidirectional messages, low latency requiredWebSocketsMessages go both ways on one connection
Multiple users editing the same content simultaneouslyCRDTs or OT, using a libraryHandles concurrent edits correctly
Must work offline with eventual syncCRDTsDesigned for offline-first apps
Corporate/restricted network environmentsPolling or SSEWorks better with proxies and firewalls

Combining Approaches

Real applications often use different approaches for different features.

A project management tool might use polling for the main dashboard, and WebSockets with CRDTs for collaborative editing of the documents inside tasks. That way, each feature gets the networking approach that suits it, instead of everything going through one solution.

When you combine approaches, it’s worth deciding deliberately which data goes through which channel. If the same update accidentally arrives through both polling and WebSockets, it can cause subtle bugs. It helps to keep the channels clearly separate, such as sending notifications through one channel and document state through another.

Signals That Your Approach Needs to Change

These signs usually mean you’ve outgrown your current approach:

  • Users complain about latency even when polling intervals are as short as your servers can handle. That’s a sign it’s time to move to a push-based approach.
  • Server costs keep climbing as you add users, even though the data rarely changes. A push-based approach can cut down the unnecessary requests.
  • Users lose work because concurrent edits overwrite each other. You need a proper Conflict Resolution strategy, possibly using CRDTs or OT.
  • Connection management takes up most of your debugging time. If WebSockets are causing more problems than they solve, it’s worth checking whether SSE or even polling would be enough.
  • Offline users can’t work effectively. If working offline has become important, you might need to move to CRDTs or a similar eventually consistent approach.

It’s better to respond to the problems you actually have than to the ones you expect, and to add complexity only when you can see your current approach causing trouble.

Decision Checklist

Before you commit to an approach, it helps to work through these questions:

What latency do users actually need? What users say they want can be different from what actually affects their experience. It’s worth testing with realistic delays before you decide you need sub-second updates.

Which direction does data flow? If updates mostly go from the server to the client, SSE might be simpler than WebSockets. If messages go back and forth often, WebSockets become a better fit.

How do you handle concurrent edits? If several users can change the same data, you need a conflict strategy. Optimistic Updates, with the server reconciling the result, work in many cases, but real collaborative editing needs CRDTs or OT.

What happens when users go offline? If they need to keep working, you need local storage and a way to merge changes. If they don’t, an approach where the server is the source of truth is simpler.

Can your infrastructure support it? It’s worth checking that your hosting and proxies support the approach before you build around it.

Can your team operate it? It helps to be honest about how much experience your team has with the approach you’re considering. A simpler solution your team can debug is better than a sophisticated one it can’t.

What’s your migration path? If your networking code is kept behind a clean interface, you can switch to a different approach later if your requirements change.

The right networking approach is usually the simplest one that meets your real requirements, and you can add complexity later, once you have evidence that you need it.

Next Guide · 8 min read

Choosing a State Management Approach

How to choose between local state, lifted state, dependency injection, and global state libraries, based on what your application needs.

Read the next guide →

Newsletter

A monthly email
from Den Odell.

Behind-the-scenes thinking on frontend patterns, site updates, and more.

No spam. Unsubscribe anytime.