Why Large Files Crash Browser Tabs (and How Streaming Fixes It)
Ask anyone who has built browser file transfer: small files are trivial, and large files are where tabs die. The crashes trace back to one root cause, buffering an entire file in memory, showing up in three different places. This article covers all three, the browser APIs that exist to avoid them, the parameters ShareDrop.org settled on and why, and what the receiving side can and cannot do on each browser.
Failure point 1: reading
The naive approach reads the whole file into an ArrayBuffer. A 4 GB video on a laptop with 8 GB of RAM ends the story right there. The fix is streaming: file.stream() yields chunks on demand, so the amount of file in memory at any moment stays constant regardless of file size. Every browser has supported this for years; the failure is a design choice, not a platform limit.
Failure point 2: sending faster than the network
A disk reads at hundreds of MB/s; a typical uplink carries a fraction of that. Push chunks into a WebRTC data channel as fast as you read them and the channel's internal send buffer absorbs the difference — growing until the tab is killed. The standard fix is backpressure via bufferedAmount: pause reading when the buffered amount crosses a high-water mark, resume on the bufferedamountlow event. ShareDrop.org pauses at 8 MiB and resumes at 1 MiB, so sender memory stays bounded whether the file is 10 MB or 100 GB, and whether the link is fiber or hotel Wi-Fi.

bufferedAmount over time with ShareDrop.org's production watermarks. Disk fills the buffer in moments; the network drains it slowly; the sawtooth never exceeds 8 MiB no matter how large the file is.Why 8 and 1
The high-water mark is a bandwidth-delay calculation. On a fast LAN the pipeline moves about 25 MB/s, so 8 MiB is roughly a third of a second of data queued ahead — enough that a brief hiccup in disk reads never starves the network, and far more than a typical round trip needs to keep the link full. Larger buys nothing except memory and a longer wait when the user cancels. The low-water mark of 1 MiB leaves a comfortable amount still in flight when reading resumes, so the network never idles between refills. On a 30 Mbps uplink the same 8 MiB is two seconds of data — still bounded, still fine. The values are not magic; they are sized so that even a fast LAN link stays saturated between refills.
Failure point 3: receiving
The receiver has the hardest problem: browsers historically offered no way to write a download progressively — you accumulated Blobs in memory and triggered a download at the end. That works until it doesn't, and “doesn't” arrives at different sizes on different devices. Several paths exist today, with very different support:
| Approach | Where it works | Memory | Catch |
|---|---|---|---|
File System Access API (showSaveFilePicker, showDirectoryPicker) | Chrome and Edge on desktop | Flat: chunks go straight to a file the user chose | Not on Firefox, Safari or any mobile browser; the user must pick a destination first |
| Origin-private file system (OPFS) | Chrome, Edge, Firefox, Safari (desktop and mobile) | Flat while receiving: the page writes to a private, disk-backed store | The file is invisible to the user until exported; subject to storage quota; Safari evicts it after seven days without a visit |
| Service-worker streamed download | Most browsers, in principle | Flat | Fragile: depends on the download manager behaviour of each browser and breaks on updates |
| In-memory Blob assembly | Everywhere | Grows with the file; can briefly approach twice the size at assembly | Fails unpredictably somewhere between a few hundred MB and a few GB |
ShareDrop.org uses the first path where it exists, because it is the only one that writes to a location the user chose and keeps folder structure. Everywhere else it currently uses in-memory assembly with a hard cap, 512 MB per file, communicated before the transfer starts, rather than letting users discover the limit as a crash at 97%. OPFS staging would lift that cap on Firefox and Safari and is the obvious next step; its quota and eviction behaviour are why it is not there yet. The native iOS and Android apps have none of these constraints: they write to storage as data arrives.
| Receiving on | Saving path | Memory while receiving | Practical limit |
|---|---|---|---|
| Chrome / Edge (desktop) | Streamed to disk (File System Access API) | Flat — a few MiB of chunk buffer | Disk space |
| Firefox (desktop) | In-memory assembly | Grows with the file | 512 MB, enforced before the transfer starts |
| Safari (macOS / iOS) | In-memory assembly | Grows with the file | 512 MB, enforced before the transfer starts |
| Chrome on Android, other mobile browsers | In-memory assembly | Grows with the file | 512 MB, enforced before the transfer starts |
| ShareDrop.org iOS / Android app | Streamed to storage | Flat | Free space on the device |
What a streamed write actually does
On the streamed path the page calls showSaveFilePicker() once (the browser shows its native save dialog, the user picks a location), and receives a file handle. From that handle it opens a writable stream and writes each chunk as it arrives. Two details make this safe. The browser writes to a temporary swap file, not the final name, and only renames it into place when the stream is closed; a crash or a cancelled transfer leaves no half-written file where the user expected a complete one. And the handle is scoped to the one file the user chose: the page cannot write anywhere else, cannot read other files, and loses the permission when the tab closes. For a folder transfer the same applies to showDirectoryPicker(): one prompt, one folder, and the page may create files and subfolders inside it and nothing outside.
The origin-private file system, in more detail
OPFS is a per-site storage area on disk that every current browser provides, including Safari on iOS. A page can write to it with ordinary streams or, in a worker, with a synchronous access handle that is fast enough for real workloads. The received file could be staged there chunk by chunk, then handed to the download manager as a disk-backed File object, which does not load it into memory. Three things stand in the way of using it as the default: storage quota, which is a fraction of free disk and may be refused for a very large file; eviction, where Safari deletes a site's storage after seven days without a visit and any browser may clear it under disk pressure; and the fact that the file is invisible to the user until the final download step succeeds. Each is manageable, none is free, and the honest status is that it would raise the Safari and Firefox limit from 512 MB to something bounded by quota rather than memory.
Why 512 MB, specifically
Mobile operating systems kill a browser tab that uses too much memory, without warning and without a dialog: iOS is especially strict, and an older iPhone with several apps open can have well under a gigabyte to give a Safari tab. In-memory assembly needs the file plus working space, and the moment of assembly can briefly need more. 512 MB is a figure chosen to complete reliably on older phones, where a tab may get well under a gigabyte. It is deliberately conservative on a desktop with 32 GB of RAM, where the honest alternative is to receive in Chrome or Edge and have no cap at all.
Chunk size: smaller than you might expect
Data channel messages cannot be arbitrarily large: implementations negotiate a maximum message size, and large messages block the channel. ShareDrop.org uses 64 KiB chunks with a small binary header (transfer ID, file ID, chunk index), which keeps messages well within every browser's limits and gives fine-grained progress and incremental hashing for free. Chunking is also what makes resume-after-disconnect possible: the receiver reports the last chunk written to disk, and the sender continues from there instead of starting over.
| Chunk size | Header overhead | Fits every browser's message limit? | Progress granularity | Resent after a drop (worst case) |
|---|---|---|---|---|
| 16 KiB | ~0.1% | Yes | Very fine | 16 KiB |
| 64 KiB | ~0.03% | Yes, with margin | Fine — 16 updates per MiB | 64 KiB |
| 256 KiB | ~0.01% | At Chrome's limit; not safely | Coarse | 256 KiB |
| 1 MiB | negligible | No | Coarse | 1 MiB |
Below 64 KiB the per-message cost in the browser (one event, one copy) starts to show on fast links; above it, interoperability gets risky and a dropped connection wastes more. 64 KiB is the size at which none of the columns hurt.
Resume, precisely
When a connection drops mid-file, the receiver has written some number of verified chunks to disk (or to memory). On reconnect it tells the sender, per file, the index of the last chunk it has; the sender continues from the next index. Files already completed and verified are never touched again. The only data that can be resent is the tail of the file that was in flight: at most a few chunks that were sent but not yet acknowledged. A tool without chunk-level state restarts the whole file, which for a 4 GB video on a flaky connection may mean never finishing at all.
The numbers, and how they are verified
- 64 KiB chunks, 8 MiB / 1 MiB watermarks are the production engine's real parameters, not illustrations — the engine test suite exercises the chunking, backpressure and resume logic against them on every change.
- Throughput is not the cost. In ShareDrop.org's same-machine benchmark the full pipeline (64 KiB chunking, encrypted data channel, and real-time SHA-256 on both ends) sustains 200+ Mbps (25 MB/s). At that rate the protocol overhead is invisible next to any real network.
- Corruption is caught, not assumed away: the engine suite includes corrupted-chunk tests confirming that a flipped byte anywhere in the stream fails the end-to-end SHA-256 check instead of producing a silently broken file.
- Memory is measured, not hoped for: on the streamed path, a multi-gigabyte receive holds the tab at a few tens of megabytes for the whole duration; on the in-memory path, usage tracks the file size one-to-one, which is the measurement behind the cap.
The sender side on a phone: the copy you did not ask for
Desktop browsers read a chosen file in place. Phones are different, and it matters for large videos. When you pick a video from the iOS photo library, the system does not hand the browser or app the original; it exports a copy to a temporary location first (transcoding it if the format is not one the receiver requested) and only then can the transfer read it. That export takes time proportional to the file (a minute or more for a long 4K clip, before a single byte has been sent) and needs free space equal to the file's size. A phone with 800 MB free cannot send a 1 GB video even though the video is already on it. Android provides content streams that can usually be read without a copy, so the same pick starts sending immediately. ShareDrop.org's iOS app shows the export as a preparation step rather than as a stalled transfer, and cleans the temporary copy up afterwards; a browser on iOS has less control and can only wait.
How other tools handle the same problem
Every serious transfer tool arrives at chunking and streaming, because there is no other way to move a file larger than memory; the differences are in where the chunks go.
- Native LAN tools (AirDrop, Quick Share, LocalSend) write straight to disk with full operating-system privileges, so they have no receiver-side limit at all. Their constraint is the network, not memory.
- Upload services chunk in the other direction: the browser sends pieces of a few megabytes to the server one after another, each acknowledged, so an upload can resume and never holds the whole file in memory. The receiver later downloads through the browser's ordinary download manager, which streams to disk everywhere, which is why a download of a 20 GB file works in Safari while a peer-to-peer receive of the same file does not. The server in the middle is doing the buffering.
- Other browser-to-browser tools mostly assemble in memory and either document a limit, fail silently above one, or leave it to luck. Some use the service-worker download trick to stream in browsers without the File System Access API; it works when it works, and breaks with browser updates often enough that ShareDrop.org uses an explicit cap instead.
The pattern is that streaming to disk needs a privileged writer: an operating system, a server, or a browser API that grants it for one file at a time. Browser-to-browser transfer has that on desktop Chromium and nowhere else yet, and the honest response is to say so up front.
Memory: what to expect, and how to check
The browser's own task manager (Chrome: menu → More tools → Task manager; Firefox: about:processes) shows a tab's memory live, so the difference between the two saving paths is easy to watch during a transfer. The magnitudes below are typical rather than benchmarks; the point is the shape, not the last digit.
| Scenario | Tab memory at start | During a 3 GB receive | At completion |
|---|---|---|---|
| Chrome, streamed to disk | ~60 MB | ~80–100 MB, flat | Same |
| Chrome, sending | ~60 MB | ~70–80 MB, flat (8 MiB buffer plus overhead) | Same |
| Firefox, in-memory (500 MB file) | ~90 MB | Rises to ~600 MB | Briefly higher at assembly, then released after the download |
| Safari on an older iPhone (500 MB file) | — | Rises with the file; the system reclaims other tabs first | Completes at 512 MB; beyond that the tab risks being reloaded, which is the reason for the cap |
The two flat rows are the whole argument for streaming: memory is a property of the pipeline, not of the file. The two rising rows are the whole argument for a cap that is announced before the transfer, not discovered during it.
On the phone apps
The ShareDrop.org iOS and Android apps do not have any of these constraints, because a native app can write to storage as bytes arrive without asking a browser's permission. What they do have is the operating system's photo library: a received photo or video is written to a temporary file and then imported, and the import (which generates thumbnails and metadata) is the step that takes a few seconds at the end of a large video (what is happening at 99%). Non-media files go to the app's own folder, visible in the system Files app, with no second step.
Frequently asked
Can I really send a 50 GB file?
Yes, to a desktop receiving in Chrome or Edge, or to the mobile app, given the disk space and the time: at 25 MB/s on a LAN that is about half an hour; across the internet at 30 Mbps upload, about four hours, which is where resume after a drop stops being a nicety.
Why not raise the 512 MB cap on a desktop Safari with lots of memory?
Because the cap cannot see the memory. A page does not know whether it is running on a Mac Studio or an old iPhone, and a limit that is safe on one and fatal on the other is not a limit. The path with no cap is the one that never holds the file in memory, and on macOS that means Chrome or Edge until Safari adds a streamed save.
Does zipping a large file help?
Not for size (video, photos and archives are already compressed), and not for the cap, which applies to the zip too. Zipping helps for many files, by turning per-file overhead into one stream (folders).
Takeaways when evaluating any transfer tool
- “No size limit” claims that ignore the receiving browser are marketing, not engineering (ShareDrop.org's stated limits).
- Constant-memory behaviour on both ends is the defining feature separating tools that handle real files from demos.
- If a tool cannot tell you which saving path your browser will use, it has not thought about the problem.
- Ask how it resumes. “It retries” usually means “from the start.”
Standards referenced
- RFC 8831: WebRTC Data Channels: message-size constraints and the SCTP-over-DTLS transport the chunks ride on.
- WHATWG Streams Standard: the backpressure model (
file.stream(), readable streams, watermarks) this article applies. - File System Access API (WICG): the streamed-to-disk saving path on Chromium desktop.
- WHATWG File System Standard: the origin-private file system available across browsers.
- W3C File API: Blob semantics behind the in-memory fallback and its practical limits.