BACK TO BLOG
WebSocketsP2PGoReact

WebRTC vs WebSockets for P2P File Sharing: What I Learned

Why I chose WebSockets over WebRTC for PeerDrop, the tradeoffs involved, and how to handle large file transfers efficiently.

November 5, 20256 min read

The Initial Plan: WebRTC

When I started PeerDrop, WebRTC seemed like the obvious choice. True peer-to-peer, no server in the data path, browser-native. I spent a week implementing it.

Then I hit the real world.

Why WebRTC Failed for My Use Case

NAT traversal is unreliable. WebRTC uses STUN/TURN servers to punch through NATs. STUN works maybe 70% of the time. TURN (relay) works always, but then you're routing data through a server anyway — defeating the purpose.

No progress on large files. The DataChannel API doesn't give you reliable progress events for large transfers. You have to implement your own chunking and reassembly.

Connection setup is slow. ICE negotiation takes 2-5 seconds. For a file sharing tool, that's a bad first impression.

The WebSocket Approach

I switched to WebSockets with a Go backend. The architecture:

  1. Sender connects, gets a room code
  2. Receiver connects with the room code
  3. Server acts as a relay — it never stores the file, just streams bytes from sender to receiver
func handleTransfer(sender, receiver *websocket.Conn) {
    buf := make([]byte, 32*1024) // 32KB chunks
    for {
        n, err := sender.Read(buf)
        if err != nil { break }
        receiver.Write(buf[:n])
    }
}

Chunking for Progress

On the client, we chunk the file and send metadata first:

const CHUNK_SIZE = 64 * 1024; // 64KB

async function sendFile(file: File, ws: WebSocket) {
  // Send metadata
  ws.send(JSON.stringify({ name: file.name, size: file.size, type: file.type }));

  // Send chunks
  let offset = 0;
  while (offset < file.size) {
    const chunk = file.slice(offset, offset + CHUNK_SIZE);
    ws.send(await chunk.arrayBuffer());
    offset += CHUNK_SIZE;
    onProgress(offset / file.size);
  }
}

End-to-End Encryption

Since data now passes through the server, E2E encryption is non-negotiable. We use the Web Crypto API:

// Generate a key and share it via URL fragment (never sent to server)
const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"]
);
const exported = await crypto.subtle.exportKey("raw", key);
const keyHex = Buffer.from(exported).toString("hex");
// Share: peerdrop.app/receive/ROOMCODE#KEY

The key is in the URL fragment — it's never sent to the server in HTTP requests.

The Tradeoff

Yes, data passes through my server. But with E2E encryption, the server sees only ciphertext. And the reliability improvement is massive — 100% connection success vs ~70% with WebRTC.

For a tool where "it just works" matters more than theoretical purity, WebSockets won.

All PostsRana Dolui