Why Is P2P File Transfer Slow? How to Find the Bottleneck
By SRIFT maintainers · Published
Reviewed by SRIFT maintainers · last reviewed against the SRIFT 4.3.2 source · third-party facts checked 8 October 2026.
Short answer
A peer-to-peer transfer runs at the speed of its slowest stage: reading the file, encrypting and chunking it, the sender's upload, the network path, the receiver's download, and writing to disk. On home and mobile connections the usual limit is the sender's upload bandwidth, which is often much lower than its download. The next suspects are a relayed path (TURN or a WebSocket relay), Wi-Fi, a VPN, and endpoint problems such as a throttled background tab or a slow disk.
Guessing wastes time. Measure each stage on its own, check which path the connection actually took, and fix only the narrowest one.
- Usual cap
- The sender's upload bandwidth
- Where to look
- chrome://webrtc-internals or about:webrtc: the selected candidate pair
- Rarely the cap
- AES-GCM on a desktop CPU with hardware AES
- Often overlooked
- Background-tab throttling and too little data in flight
A transfer is a pipeline: the slowest stage sets the speed
Every byte passes through the same sequence of stages, and each stage has its own capacity. Throughput is the minimum of those capacities, not their average. A gigabit fibre download on the receiving side does nothing if the sender is uploading over a connection with a fraction of that, and a fast network does nothing if the receiving phone writes to storage slowly or the sending tab has been throttled in the background.
This is why “remove the server and it gets faster” is only sometimes true. A direct path removes a hop, but if the hop was not the narrowest stage, nothing changes. The method in this article is simple: estimate the capacity of each stage, find the smallest, and only then change something.
Network limits: upload, download, Wi-Fi and VPNs
The sender's uplink
Many residential and mobile plans are asymmetric: upload is a fraction of download. A peer-to-peer transfer is an upload from the sender, so run a speed test on the sending device and note the upload figure. If the transfer runs close to it, you have found the cap, and no software setting will raise it. Two practical fixes: send from the better-connected machine (an office or a cloud VM rather than a phone on cellular), or move the sending device to a wired connection.
The receiver's downlink
Less often the limit, but check it on mobile data, on congested shared networks, and when someone else on the receiving network is downloading heavily. Both tests matter because the transfer gets the lower of the two.
Wi-Fi contention and distance
The link rate your operating system displays is a signal-quality estimate, not throughput. Wi-Fi is a shared medium: every device on the channel, and neighbouring networks on the same channel, compete for airtime, and the rate drops as signal weakens with distance and walls. When both devices are on the same access point, each packet crosses the air twice (sender to access point, access point to receiver), so a same-room transfer can be slower than either device's speed test suggests. Wiring one side, moving closer, or using the 5 GHz or 6 GHz band usually helps more than any software change.
VPNs
A VPN adds encapsulation overhead and sends traffic through its own server, which may be far from both peers. Some VPN clients also block UDP outside the tunnel, which pushes WebRTC from a direct path onto a TURN relay or stops it from connecting at all (see why WebRTC connections fail). If a transfer is slow only with the VPN on, test with it off or with split tunnelling for the browser.
Which path did the connection take?
A WebRTC data channel can run over three kinds of path, and a relay-based tool adds a fourth:
- Direct, host or server-reflexive candidates. Packets go from one peer's network to the other's. The fastest option when it works.
- TURN over UDP. Traffic goes through a TURN server (RFC 8656). It adds a hop, the round-trip time to that server, and the server's own capacity, but keeps UDP semantics.
- TURN over TCP or TLS. Used when UDP is blocked. The data channel's SCTP now runs inside DTLS inside TCP, so a single lost packet stalls everything behind it (head-of-line blocking) and two congestion controllers react to the same loss. Expect it to suffer most on lossy networks.
- An application relay over WebSockets. No WebRTC at all: both peers hold TCP connections to a server that forwards chunks. Speed depends on each side's route to that server and on how the application frames data. Sending binary inside JSON as base64 inflates every chunk by a third (RFC 4648 encodes 3 bytes as 4 characters) and costs CPU to encode and decode.
Reading the selected candidate pair
In Chrome or Edge, open chrome://webrtc-internals (or edge://webrtc-internals) in a new tab before starting the transfer, then expand the peer connection. In the stats, find the candidate-pair that is nominated with state succeeded and follow its local and remote candidate IDs. In Firefox, about:webrtc lists the ICE candidate pairs for each connection and marks the selected one. What to read:
candidateTypeof each side:hostorsrflx(orprflx) means direct;relayon either side means TURN.relayProtocolon a relay candidate:udp,tcportls.currentRoundTripTimeon the pair. A round trip much longer than a ping between the two networks suggests a distant relay.
The same information is available from the page itself through the statistics API:
const stats = await pc.getStats();
let pair;
stats.forEach((r) => {
if (r.type === "transport" && r.selectedCandidatePairId) pair = stats.get(r.selectedCandidatePairId);
});
// Fallback for browsers without transport stats
if (!pair) stats.forEach((r) => {
if (r.type === "candidate-pair" && r.nominated && r.state === "succeeded") pair = r;
});
const local = stats.get(pair.localCandidateId);
const remote = stats.get(pair.remoteCandidateId);
console.log(local.candidateType, local.relayProtocol ?? local.protocol,
"->", remote.candidateType, "RTT s:", pair.currentRoundTripTime);The field names come from the W3C WebRTC statistics specification. For the difference between the candidate types, see STUN vs TURN.
Endpoint limits: CPU, buffering, storage and background tabs
Encryption and CPU
AES-GCM is rarely the bottleneck on a modern laptop or desktop, because x86 processors with AES-NI and ARMv8 processors with the cryptography extensions run it in hardware. It can become the limit on low-end phones, older devices without those instructions, or when the JavaScript around it is inefficient: copying buffers several times, base64 encoding, or encrypting tiny pieces one at a time. The tell-tale sign is one CPU core near 100% on the slow device while the network sits partly idle. Moving the transfer to a stronger machine, or to a command-line client, fixes it.
Too little, or too much, data in flight
A reliable sender that waits for each acknowledgement before sending the next chunk is limited to one chunk per round trip. With 64 KB chunks and a 50 ms round trip, that is 64 KB / 0.05 s, about 1.3 MB/s, regardless of the bandwidth available. Throughput can only approach the link rate when the data in flight covers the bandwidth-delay product (bandwidth multiplied by round-trip time), so a sender needs a window of several chunks or a deep enough send buffer.
The opposite mistake is just as common. RTCDataChannel.send() never blocks; it queues data and raises bufferedAmount. A loop that ignores it can queue gigabytes in memory, after which the tab slows to a crawl, garbage collection stalls the event loop, or the channel closes. The correct pattern is a high-water mark: stop sending above it, and resume on bufferedamountlow. Very small messages add per-message overhead and event loop work, so tiny chunks also cost throughput. The WebRTC data channel guide covers message sizes and backpressure in detail.
Disk writes and browser storage
The receiver must write as fast as data arrives. A full or nearly full disk, a slow SD card or USB stick, or a browser that is buffering the file in memory because it has no streaming path will all show up as bursts of progress followed by pauses. In a browser, a fast receiver writes through a origin private file system (OPFS) handle in a worker, or streams the file into the browser's download manager through a service worker. Holding chunks in memory and building a Blob at the end is fine for small files and the wrong design for large ones.
Background tabs and power saving
Browsers throttle hidden pages. Chrome's documented rules check chained timers in a hidden tab at most once per second, and after five minutes hidden, once per minute (“intensive throttling”), unless the page has an active WebRTC connection or has played sound recently (Chrome timer throttling). A transfer whose sending loop is paced by timers can slow sharply when its tab is in the background, particularly when no WebRTC connection is open (for example on a WebSocket relay), because the WebRTC exemption then does not apply. Mobile operating systems go further and suspend background pages, and battery-saver modes reduce CPU and network activity. Keep the sending tab visible and the device plugged in for long transfers.
Step-by-step diagnosis
Work through the table from the top. Each row is a symptom you can observe, a check that confirms the cause, and the fix. Stop at the first row that matches: fixing a stage that is not the narrowest one will not help.
| Symptom | Check | Fix |
|---|---|---|
| Speed is steady and close to a fixed value | Run a speed test on the sender; compare its upload figure with the transfer rate. | If they match, that is the cap. Send from a better-connected machine or wire the sender. |
| Fast on one network, slow on another | Selected candidate pair in chrome://webrtc-internals or about:webrtc: is either side type relay? | Allow UDP on the slow network, disconnect the VPN, or accept relay speed. |
| Relay candidate with relayProtocol tcp or tls | Same stats entry; also try another network. | UDP is blocked. Allow outbound UDP to the TURN server, or expect lower speed on lossy links. |
| Two devices in the same room transfer slowly | Both on Wi-Fi? Is the selected pair host-to-host or relay? | Wire one side or move closer to the access point. If the pair is relay, see the connection-failure guide. |
| Slow only when the VPN is on | Compare a transfer and a speed test with the VPN on and off. | Split-tunnel the browser or pause the VPN during the transfer. |
| Speed collapses when the tab goes to the background or the screen locks | Keep the tab in front and repeat. | Keep both tabs visible, disable battery saver, keep the screen on for phones. |
| One CPU core near 100% on one device, network partly idle | Task Manager, Activity Monitor or top during the transfer. | Use a stronger device or a command-line client; close other heavy tabs. |
| Progress comes in bursts; receiver memory keeps climbing | Free disk space and the browser's memory use on the receiver. | Free space, use a browser that streams to disk, or receive with a CLI. |
| Own code: rate equals chunk size divided by round-trip time | Count chunks in flight; log bufferedAmount over time. | Send a window of chunks; use a bufferedAmount high-water mark and bufferedamountlow. |
How SRIFT behaves, and what speed to expect
SRIFT browser sessions use a relay-first design with a WebRTC head start. When a transfer starts, the browser checks whether a direct path is even possible: a short probe that succeeds if a STUN server returns a server-reflexive candidate (UDP works), or TURN credentials being available. If either holds, it opens a direct WebRTC data channel and gives it 3 seconds. If the channel is not open by then, or WebRTC is not possible on the network, the transfer runs over SRIFT's WebSocket relay. The browser console logs this decision on a line starting with Relay-first: that shows whether UDP and TURN were detected.
| Path | Parameters |
|---|---|
| Direct (WebRTC) | Messages sized from the negotiated maximum (16 KB to 256 KB, 64 KB default); file read in 4 MB slices; sender pauses above 8 MB buffered and resumes at 2 MB; the receiver pauses the sender when more than 32 MB is waiting to be written. |
| Relay (WebSocket) | 256 KB chunks sent as base64, 16 chunks in flight with a sliding window of acknowledgements; each chunk sealed with AES-256-GCM. |
| Relay speed | The 4.3.2 changelog reports about 8-13 MB/s measured locally, up from about 2 MB/s with the older format. That is a local measurement, not a guarantee: across the internet both sides' links and their routes to the server set the ceiling. |
| Receiver memory | The changelog records a 20 GB browser-to-browser transfer with about 100 MB of page memory, using streaming into Downloads. |
Two consequences for speed. First, a transfer that starts on the relay, or falls back to it when the direct path fails mid-way, stays on the relay: SRIFT continues from the bytes already received and does not switch back, because changing transports mid-stream is where ordering bugs hide. Second, the relay window of 16 chunks of 256 KB keeps 4 MB in flight, which by the bandwidth-delay arithmetic above allows a theoretical 80 MB/s at a 50 ms round trip. On typical consumer connections the window is therefore not what limits the relay; the two access links and their routes to the server are.
Frequently asked questions
Is a TURN relay always slower than a direct connection?
Not always. TURN over UDP through a nearby, well-provisioned server adds a hop and a little round-trip time, and is often close to direct speed. TURN over TCP or TLS, a distant TURN server, or a busy one is where the drop becomes large.
Does end-to-end encryption slow down a file transfer?
On desktop and laptop CPUs with hardware AES instructions, AES-GCM is rarely the limit. On low-end phones, or in code that copies and base64-encodes every chunk, the CPU work around encryption can become the bottleneck.
Why is a transfer between two devices on the same Wi-Fi slow?
Each packet crosses the air twice, sender to access point and access point to receiver, and shares airtime with every other device on the channel. If the selected candidate pair is a relay rather than host-to-host, the traffic is also leaving the building and coming back.
References
Try a direct transfer
Open a session in one browser, join it from another device, and send a file. No account, nothing to install on either side.
Related resources
- Why WebRTC Connections Fail, and How to Fix ThemWhy two browsers sometimes cannot connect directly: ICE failures, blocked UDP, symmetric NAT and missing TURN, with a diagnosis checklist.Troubleshooting
- How WebRTC File Transfer Works: Data Channels in PracticeWebRTC data channels explained for file transfer: SCTP over DTLS, message sizes, backpressure with bufferedAmount, ordering, and what breaks at scale.Explainer
- STUN vs TURN: How WebRTC Gets Through NATWhat STUN and TURN servers do, how ICE picks between host, server-reflexive and relay candidates, and what a TURN relay can and cannot see.Explainer
- Large File Transfers: Interruptions, Resume and IntegrityWhy multi-gigabyte transfers fail, how browsers store incoming files, what resume really requires, and how to verify the file that arrived.Troubleshooting
