<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anirban Roy</title>
    <description>The latest articles on DEV Community by Anirban Roy (@anirban00537).</description>
    <link>https://dev.to/anirban00537</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1278665%2F9d2cdd92-d3e1-4085-b912-89aa129dc3b6.jpeg</url>
      <title>DEV Community: Anirban Roy</title>
      <link>https://dev.to/anirban00537</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anirban00537"/>
    <language>en</language>
    <item>
      <title>Why I Chose P2P Instead of S3 for My File Transfer Product</title>
      <dc:creator>Anirban Roy</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:34:59 +0000</pubDate>
      <link>https://dev.to/anirban00537/why-i-chose-p2p-instead-of-s3-for-my-file-transfer-product-3mgo</link>
      <guid>https://dev.to/anirban00537/why-i-chose-p2p-instead-of-s3-for-my-file-transfer-product-3mgo</guid>
      <description>&lt;p&gt;When I started thinking seriously about building a large-file transfer product, Amazon S3 felt like the obvious architecture.&lt;/p&gt;

&lt;p&gt;It is reliable.&lt;/p&gt;

&lt;p&gt;It scales.&lt;/p&gt;

&lt;p&gt;It has excellent tooling.&lt;/p&gt;

&lt;p&gt;Every backend framework knows how to work with it.&lt;/p&gt;

&lt;p&gt;And the standard implementation is incredibly straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User selects file
      ↓
Upload to S3
      ↓
Generate download URL
      ↓
Recipient downloads file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most products, that is a perfectly reasonable design.&lt;/p&gt;

&lt;p&gt;In fact, if I were building document storage, backups, team collaboration, or a Dropbox-style product, I would probably reach for object storage immediately.&lt;/p&gt;

&lt;p&gt;But ButterShare was supposed to solve a slightly different problem.&lt;/p&gt;

&lt;p&gt;I was not trying to build:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a place where users store files&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was trying to build:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a way to move a file from one device to another&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction eventually changed the entire architecture.&lt;/p&gt;

&lt;p&gt;Instead of making S3 the center of the product, I started experimenting with direct peer-to-peer transfer using WebRTC.&lt;/p&gt;

&lt;p&gt;The architecture became closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   ↓
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   ↓
S3
   ↓
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds like a small change.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;It changes bandwidth economics, infrastructure complexity, user experience, privacy assumptions, failure modes, and even what the product fundamentally is.&lt;/p&gt;

&lt;p&gt;This is why I chose P2P instead of building ButterShare around S3.&lt;/p&gt;




&lt;h1&gt;
  
  
  First: S3 is not the problem
&lt;/h1&gt;

&lt;p&gt;I want to make this clear before going any further.&lt;/p&gt;

&lt;p&gt;S3 is excellent.&lt;/p&gt;

&lt;p&gt;Object storage is one of the most useful infrastructure primitives we have.&lt;/p&gt;

&lt;p&gt;If your application needs files to remain available after the uploader disappears, object storage makes enormous sense.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;profile images&lt;/li&gt;
&lt;li&gt;backups&lt;/li&gt;
&lt;li&gt;user-generated content&lt;/li&gt;
&lt;li&gt;product assets&lt;/li&gt;
&lt;li&gt;documents&lt;/li&gt;
&lt;li&gt;media libraries&lt;/li&gt;
&lt;li&gt;asynchronous downloads&lt;/li&gt;
&lt;li&gt;team collaboration&lt;/li&gt;
&lt;li&gt;long-term storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;S3 is extremely good at those things.&lt;/p&gt;

&lt;p&gt;My decision was not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;S3 is bad.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;S3 solves a different problem than the one I want ButterShare to solve.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference is important.&lt;/p&gt;




&lt;h1&gt;
  
  
  Transfer and storage are not the same thing
&lt;/h1&gt;

&lt;p&gt;Suppose Alice has a 100GB video project.&lt;/p&gt;

&lt;p&gt;Bob needs that project.&lt;/p&gt;

&lt;p&gt;There are two ways to think about the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage-first approach
&lt;/h2&gt;

&lt;p&gt;Alice uploads the file somewhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice
  ↓
Cloud storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bob later downloads it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cloud storage
  ↓
Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cloud copy acts as the middleman.&lt;/p&gt;

&lt;p&gt;This is asynchronous.&lt;/p&gt;

&lt;p&gt;Alice can upload the file, close her computer, go offline, and Bob can download it hours later.&lt;/p&gt;

&lt;p&gt;That is incredibly useful.&lt;/p&gt;

&lt;p&gt;But there is another model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transfer-first approach
&lt;/h2&gt;

&lt;p&gt;Alice and Bob are both online.&lt;/p&gt;

&lt;p&gt;Alice's goal is simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Get these bytes from my machine to Bob's machine.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice
  ↓
Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No permanent intermediate copy is required.&lt;/p&gt;

&lt;p&gt;That is the model that interested me.&lt;/p&gt;




&lt;h1&gt;
  
  
  The 100GB thought experiment
&lt;/h1&gt;

&lt;p&gt;Large files make the architectural difference easier to see.&lt;/p&gt;

&lt;p&gt;Imagine someone transfers a 100GB file.&lt;/p&gt;

&lt;p&gt;With an S3-style workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender → S3
100GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S3 → Recipient
100GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user thinks they are performing one transfer.&lt;/p&gt;

&lt;p&gt;But from the infrastructure perspective, the data has moved through two major legs.&lt;/p&gt;

&lt;p&gt;You also temporarily have to store 100GB.&lt;/p&gt;

&lt;p&gt;Now imagine 1,000 transfers like that.&lt;/p&gt;

&lt;p&gt;You are no longer building a tiny file-sharing app.&lt;/p&gt;

&lt;p&gt;You are operating a serious storage and bandwidth system.&lt;/p&gt;

&lt;p&gt;That may be exactly what you want.&lt;/p&gt;

&lt;p&gt;But I started asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why should my infrastructure handle the entire file twice if the sender and receiver are online at the same time?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question pushed me toward P2P.&lt;/p&gt;




&lt;h1&gt;
  
  
  What an S3 architecture would look like
&lt;/h1&gt;

&lt;p&gt;The conventional architecture is actually very attractive.&lt;/p&gt;

&lt;p&gt;Your backend generates a presigned upload URL.&lt;/p&gt;

&lt;p&gt;The browser uploads directly to object storage.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
   |
   | request upload URL
   v
Backend
   |
   | presigned URL
   v
Browser
   |
   | upload
   v
S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the recipient gets a download URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recipient
   |
   | download
   v
S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids sending the large file through your application server.&lt;/p&gt;

&lt;p&gt;That is already much better than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
   ↓
Node.js server
   ↓
S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for huge files.&lt;/p&gt;

&lt;p&gt;Your Node server does not need to proxy 100GB.&lt;/p&gt;

&lt;p&gt;S3 handles the actual upload and download.&lt;/p&gt;

&lt;p&gt;This architecture is mature, simple, and reliable.&lt;/p&gt;

&lt;p&gt;So why not stop there?&lt;/p&gt;

&lt;p&gt;Because ButterShare's goal was not storage-backed sharing.&lt;/p&gt;




&lt;h1&gt;
  
  
  The cost model changes as files get bigger
&lt;/h1&gt;

&lt;p&gt;For small files, infrastructure cost is often barely noticeable.&lt;/p&gt;

&lt;p&gt;A user sending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not going to destroy your startup.&lt;/p&gt;

&lt;p&gt;But the economics look different when your product specifically encourages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10GB
50GB
100GB
200GB+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;object storage&lt;/li&gt;
&lt;li&gt;upload traffic&lt;/li&gt;
&lt;li&gt;download traffic&lt;/li&gt;
&lt;li&gt;egress&lt;/li&gt;
&lt;li&gt;API requests&lt;/li&gt;
&lt;li&gt;multipart upload&lt;/li&gt;
&lt;li&gt;abandoned uploads&lt;/li&gt;
&lt;li&gt;lifecycle cleanup&lt;/li&gt;
&lt;li&gt;replication&lt;/li&gt;
&lt;li&gt;logging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And most importantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the bigger your product succeeds, the more file data your infrastructure has to handle&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That creates a very direct relationship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;more users
=
more storage
+
more bandwidth
+
higher infrastructure cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;P2P changes that relationship.&lt;/p&gt;




&lt;h1&gt;
  
  
  P2P moves the data plane away from my servers
&lt;/h1&gt;

&lt;p&gt;With a peer-to-peer architecture, my backend still exists.&lt;/p&gt;

&lt;p&gt;But it mostly handles coordination.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   |
   | create session
   v
Signaling server
   |
   | connection information
   v
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once WebRTC establishes the peer connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender &amp;lt;----------------------&amp;gt; Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual file can travel directly between them when network conditions allow.&lt;/p&gt;

&lt;p&gt;My backend is responsible for things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creating transfer sessions&lt;/li&gt;
&lt;li&gt;exchanging signaling data&lt;/li&gt;
&lt;li&gt;handling room state&lt;/li&gt;
&lt;li&gt;coordinating peers&lt;/li&gt;
&lt;li&gt;expiring sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not need to become the permanent home of every file.&lt;/p&gt;

&lt;p&gt;That is a very different infrastructure problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  Control plane vs data plane
&lt;/h1&gt;

&lt;p&gt;This was a useful way for me to think about the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control plane
&lt;/h2&gt;

&lt;p&gt;Small messages.&lt;/p&gt;

&lt;p&gt;Things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create room
Join room
Peer connected
SDP offer
SDP answer
ICE candidate
Transfer started
Transfer finished
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These messages are tiny.&lt;/p&gt;

&lt;p&gt;They are easy and cheap to handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data plane
&lt;/h2&gt;

&lt;p&gt;The actual file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100GB video
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the expensive part.&lt;/p&gt;

&lt;p&gt;With an S3 architecture, the cloud owns the data plane.&lt;/p&gt;

&lt;p&gt;With P2P, I try to keep the data plane between the users whenever possible.&lt;/p&gt;

&lt;p&gt;That separation is one of the biggest reasons I liked the architecture.&lt;/p&gt;




&lt;h1&gt;
  
  
  P2P does not mean "no servers"
&lt;/h1&gt;

&lt;p&gt;This is one of the biggest misconceptions around WebRTC.&lt;/p&gt;

&lt;p&gt;People hear:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;peer-to-peer&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No backend
No infrastructure
No server bills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, physics is not that generous.&lt;/p&gt;

&lt;p&gt;You still need infrastructure.&lt;/p&gt;

&lt;p&gt;For ButterShare, that can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application servers&lt;/li&gt;
&lt;li&gt;signaling&lt;/li&gt;
&lt;li&gt;session coordination&lt;/li&gt;
&lt;li&gt;STUN&lt;/li&gt;
&lt;li&gt;TURN&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;abuse prevention&lt;/li&gt;
&lt;li&gt;databases or temporary state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And TURN deserves special attention.&lt;/p&gt;




&lt;h1&gt;
  
  
  TURN is the part that ruins the perfect diagram
&lt;/h1&gt;

&lt;p&gt;In the ideal case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender &amp;lt;----------------------&amp;gt; Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great.&lt;/p&gt;

&lt;p&gt;But users live behind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NAT&lt;/li&gt;
&lt;li&gt;corporate firewalls&lt;/li&gt;
&lt;li&gt;carrier-grade NAT&lt;/li&gt;
&lt;li&gt;VPNs&lt;/li&gt;
&lt;li&gt;restrictive routers&lt;/li&gt;
&lt;li&gt;mobile networks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes a direct connection cannot be established.&lt;/p&gt;

&lt;p&gt;Then WebRTC may use a TURN relay.&lt;/p&gt;

&lt;p&gt;The architecture becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   ↓
TURN server
   ↓
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your infrastructure is handling the bandwidth again.&lt;/p&gt;

&lt;p&gt;So P2P does not magically eliminate bandwidth costs.&lt;/p&gt;

&lt;p&gt;It changes how often you need to pay them.&lt;/p&gt;

&lt;p&gt;That means one of the most important metrics in a P2P file-transfer product is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What percentage of transfers are direct?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If most transfers are direct, the infrastructure economics can be very attractive.&lt;/p&gt;

&lt;p&gt;If most transfers require TURN, your architecture starts looking much more bandwidth-heavy.&lt;/p&gt;




&lt;h1&gt;
  
  
  But TURN is still not S3
&lt;/h1&gt;

&lt;p&gt;Even when a file goes through TURN, there is an important conceptual difference.&lt;/p&gt;

&lt;p&gt;With object storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   ↓
Stored file
   ↓
Recipient
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With TURN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   ↓
Relay
   ↓
Recipient
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TURN relays traffic.&lt;/p&gt;

&lt;p&gt;It does not need to create a persistent copy of the full file for later downloading.&lt;/p&gt;

&lt;p&gt;That means you still avoid many storage-related concerns.&lt;/p&gt;




&lt;h1&gt;
  
  
  No persistent file storage changes the operational burden
&lt;/h1&gt;

&lt;p&gt;If I stored arbitrary user files, I would immediately inherit an entire category of problems.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage lifecycle
&lt;/h2&gt;

&lt;p&gt;How long do files stay?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 hour?
24 hours?
7 days?
30 days?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At some point you need cleanup jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abandoned uploads
&lt;/h2&gt;

&lt;p&gt;What happens when someone uploads 80GB of a 100GB file and closes the browser?&lt;/p&gt;

&lt;p&gt;Do you keep the partial upload?&lt;/p&gt;

&lt;p&gt;For how long?&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage quotas
&lt;/h2&gt;

&lt;p&gt;If the product is free, users will eventually test how free it really is.&lt;/p&gt;

&lt;p&gt;You need policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abuse
&lt;/h2&gt;

&lt;p&gt;If your servers permanently store arbitrary files, users can upload things you absolutely do not want to host.&lt;/p&gt;

&lt;p&gt;Now you need systems around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;abuse reports&lt;/li&gt;
&lt;li&gt;takedowns&lt;/li&gt;
&lt;li&gt;malware&lt;/li&gt;
&lt;li&gt;illegal content&lt;/li&gt;
&lt;li&gt;spam&lt;/li&gt;
&lt;li&gt;automated storage abuse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Direct transfer does not eliminate every abuse issue.&lt;/p&gt;

&lt;p&gt;But not becoming a long-term file host changes the problem dramatically.&lt;/p&gt;




&lt;h1&gt;
  
  
  Privacy was another reason
&lt;/h1&gt;

&lt;p&gt;There is also a product philosophy difference.&lt;/p&gt;

&lt;p&gt;With traditional cloud transfer, the file usually exists in three places during the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender device
Cloud provider
Recipient device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With direct transfer, the goal is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender device
Recipient device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is appealing for a product whose main job is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;move this file&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;rather than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;store this file for me&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Again, this does not magically make every P2P implementation private or secure.&lt;/p&gt;

&lt;p&gt;The details matter.&lt;/p&gt;

&lt;p&gt;You still need secure signaling, strong session identifiers, encrypted transport, good session expiration, and careful metadata handling.&lt;/p&gt;

&lt;p&gt;But architecturally, not retaining the actual file reduces how much user content my service needs to hold.&lt;/p&gt;

&lt;p&gt;I like that.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why didn't I just use S3 multipart upload?
&lt;/h1&gt;

&lt;p&gt;That was one of the obvious alternatives.&lt;/p&gt;

&lt;p&gt;S3 supports multipart uploads, which are excellent for large files.&lt;/p&gt;

&lt;p&gt;Instead of uploading one huge object in one request, you upload parts.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100GB file

Part 1
Part 2
Part 3
...
Part N
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If one part fails, you retry that part instead of restarting the entire upload.&lt;/p&gt;

&lt;p&gt;It is a proven solution.&lt;/p&gt;

&lt;p&gt;But multipart upload solves:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I reliably upload a very large object to cloud storage?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My question was different:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do I need to upload the object to cloud storage at all?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the architectural fork.&lt;/p&gt;




&lt;h1&gt;
  
  
  The cloud-storage architecture has a major UX advantage
&lt;/h1&gt;

&lt;p&gt;There is one area where S3 wins very clearly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;asynchronous transfer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Suppose Alice uploads a file at 2 PM.&lt;/p&gt;

&lt;p&gt;She closes her laptop.&lt;/p&gt;

&lt;p&gt;Bob opens the link at midnight.&lt;/p&gt;

&lt;p&gt;With S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Works perfectly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With P2P:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice is offline.
No transfer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a major limitation.&lt;/p&gt;

&lt;p&gt;With ButterShare's direct-transfer model, both peers need to participate in the live transfer.&lt;/p&gt;

&lt;p&gt;That is not a small tradeoff.&lt;/p&gt;

&lt;p&gt;It fundamentally changes the use case.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I accepted that tradeoff
&lt;/h1&gt;

&lt;p&gt;Because I wanted to optimize for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"I have a huge file.
You are online.
I need to give it to you now."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;video editor sending footage&lt;/li&gt;
&lt;li&gt;photographer sending RAW files&lt;/li&gt;
&lt;li&gt;developer sending a VM image&lt;/li&gt;
&lt;li&gt;designer sending a large project&lt;/li&gt;
&lt;li&gt;friend sending a huge archive&lt;/li&gt;
&lt;li&gt;creator moving footage between devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this workflow, I do not necessarily need persistent cloud storage.&lt;/p&gt;

&lt;p&gt;I need a reliable pipe.&lt;/p&gt;

&lt;p&gt;That is the niche.&lt;/p&gt;




&lt;h1&gt;
  
  
  This is why I don't think P2P replaces S3
&lt;/h1&gt;

&lt;p&gt;They are complementary.&lt;/p&gt;

&lt;p&gt;If I need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;storage
persistence
sharing later
multiple downloads
CDN delivery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would choose object storage.&lt;/p&gt;

&lt;p&gt;If I need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;live transfer
one sender
one receiver
large file
both online
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;P2P starts looking very attractive.&lt;/p&gt;

&lt;p&gt;The architecture should follow the use case, not ideology.&lt;/p&gt;




&lt;h1&gt;
  
  
  The biggest advantage: no duplicate cloud journey
&lt;/h1&gt;

&lt;p&gt;This is the simplest way I explain the product to myself.&lt;/p&gt;

&lt;p&gt;With cloud storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your file is here.

Destination is there.

But first send it somewhere else.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That "somewhere else" is valuable because it gives you persistence.&lt;/p&gt;

&lt;p&gt;But if persistence is unnecessary, it can feel redundant.&lt;/p&gt;

&lt;p&gt;With P2P:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source → Destination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That directness becomes more meaningful the larger the file is.&lt;/p&gt;




&lt;h1&gt;
  
  
  Large files expose the difference
&lt;/h1&gt;

&lt;p&gt;Imagine transferring a 20MB PDF.&lt;/p&gt;

&lt;p&gt;Does the architecture matter?&lt;/p&gt;

&lt;p&gt;Probably not to the user.&lt;/p&gt;

&lt;p&gt;Whether it goes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender → S3 → Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender → Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the whole thing may happen quickly.&lt;/p&gt;

&lt;p&gt;Now imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;150GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intermediate step becomes much more visible.&lt;/p&gt;

&lt;p&gt;The user may have to upload 150GB before the recipient can even start downloading, depending on the implementation.&lt;/p&gt;

&lt;p&gt;Direct transfer lets the destination receive data as it is being sent.&lt;/p&gt;




&lt;h1&gt;
  
  
  Time-to-first-byte is different too
&lt;/h1&gt;

&lt;p&gt;In a standard upload-then-share flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upload 100GB
      ↓
Finish upload
      ↓
Recipient starts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a direct-transfer flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender starts
      ↓
Recipient starts receiving
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;almost immediately after the connection is established.&lt;/p&gt;

&lt;p&gt;This changes the experience.&lt;/p&gt;

&lt;p&gt;The receiver does not necessarily have to wait for an entire cloud upload to finish first.&lt;/p&gt;




&lt;h1&gt;
  
  
  The bandwidth bottleneck still exists
&lt;/h1&gt;

&lt;p&gt;P2P does not make the user's internet connection faster.&lt;/p&gt;

&lt;p&gt;This is important.&lt;/p&gt;

&lt;p&gt;Suppose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender upload:
20 Mbps

Receiver download:
500 Mbps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transfer is still constrained by the sender.&lt;/p&gt;

&lt;p&gt;A 100GB file is still a 100GB file.&lt;/p&gt;

&lt;p&gt;WebRTC does not violate physics.&lt;/p&gt;

&lt;p&gt;The architectural advantage is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the file becomes tiny&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the sender's upload is going toward the actual destination rather than an intermediate storage system first&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much more honest way to think about it.&lt;/p&gt;




&lt;h1&gt;
  
  
  P2P also creates harder engineering problems
&lt;/h1&gt;

&lt;p&gt;Choosing P2P was not choosing the easier architecture.&lt;/p&gt;

&lt;p&gt;In many ways, S3 would have been much easier.&lt;/p&gt;

&lt;p&gt;With S3, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;durable storage&lt;/li&gt;
&lt;li&gt;resumable multipart uploads&lt;/li&gt;
&lt;li&gt;globally mature infrastructure&lt;/li&gt;
&lt;li&gt;reliable HTTP downloads&lt;/li&gt;
&lt;li&gt;straightforward authorization&lt;/li&gt;
&lt;li&gt;mature SDKs&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;excellent tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With P2P, I have to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signaling&lt;/li&gt;
&lt;li&gt;NAT traversal&lt;/li&gt;
&lt;li&gt;TURN&lt;/li&gt;
&lt;li&gt;WebRTC Data Channels&lt;/li&gt;
&lt;li&gt;browser differences&lt;/li&gt;
&lt;li&gt;connection failures&lt;/li&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;backpressure&lt;/li&gt;
&lt;li&gt;reconnection&lt;/li&gt;
&lt;li&gt;resumability&lt;/li&gt;
&lt;li&gt;long-running browser sessions&lt;/li&gt;
&lt;li&gt;mobile background behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So why do it?&lt;/p&gt;

&lt;p&gt;Because those engineering problems are directly aligned with the product I want.&lt;/p&gt;




&lt;h1&gt;
  
  
  S3 turns the server side into the easy part
&lt;/h1&gt;

&lt;p&gt;Imagine implementing the S3 version.&lt;/p&gt;

&lt;p&gt;Your backend might create a presigned URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uploadUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPresignedUploadUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client uploads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uploadUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you save metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file ID
owner
size
storage key
expiration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, create a download URL.&lt;/p&gt;

&lt;p&gt;That is relatively straightforward.&lt;/p&gt;

&lt;p&gt;Now compare that to P2P.&lt;/p&gt;




&lt;h1&gt;
  
  
  The P2P version requires connection negotiation
&lt;/h1&gt;

&lt;p&gt;Before a single byte moves, I need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender joins room
Receiver joins room
Exchange SDP
Exchange ICE candidates
Attempt direct connection
Possibly fallback to TURN
Open Data Channel
Negotiate transfer metadata
Start streaming chunks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much more complicated.&lt;/p&gt;

&lt;p&gt;The cost benefit comes with engineering complexity.&lt;/p&gt;

&lt;p&gt;There is no free lunch.&lt;/p&gt;




&lt;h1&gt;
  
  
  The first naive P2P implementation was terrible
&lt;/h1&gt;

&lt;p&gt;The first instinct is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works great for a small screenshot.&lt;/p&gt;

&lt;p&gt;Then someone selects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;50GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Goodbye browser.&lt;/p&gt;

&lt;p&gt;Large P2P transfer requires chunking.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But even that is not enough.&lt;/p&gt;

&lt;p&gt;You also need flow control.&lt;/p&gt;




&lt;h1&gt;
  
  
  Backpressure became one of the most important concepts
&lt;/h1&gt;

&lt;p&gt;If JavaScript produces chunks faster than the connection sends them, WebRTC queues them.&lt;/p&gt;

&lt;p&gt;You can see that queue using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmount&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you ignore it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send
send
send
send
send
send
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;eventually memory starts growing.&lt;/p&gt;

&lt;p&gt;So a real sender needs to respect the buffer.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForDrain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_BUFFER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_BUFFER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForDrain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the type of problem you never need to think about when S3 is handling everything.&lt;/p&gt;




&lt;h1&gt;
  
  
  The receiver has an even bigger challenge
&lt;/h1&gt;

&lt;p&gt;Suppose I successfully stream a 100GB file.&lt;/p&gt;

&lt;p&gt;What happens on the receiver?&lt;/p&gt;

&lt;p&gt;The naive version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is fine for a 20MB file.&lt;/p&gt;

&lt;p&gt;For 100GB, it is ridiculous.&lt;/p&gt;

&lt;p&gt;You need progressive writing.&lt;/p&gt;

&lt;p&gt;The receiver needs to write chunks somewhere as they arrive rather than keeping the entire file in memory.&lt;/p&gt;

&lt;p&gt;That means browser filesystem APIs, writable streams, or other platform-specific strategies.&lt;/p&gt;

&lt;p&gt;This was one of the moments where I realized:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Direct large-file transfer is basically a systems problem disguised as a web app.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Browser compatibility becomes infrastructure
&lt;/h1&gt;

&lt;p&gt;If you build around S3 and HTTP, browsers already know how to upload and download files very well.&lt;/p&gt;

&lt;p&gt;When you move more of the transfer engine into the browser, browser behavior becomes part of your infrastructure.&lt;/p&gt;

&lt;p&gt;You now care deeply about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Chrome
Edge
Firefox
Safari
iOS Safari
Android Chrome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And they do not all behave identically.&lt;/p&gt;

&lt;p&gt;You care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;filesystem APIs&lt;/li&gt;
&lt;li&gt;memory behavior&lt;/li&gt;
&lt;li&gt;background tab suspension&lt;/li&gt;
&lt;li&gt;WebRTC quirks&lt;/li&gt;
&lt;li&gt;download handling&lt;/li&gt;
&lt;li&gt;mobile lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your "server infrastructure" may be smaller.&lt;/p&gt;

&lt;p&gt;But your client-side complexity becomes much larger.&lt;/p&gt;




&lt;h1&gt;
  
  
  Mobile is where P2P gets painful
&lt;/h1&gt;

&lt;p&gt;A desktop user might leave a browser tab open for an hour.&lt;/p&gt;

&lt;p&gt;A mobile user will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;switch apps&lt;/li&gt;
&lt;li&gt;lock their phone&lt;/li&gt;
&lt;li&gt;receive a call&lt;/li&gt;
&lt;li&gt;enable battery saver&lt;/li&gt;
&lt;li&gt;move between Wi-Fi and cellular&lt;/li&gt;
&lt;li&gt;let the OS suspend the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a storage-backed upload, the architecture can sometimes recover more naturally.&lt;/p&gt;

&lt;p&gt;For a live P2P connection, both devices matter.&lt;/p&gt;

&lt;p&gt;That makes mobile reliability one of the hardest parts.&lt;/p&gt;




&lt;h1&gt;
  
  
  Reconnection is mandatory for huge files
&lt;/h1&gt;

&lt;p&gt;If I am asking someone to transfer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I cannot reasonably say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If your Wi-Fi drops after 99GB, please start again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is unacceptable.&lt;/p&gt;

&lt;p&gt;P2P means I need application-level resume logic.&lt;/p&gt;

&lt;p&gt;For example, the receiver can say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fileId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"receivedBytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;84639214822&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sender can then continue from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, a real implementation needs more validation than that.&lt;/p&gt;

&lt;p&gt;But resumability becomes part of your transfer protocol.&lt;/p&gt;

&lt;p&gt;S3 gives you mature multipart-upload patterns.&lt;/p&gt;

&lt;p&gt;With P2P, you own this problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  So why not let S3 handle failures for me?
&lt;/h1&gt;

&lt;p&gt;Because every architecture has tradeoffs.&lt;/p&gt;

&lt;p&gt;S3 gives me easier reliability.&lt;/p&gt;

&lt;p&gt;P2P gives me a different economic and product model.&lt;/p&gt;

&lt;p&gt;I decided the complexity was worth exploring because the defining promise of ButterShare is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We built another upload box.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Move large files directly between devices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If I hide an S3 upload behind the UI, I am building a different product.&lt;/p&gt;




&lt;h1&gt;
  
  
  The economics become especially interesting for free products
&lt;/h1&gt;

&lt;p&gt;This mattered to me because ButterShare is designed to be accessible without charging someone just because their file is large.&lt;/p&gt;

&lt;p&gt;Cloud storage products usually have a very understandable reason for limits.&lt;/p&gt;

&lt;p&gt;If every free user can upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;200GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and leave it sitting on your infrastructure, costs can explode.&lt;/p&gt;

&lt;p&gt;So products impose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;storage caps&lt;/li&gt;
&lt;li&gt;transfer caps&lt;/li&gt;
&lt;li&gt;expiration&lt;/li&gt;
&lt;li&gt;paid tiers&lt;/li&gt;
&lt;li&gt;monthly limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is completely rational.&lt;/p&gt;

&lt;p&gt;But if the service does not permanently store the file, the economics change.&lt;/p&gt;

&lt;p&gt;It does not make bandwidth free.&lt;/p&gt;

&lt;p&gt;It does not eliminate TURN.&lt;/p&gt;

&lt;p&gt;But the business is no longer paying to keep every file around.&lt;/p&gt;

&lt;p&gt;That opens up different product possibilities.&lt;/p&gt;




&lt;h1&gt;
  
  
  Storage creates another problem: unused files
&lt;/h1&gt;

&lt;p&gt;Suppose a user uploads 70GB.&lt;/p&gt;

&lt;p&gt;They generate a link.&lt;/p&gt;

&lt;p&gt;The recipient never downloads it.&lt;/p&gt;

&lt;p&gt;With object storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;you still stored 70GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maybe only for a day.&lt;/p&gt;

&lt;p&gt;Maybe a week.&lt;/p&gt;

&lt;p&gt;But the resource was consumed.&lt;/p&gt;

&lt;p&gt;With live P2P transfer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;no receiver
=
no 70GB transfer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This aligns infrastructure usage more closely with actual transfers.&lt;/p&gt;

&lt;p&gt;I find that elegant.&lt;/p&gt;




&lt;h1&gt;
  
  
  Multiple recipients change the answer
&lt;/h1&gt;

&lt;p&gt;Now let's say Alice needs to send the same 100GB file to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 people
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly P2P is less obviously attractive.&lt;/p&gt;

&lt;p&gt;Alice would potentially need to upload enormous amounts of data repeatedly.&lt;/p&gt;

&lt;p&gt;This is where cloud storage/CDN architecture becomes much better.&lt;/p&gt;

&lt;p&gt;Upload once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice → Cloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then distribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cloud → 100 recipients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is exactly what cloud infrastructure is good at.&lt;/p&gt;

&lt;p&gt;Again:&lt;/p&gt;

&lt;p&gt;P2P is not universally better.&lt;/p&gt;

&lt;p&gt;It is optimized for a different topology.&lt;/p&gt;




&lt;h1&gt;
  
  
  My ideal P2P use case
&lt;/h1&gt;

&lt;p&gt;The architecture makes the most sense when the shape looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 sender
1 receiver
both online
large file
one-time transfer
no long-term storage required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the core scenario I optimize for.&lt;/p&gt;

&lt;p&gt;Once the requirements move away from that, object storage becomes increasingly attractive.&lt;/p&gt;




&lt;h1&gt;
  
  
  S3 would win for collaboration
&lt;/h1&gt;

&lt;p&gt;Imagine building:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shared project folders
version history
team permissions
comments
persistent links
multiple devices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would absolutely want centralized storage.&lt;/p&gt;

&lt;p&gt;Trying to turn pure P2P into a Dropbox replacement would create unnecessary pain.&lt;/p&gt;

&lt;p&gt;ButterShare is intentionally focused on transfer.&lt;/p&gt;

&lt;p&gt;Not collaboration storage.&lt;/p&gt;

&lt;p&gt;That product boundary matters.&lt;/p&gt;




&lt;h1&gt;
  
  
  Architecture affects the user interface too
&lt;/h1&gt;

&lt;p&gt;With S3, I could show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upload complete.

You can close this page.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With P2P, I need to say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Keep this tab open while the transfer is in progress.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is objectively less convenient in some situations.&lt;/p&gt;

&lt;p&gt;But P2P gives me advantages elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no cloud upload stage&lt;/li&gt;
&lt;li&gt;no account needed for storage&lt;/li&gt;
&lt;li&gt;no storage quota in the traditional sense&lt;/li&gt;
&lt;li&gt;no waiting for a full upload before live transfer can proceed&lt;/li&gt;
&lt;li&gt;direct cross-device workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UX tradeoff reflects the architecture.&lt;/p&gt;




&lt;h1&gt;
  
  
  Architecture also changes what "file size limit" means
&lt;/h1&gt;

&lt;p&gt;In a storage-backed system, limits may come from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plan restrictions&lt;/li&gt;
&lt;li&gt;maximum object size&lt;/li&gt;
&lt;li&gt;upload gateway limits&lt;/li&gt;
&lt;li&gt;account storage quotas&lt;/li&gt;
&lt;li&gt;infrastructure policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a streaming P2P design, I do not need to allocate storage equal to the entire file on my server.&lt;/p&gt;

&lt;p&gt;So the application can avoid imposing a traditional cloud-storage file-size cap.&lt;/p&gt;

&lt;p&gt;But practical constraints still exist.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sender storage&lt;/li&gt;
&lt;li&gt;receiver storage&lt;/li&gt;
&lt;li&gt;browser capabilities&lt;/li&gt;
&lt;li&gt;network stability&lt;/li&gt;
&lt;li&gt;available memory&lt;/li&gt;
&lt;li&gt;transfer duration&lt;/li&gt;
&lt;li&gt;device behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "no file size limit" should never mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There are no physical limitations anywhere.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It means the product is not intentionally saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sorry, 5GB maximum.
Pay $20.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;just because the service would otherwise have to store the file.&lt;/p&gt;




&lt;h1&gt;
  
  
  One thing I really like: simpler data retention
&lt;/h1&gt;

&lt;p&gt;If the file is not stored on my infrastructure, there is less file data to manage after the transfer.&lt;/p&gt;

&lt;p&gt;I do not need a lifecycle like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uploaded
available
downloaded
expired
scheduled for deletion
deleted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for every file object.&lt;/p&gt;

&lt;p&gt;The transfer session can be much more ephemeral.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created
peer joined
connected
transferring
completed
expired
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That feels much closer to the product I wanted to build.&lt;/p&gt;




&lt;h1&gt;
  
  
  But metadata still needs discipline
&lt;/h1&gt;

&lt;p&gt;Not storing file bytes does not mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;collect everything else forever&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You still need to think about what metadata is necessary.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Do I really need to permanently store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;filename
file size
IP address
recipient metadata
transfer history
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for every transfer?&lt;/p&gt;

&lt;p&gt;Maybe not.&lt;/p&gt;

&lt;p&gt;A privacy-conscious architecture is not just about avoiding object storage.&lt;/p&gt;

&lt;p&gt;It is also about minimizing unnecessary metadata.&lt;/p&gt;




&lt;h1&gt;
  
  
  Security still matters enormously
&lt;/h1&gt;

&lt;p&gt;WebRTC transport is encrypted, but security does not end there.&lt;/p&gt;

&lt;p&gt;You still need to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;session entropy&lt;/li&gt;
&lt;li&gt;room guessing&lt;/li&gt;
&lt;li&gt;unauthorized peers&lt;/li&gt;
&lt;li&gt;session expiration&lt;/li&gt;
&lt;li&gt;signaling abuse&lt;/li&gt;
&lt;li&gt;rate limits&lt;/li&gt;
&lt;li&gt;replay&lt;/li&gt;
&lt;li&gt;malformed messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if your room ID is too predictable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ABC001
ABC002
ABC003
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then your architecture has a problem even if the underlying transport is encrypted.&lt;/p&gt;

&lt;p&gt;The entire system matters.&lt;/p&gt;




&lt;h1&gt;
  
  
  P2P makes abuse different, not nonexistent
&lt;/h1&gt;

&lt;p&gt;This is another important distinction.&lt;/p&gt;

&lt;p&gt;If I am not storing files, I reduce my role as a file host.&lt;/p&gt;

&lt;p&gt;But users can still abuse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signaling endpoints&lt;/li&gt;
&lt;li&gt;TURN bandwidth&lt;/li&gt;
&lt;li&gt;session creation&lt;/li&gt;
&lt;li&gt;automated connections&lt;/li&gt;
&lt;li&gt;resource-intensive transfers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So abuse prevention becomes more network-oriented.&lt;/p&gt;

&lt;p&gt;You may need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rate limiting
session limits
TURN credentials
temporary tokens
connection quotas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You have fewer storage-abuse problems but more real-time infrastructure concerns.&lt;/p&gt;




&lt;h1&gt;
  
  
  TURN abuse can become very expensive
&lt;/h1&gt;

&lt;p&gt;Imagine someone intentionally forces relay connections and pushes huge amounts of traffic through your TURN servers.&lt;/p&gt;

&lt;p&gt;Congratulations.&lt;/p&gt;

&lt;p&gt;Your supposedly cheap P2P application just became a bandwidth pipe.&lt;/p&gt;

&lt;p&gt;TURN credentials should not be treated casually.&lt;/p&gt;

&lt;p&gt;Short-lived credentials, authentication, rate limiting, and monitoring matter.&lt;/p&gt;

&lt;p&gt;This is one area where P2P architecture can surprise people.&lt;/p&gt;




&lt;h1&gt;
  
  
  Metrics I care about more than storage usage
&lt;/h1&gt;

&lt;p&gt;In an S3 architecture, I might watch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;storage consumed
object count
download bandwidth
upload bandwidth
lifecycle cleanup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In P2P, I care more about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;peer connection success rate
direct connection percentage
TURN percentage
average transfer size
average transfer duration
completion rate
disconnect rate
resume rate
browser failure rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These numbers tell me whether the architecture is actually working.&lt;/p&gt;




&lt;h1&gt;
  
  
  Completion rate matters more than theoretical savings
&lt;/h1&gt;

&lt;p&gt;Let's say P2P reduces infrastructure cost dramatically.&lt;/p&gt;

&lt;p&gt;Great.&lt;/p&gt;

&lt;p&gt;But if:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30% of 100GB transfers fail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then the architecture is a failure.&lt;/p&gt;

&lt;p&gt;Users do not care that my AWS bill is low.&lt;/p&gt;

&lt;p&gt;The product has to work.&lt;/p&gt;

&lt;p&gt;That is why my priority order is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Reliability
2. Compatibility
3. UX
4. Performance
5. Infrastructure efficiency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cheap broken software is still broken software.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I wouldn't build this with my Node.js server in the middle
&lt;/h1&gt;

&lt;p&gt;There is another architecture I could have chosen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   ↓
Node.js server
   ↓
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No S3.&lt;/p&gt;

&lt;p&gt;Just stream everything through my backend.&lt;/p&gt;

&lt;p&gt;For small transfers, this can work.&lt;/p&gt;

&lt;p&gt;For huge transfers, I would be turning my app server into a giant bandwidth proxy.&lt;/p&gt;

&lt;p&gt;Every transfer consumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inbound bandwidth&lt;/li&gt;
&lt;li&gt;outbound bandwidth&lt;/li&gt;
&lt;li&gt;open connections&lt;/li&gt;
&lt;li&gt;server resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At scale, that does not sound attractive.&lt;/p&gt;

&lt;p&gt;If my goal is direct transfer, WebRTC is much more aligned with the architecture.&lt;/p&gt;




&lt;h1&gt;
  
  
  What about WebSockets?
&lt;/h1&gt;

&lt;p&gt;WebSockets are excellent for real-time communication.&lt;/p&gt;

&lt;p&gt;I could theoretically do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   ↓
WebSocket server
   ↓
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But again, the server is carrying every byte.&lt;/p&gt;

&lt;p&gt;For a chat message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;perfect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would rather not make my server the middleman if I can avoid it.&lt;/p&gt;

&lt;p&gt;That is another reason WebRTC Data Channels are interesting.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why WebRTC instead of reinventing transport
&lt;/h1&gt;

&lt;p&gt;Browsers already expose WebRTC.&lt;/p&gt;

&lt;p&gt;It gives me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;encrypted transport&lt;/li&gt;
&lt;li&gt;peer connection negotiation&lt;/li&gt;
&lt;li&gt;ICE&lt;/li&gt;
&lt;li&gt;STUN/TURN integration&lt;/li&gt;
&lt;li&gt;Data Channels&lt;/li&gt;
&lt;li&gt;cross-browser networking primitives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a huge amount of functionality.&lt;/p&gt;

&lt;p&gt;Building a native P2P protocol across every platform would be much more difficult.&lt;/p&gt;

&lt;p&gt;The browser becomes the common runtime.&lt;/p&gt;

&lt;p&gt;That means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Windows
Mac
Linux
Android
iPhone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can potentially participate without dedicated native clients.&lt;/p&gt;

&lt;p&gt;That distribution advantage matters a lot to me.&lt;/p&gt;




&lt;h1&gt;
  
  
  The browser is also the biggest limitation
&lt;/h1&gt;

&lt;p&gt;The beauty is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No app installation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pain is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are inside a browser.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do not control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lifecycle&lt;/li&gt;
&lt;li&gt;memory limits&lt;/li&gt;
&lt;li&gt;background execution&lt;/li&gt;
&lt;li&gt;filesystem support&lt;/li&gt;
&lt;li&gt;OS behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the architecture is a constant tradeoff.&lt;/p&gt;

&lt;p&gt;The browser gives incredible reach.&lt;/p&gt;

&lt;p&gt;Native apps give more control.&lt;/p&gt;

&lt;p&gt;For ButterShare, I decided reach was worth the constraints.&lt;/p&gt;




&lt;h1&gt;
  
  
  P2P changes scaling in an interesting way
&lt;/h1&gt;

&lt;p&gt;In a storage-centric product, user growth often means your central infrastructure moves proportionally more file data.&lt;/p&gt;

&lt;p&gt;In a P2P product, some of that load is distributed to the edge.&lt;/p&gt;

&lt;p&gt;That is conceptually interesting.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 users → server traffic
1,000 users → much more server traffic
100,000 users → enormous server traffic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the peers carry much of the data themselves when direct connections succeed.&lt;/p&gt;

&lt;p&gt;Your signaling infrastructure can scale very differently from the files themselves.&lt;/p&gt;

&lt;p&gt;This is one of the most attractive technical properties of the design.&lt;/p&gt;




&lt;h1&gt;
  
  
  But connection count still matters
&lt;/h1&gt;

&lt;p&gt;Even if your server is not handling the bytes, you may still have thousands or millions of sessions.&lt;/p&gt;

&lt;p&gt;You need to scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signaling connections&lt;/li&gt;
&lt;li&gt;WebSockets&lt;/li&gt;
&lt;li&gt;room state&lt;/li&gt;
&lt;li&gt;ephemeral session data&lt;/li&gt;
&lt;li&gt;TURN credential generation&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;P2P reduces one category of scaling problem.&lt;/p&gt;

&lt;p&gt;It does not eliminate scaling.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I didn't choose a hybrid storage-first architecture
&lt;/h1&gt;

&lt;p&gt;A hybrid model is also possible.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Try P2P first.

If direct transfer fails:
upload to S3.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sounds great.&lt;/p&gt;

&lt;p&gt;But it dramatically increases product complexity.&lt;/p&gt;

&lt;p&gt;Now you have two transfer engines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P2P path
cloud path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you need to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When do we switch?&lt;/li&gt;
&lt;li&gt;Does the user know?&lt;/li&gt;
&lt;li&gt;Who pays for storage?&lt;/li&gt;
&lt;li&gt;How long is the cloud copy retained?&lt;/li&gt;
&lt;li&gt;Do we start uploading immediately as fallback?&lt;/li&gt;
&lt;li&gt;What if P2P reconnects?&lt;/li&gt;
&lt;li&gt;How does resume work across both modes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hybrid architecture may make sense later.&lt;/p&gt;

&lt;p&gt;But for a focused product, simplicity of product behavior has value.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why not automatically upload while trying P2P?
&lt;/h1&gt;

&lt;p&gt;You could do both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender → Receiver
   \
    → S3 backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then if the peer connection fails, the recipient continues from the cloud.&lt;/p&gt;

&lt;p&gt;Very robust.&lt;/p&gt;

&lt;p&gt;Also potentially very expensive.&lt;/p&gt;

&lt;p&gt;You just recreated the cloud bandwidth problem even when direct transfer works.&lt;/p&gt;

&lt;p&gt;Again, architecture follows priorities.&lt;/p&gt;




&lt;h1&gt;
  
  
  The simplicity I wanted was conceptual, not technical
&lt;/h1&gt;

&lt;p&gt;This is a funny thing about ButterShare.&lt;/p&gt;

&lt;p&gt;The technical implementation is complicated.&lt;/p&gt;

&lt;p&gt;But the product concept is extremely simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I have a file.

You need the file.

Let's connect.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wanted the infrastructure to reflect that as much as possible.&lt;/p&gt;

&lt;p&gt;That was more important to me than choosing the easiest backend implementation.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I would choose for different products
&lt;/h1&gt;

&lt;p&gt;If I were starting other products today, my choices would be different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Photo backup app
&lt;/h2&gt;

&lt;p&gt;S3/object storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Team document workspace
&lt;/h2&gt;

&lt;p&gt;S3/object storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video hosting platform
&lt;/h2&gt;

&lt;p&gt;Object storage + CDN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Software distribution
&lt;/h2&gt;

&lt;p&gt;Object storage + CDN.&lt;/p&gt;

&lt;h2&gt;
  
  
  One-time 100GB transfer between two online people
&lt;/h2&gt;

&lt;p&gt;P2P becomes very interesting.&lt;/p&gt;

&lt;p&gt;The phrase:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;use the right tool for the job&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is boring.&lt;/p&gt;

&lt;p&gt;It is also correct.&lt;/p&gt;




&lt;h1&gt;
  
  
  A simplified architecture for ButterShare
&lt;/h1&gt;

&lt;p&gt;At a high level, the system looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               +-------------------+
               | Signaling Service |
               +-------------------+
                    ↑          ↑
                    |          |
                    |          |
                    ↓          ↓
               Sender      Receiver
               Browser      Browser
                   \          /
                    \        /
                     \      /
                      \    /
                WebRTC Connection
                        |
                        |
                 Direct if possible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If direct connection cannot be established:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   |
   v
TURN relay
   |
   v
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that the file does not need to become a traditional stored object before the recipient can receive it.&lt;/p&gt;




&lt;h1&gt;
  
  
  The decision came down to product identity
&lt;/h1&gt;

&lt;p&gt;The more I thought about it, the less this felt like a pure infrastructure decision.&lt;/p&gt;

&lt;p&gt;It became a product decision.&lt;/p&gt;

&lt;p&gt;If I built ButterShare around S3, the product would effectively be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Upload files to ButterShare and share them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With P2P, the product becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use ButterShare to connect two devices and transfer files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are subtly different.&lt;/p&gt;

&lt;p&gt;I wanted the second one.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I gave up by not choosing S3
&lt;/h1&gt;

&lt;p&gt;There are real downsides.&lt;/p&gt;

&lt;p&gt;I gave up easy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;asynchronous downloads&lt;/li&gt;
&lt;li&gt;persistent links&lt;/li&gt;
&lt;li&gt;sender-offline downloads&lt;/li&gt;
&lt;li&gt;one-to-many distribution&lt;/li&gt;
&lt;li&gt;mature multipart infrastructure&lt;/li&gt;
&lt;li&gt;global cloud delivery&lt;/li&gt;
&lt;li&gt;simple browser downloading&lt;/li&gt;
&lt;li&gt;durable server-side resume state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are significant benefits.&lt;/p&gt;

&lt;p&gt;I do not pretend otherwise.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I gained
&lt;/h1&gt;

&lt;p&gt;In exchange, I gained an architecture designed around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;direct transfer&lt;/li&gt;
&lt;li&gt;very large files&lt;/li&gt;
&lt;li&gt;minimal persistent file storage&lt;/li&gt;
&lt;li&gt;potentially lower central bandwidth requirements&lt;/li&gt;
&lt;li&gt;no traditional cloud-storage quota requirement&lt;/li&gt;
&lt;li&gt;cross-platform browser access&lt;/li&gt;
&lt;li&gt;simple one-time transfer sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For ButterShare, those benefits align closely with the product.&lt;/p&gt;

&lt;p&gt;That is why the tradeoff made sense.&lt;/p&gt;




&lt;h1&gt;
  
  
  The lesson I would give another developer
&lt;/h1&gt;

&lt;p&gt;Do not start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should I use S3 or WebRTC?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What exactly is my product doing with the file?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Does the file need to exist tomorrow?

Does the sender need to stay online?

Will multiple people download it?

Does it need versioning?

Is this storage or transfer?

How large are the files?

Who pays for bandwidth?

How important is app-free browser access?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answers will usually tell you which architecture makes sense.&lt;/p&gt;




&lt;h1&gt;
  
  
  My decision tree
&lt;/h1&gt;

&lt;p&gt;This is roughly how I think about it.&lt;/p&gt;

&lt;p&gt;If the answer to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Must the file remain available when the sender goes offline?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;use cloud storage.&lt;/p&gt;

&lt;p&gt;If:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does the file need to be downloaded by many people?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cloud storage/CDN probably wins.&lt;/p&gt;

&lt;p&gt;If:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this basically long-term storage?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;use object storage.&lt;/p&gt;

&lt;p&gt;But if:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;one sender
one receiver
both online
large file
one-time transfer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then P2P deserves serious consideration.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final thoughts
&lt;/h1&gt;

&lt;p&gt;Choosing P2P instead of S3 made ButterShare harder to build.&lt;/p&gt;

&lt;p&gt;There is no question about that.&lt;/p&gt;

&lt;p&gt;S3 would have solved many problems for me immediately.&lt;/p&gt;

&lt;p&gt;With WebRTC, I had to care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;peer discovery&lt;/li&gt;
&lt;li&gt;signaling&lt;/li&gt;
&lt;li&gt;STUN&lt;/li&gt;
&lt;li&gt;TURN&lt;/li&gt;
&lt;li&gt;NAT traversal&lt;/li&gt;
&lt;li&gt;browser memory&lt;/li&gt;
&lt;li&gt;chunking&lt;/li&gt;
&lt;li&gt;backpressure&lt;/li&gt;
&lt;li&gt;progressive writes&lt;/li&gt;
&lt;li&gt;reconnection&lt;/li&gt;
&lt;li&gt;resume&lt;/li&gt;
&lt;li&gt;mobile browsers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a lot of engineering.&lt;/p&gt;

&lt;p&gt;But it also created a product architecture that matches the problem I actually wanted to solve.&lt;/p&gt;

&lt;p&gt;I am not trying to create another cloud drive.&lt;/p&gt;

&lt;p&gt;I am trying to make this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Device A
   ↓
Device B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;feel as simple as possible.&lt;/p&gt;

&lt;p&gt;That is why I chose P2P.&lt;/p&gt;

&lt;p&gt;Not because S3 is bad.&lt;/p&gt;

&lt;p&gt;Not because P2P is magically free.&lt;/p&gt;

&lt;p&gt;Not because WebRTC solves everything.&lt;/p&gt;

&lt;p&gt;But because for &lt;strong&gt;one-time, extremely large transfers between two online devices&lt;/strong&gt;, sending the file directly to its destination makes a lot of sense.&lt;/p&gt;

&lt;p&gt;And once I started thinking about file transfer that way, it became very difficult to go back to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upload it somewhere else first.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm building that idea into &lt;strong&gt;ButterShare&lt;/strong&gt;, a browser-based peer-to-peer file transfer tool for sending large files directly between devices without relying on traditional cloud file storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ButterShare:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://buttershare.com/" rel="noopener noreferrer"&gt;https://buttershare.com/&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I chose P2P instead of building ButterShare around S3 because the product is primarily about &lt;strong&gt;transferring files, not storing them&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender → Cloud → Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With P2P:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender → Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;P2P gives me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no requirement to permanently store every file&lt;/li&gt;
&lt;li&gt;potentially less centralized bandwidth usage&lt;/li&gt;
&lt;li&gt;direct device-to-device transfer&lt;/li&gt;
&lt;li&gt;better economics for very large one-time transfers&lt;/li&gt;
&lt;li&gt;a browser-based cross-platform workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But I also accept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;harder engineering&lt;/li&gt;
&lt;li&gt;TURN costs&lt;/li&gt;
&lt;li&gt;both users needing to be online&lt;/li&gt;
&lt;li&gt;more browser complexity&lt;/li&gt;
&lt;li&gt;harder resume and reliability problems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Dropbox-style storage, I would choose object storage.&lt;/p&gt;

&lt;p&gt;For sending a 100GB file between two people who are online right now, I think P2P is a much more interesting architecture.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webrtc</category>
      <category>webdev</category>
      <category>p2p</category>
    </item>
    <item>
      <title>WebRTC Data Channels for Large File Transfer: What I Learned the Hard Way</title>
      <dc:creator>Anirban Roy</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:29:26 +0000</pubDate>
      <link>https://dev.to/anirban00537/webrtc-data-channels-for-large-file-transfer-what-i-learned-the-hard-way-2ama</link>
      <guid>https://dev.to/anirban00537/webrtc-data-channels-for-large-file-transfer-what-i-learned-the-hard-way-2ama</guid>
      <description>&lt;p&gt;WebRTC looks deceptively simple when you first use it for file transfer.&lt;/p&gt;

&lt;p&gt;Create a peer connection.&lt;/p&gt;

&lt;p&gt;Open a data channel.&lt;/p&gt;

&lt;p&gt;Read a file.&lt;/p&gt;

&lt;p&gt;Send the bytes.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;That approach works beautifully in a demo.&lt;/p&gt;

&lt;p&gt;Then you try a 20GB file.&lt;/p&gt;

&lt;p&gt;Then 50GB.&lt;/p&gt;

&lt;p&gt;Then 100GB.&lt;/p&gt;

&lt;p&gt;Then Safari decides your tab has had enough.&lt;/p&gt;

&lt;p&gt;Then the receiver runs out of memory.&lt;/p&gt;

&lt;p&gt;Then the sender's &lt;code&gt;bufferedAmount&lt;/code&gt; keeps climbing.&lt;/p&gt;

&lt;p&gt;Then the connection drops at 93%.&lt;/p&gt;

&lt;p&gt;Then you realize the hard part was never getting bytes from one browser to another.&lt;/p&gt;

&lt;p&gt;The hard part is doing it &lt;strong&gt;reliably, for a long time, across real browsers, on real networks, without blowing up memory or making the user restart from zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is what I learned while building large-file transfer into ButterShare.&lt;/p&gt;

&lt;p&gt;This post is not about a toy WebRTC example.&lt;/p&gt;

&lt;p&gt;It is about the parts that start to matter when your transfer is no longer 5MB.&lt;/p&gt;




&lt;h2&gt;
  
  
  WebRTC Data Channels are powerful, but they are not a file transfer protocol
&lt;/h2&gt;

&lt;p&gt;That was the first important lesson.&lt;/p&gt;

&lt;p&gt;A WebRTC data channel gives you a way to move arbitrary data between peers.&lt;/p&gt;

&lt;p&gt;That is all.&lt;/p&gt;

&lt;p&gt;It does not automatically give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file metadata&lt;/li&gt;
&lt;li&gt;chunking&lt;/li&gt;
&lt;li&gt;progress&lt;/li&gt;
&lt;li&gt;backpressure&lt;/li&gt;
&lt;li&gt;retry logic&lt;/li&gt;
&lt;li&gt;resume support&lt;/li&gt;
&lt;li&gt;integrity checks&lt;/li&gt;
&lt;li&gt;folder structure&lt;/li&gt;
&lt;li&gt;acknowledgements&lt;/li&gt;
&lt;li&gt;transfer state&lt;/li&gt;
&lt;li&gt;error recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need those things, you have to design them.&lt;/p&gt;

&lt;p&gt;The browser gives you transport.&lt;/p&gt;

&lt;p&gt;You still need to build the protocol.&lt;/p&gt;

&lt;p&gt;That difference becomes very important once transfers get large.&lt;/p&gt;




&lt;h1&gt;
  
  
  The naive implementation
&lt;/h1&gt;

&lt;p&gt;The obvious first version looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a tiny file, this is fine.&lt;/p&gt;

&lt;p&gt;For a large file, it is a disaster.&lt;/p&gt;

&lt;p&gt;If the file is 10GB, you absolutely do not want to read 10GB into memory.&lt;/p&gt;

&lt;p&gt;And you definitely do not want to do this on a phone.&lt;/p&gt;

&lt;p&gt;The better mental model is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never think of a large file as one object.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Think of it as a sequence of chunks.&lt;/p&gt;




&lt;h1&gt;
  
  
  Chunking is not optional
&lt;/h1&gt;

&lt;p&gt;The basic idea is simple.&lt;/p&gt;

&lt;p&gt;Instead of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ 100GB file ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you treat it as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[chunk 1]
[chunk 2]
[chunk 3]
[chunk 4]
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is already much better.&lt;/p&gt;

&lt;p&gt;You only keep a small part of the file in memory at a time.&lt;/p&gt;

&lt;p&gt;But this still breaks under pressure.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because you're sending chunks faster than the network can necessarily transmit them.&lt;/p&gt;

&lt;p&gt;That leads to the next problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  &lt;code&gt;bufferedAmount&lt;/code&gt; will humble you
&lt;/h1&gt;

&lt;p&gt;WebRTC data channels have an internal outgoing buffer.&lt;/p&gt;

&lt;p&gt;You can inspect it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmount&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you keep doing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;inside a fast loop, you can fill that buffer faster than the browser can drain it.&lt;/p&gt;

&lt;p&gt;At first, everything looks normal.&lt;/p&gt;

&lt;p&gt;Then memory starts climbing.&lt;/p&gt;

&lt;p&gt;Then the tab becomes sluggish.&lt;/p&gt;

&lt;p&gt;Then performance collapses.&lt;/p&gt;

&lt;p&gt;Then the browser may kill the page.&lt;/p&gt;

&lt;p&gt;This is one of the first things that separates a demo from a serious file-transfer implementation.&lt;/p&gt;




&lt;h1&gt;
  
  
  Backpressure matters more than chunk size
&lt;/h1&gt;

&lt;p&gt;A common mistake is focusing too much on finding the "perfect" chunk size.&lt;/p&gt;

&lt;p&gt;Should it be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;16KB?
64KB?
256KB?
1MB?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chunk size matters, but flow control matters more.&lt;/p&gt;

&lt;p&gt;You need to slow down when the browser's send buffer gets too large.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_BUFFERED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_BUFFERED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works better.&lt;/p&gt;

&lt;p&gt;But polling every few milliseconds is still not ideal.&lt;/p&gt;

&lt;p&gt;WebRTC gives you a cleaner option.&lt;/p&gt;




&lt;h1&gt;
  
  
  Use &lt;code&gt;bufferedAmountLowThreshold&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;You can tell the browser:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let me know when your send buffer has dropped below this level.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmountLowThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitUntilWritable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmountLowThreshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bufferedamountlow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bufferedamountlow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the sender can wait until the channel is ready for more data.&lt;/p&gt;

&lt;p&gt;This is far more stable than dumping chunks into the buffer as fast as possible.&lt;/p&gt;




&lt;h1&gt;
  
  
  Large file transfer is a flow-control problem
&lt;/h1&gt;

&lt;p&gt;Once files get big, your job is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;send as fast as possible&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;send as fast as the browser and network can safely sustain&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;If you push too aggressively, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;memory pressure&lt;/li&gt;
&lt;li&gt;unstable throughput&lt;/li&gt;
&lt;li&gt;browser crashes&lt;/li&gt;
&lt;li&gt;poor mobile behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you throttle too much, you leave bandwidth unused.&lt;/p&gt;

&lt;p&gt;So you are constantly balancing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;speed
vs
stability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for 100GB transfers, stability wins.&lt;/p&gt;

&lt;p&gt;A slightly slower transfer that finishes is better than a faster transfer that dies after 70GB.&lt;/p&gt;




&lt;h1&gt;
  
  
  Metadata has to come first
&lt;/h1&gt;

&lt;p&gt;The receiver needs context before binary chunks arrive.&lt;/p&gt;

&lt;p&gt;At minimum, I want the receiver to know things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file-start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"final-cut.mov"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21474836480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mimeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"video/quicktime"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the actual file data can follow.&lt;/p&gt;

&lt;p&gt;For multiple files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transfer-start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"video.mov"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21474836480&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file-002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"thumbnail.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5278321&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the receiver knows what to expect.&lt;/p&gt;

&lt;p&gt;This sounds trivial.&lt;/p&gt;

&lt;p&gt;It becomes very important for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;progress calculation&lt;/li&gt;
&lt;li&gt;folder reconstruction&lt;/li&gt;
&lt;li&gt;resume&lt;/li&gt;
&lt;li&gt;error reporting&lt;/li&gt;
&lt;li&gt;multi-file transfers&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  I stopped thinking in messages and started thinking in protocol states
&lt;/h1&gt;

&lt;p&gt;At first, you can get away with random messages like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"start"
"chunk"
"done"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gets messy very quickly.&lt;/p&gt;

&lt;p&gt;A proper protocol is easier to reason about.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SESSION_READY
TRANSFER_START
FILE_START
FILE_CHUNK
FILE_PROGRESS
FILE_END
TRANSFER_END
ACK
RETRY
RESUME_REQUEST
ERROR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have that, the system becomes much easier to debug.&lt;/p&gt;

&lt;p&gt;You can actually answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What state is the receiver in?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why is this boolean false?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  The receiver is where many implementations fall apart
&lt;/h1&gt;

&lt;p&gt;Sending chunks safely is only half of the problem.&lt;/p&gt;

&lt;p&gt;A lot of examples do this on the receiver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fine for small files.&lt;/p&gt;

&lt;p&gt;It is completely wrong for huge files.&lt;/p&gt;

&lt;p&gt;If you're receiving 100GB and storing every chunk in memory, you are effectively trying to build a 100GB array inside the browser.&lt;/p&gt;

&lt;p&gt;That is not going to end well.&lt;/p&gt;

&lt;p&gt;The receiver needs progressive writing.&lt;/p&gt;




&lt;h1&gt;
  
  
  Progressive writing is where browser differences become painful
&lt;/h1&gt;

&lt;p&gt;On supported browsers, APIs such as the File System Access API can let you write incrementally.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showSaveFilePicker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;suggestedName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWritable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then as chunks arrive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the right model.&lt;/p&gt;

&lt;p&gt;Data arrives.&lt;/p&gt;

&lt;p&gt;Data is written.&lt;/p&gt;

&lt;p&gt;Memory stays bounded.&lt;/p&gt;

&lt;p&gt;But browser support is not uniform.&lt;/p&gt;

&lt;p&gt;That is where the product challenge starts.&lt;/p&gt;

&lt;p&gt;Chrome on desktop may give you capabilities that Safari on iPhone does not expose in the same way.&lt;/p&gt;

&lt;p&gt;So you cannot build your architecture assuming one browser API is universally available.&lt;/p&gt;




&lt;h1&gt;
  
  
  "Works in Chrome" means almost nothing
&lt;/h1&gt;

&lt;p&gt;One of the biggest mistakes in browser networking is testing only in Chrome desktop.&lt;/p&gt;

&lt;p&gt;A real file-transfer product needs to survive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome&lt;/li&gt;
&lt;li&gt;Edge&lt;/li&gt;
&lt;li&gt;Firefox&lt;/li&gt;
&lt;li&gt;Safari&lt;/li&gt;
&lt;li&gt;iOS Safari&lt;/li&gt;
&lt;li&gt;Android Chrome&lt;/li&gt;
&lt;li&gt;low-memory devices&lt;/li&gt;
&lt;li&gt;aggressive mobile OS background behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And each one behaves differently under long transfers.&lt;/p&gt;

&lt;p&gt;Desktop WebRTC is the easy part.&lt;/p&gt;

&lt;p&gt;Mobile is where the real problems show up.&lt;/p&gt;




&lt;h1&gt;
  
  
  Mobile browsers are brutal
&lt;/h1&gt;

&lt;p&gt;A 30-minute transfer on desktop might be fine.&lt;/p&gt;

&lt;p&gt;On a phone, many things can interrupt you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;screen locking&lt;/li&gt;
&lt;li&gt;switching apps&lt;/li&gt;
&lt;li&gt;battery saver&lt;/li&gt;
&lt;li&gt;background tab throttling&lt;/li&gt;
&lt;li&gt;memory pressure&lt;/li&gt;
&lt;li&gt;network handoff&lt;/li&gt;
&lt;li&gt;Wi-Fi to mobile-data switching&lt;/li&gt;
&lt;li&gt;browser suspension&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user does not care why.&lt;/p&gt;

&lt;p&gt;They just see:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Transfer failed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That means your UX needs to prepare them.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Keep this tab open until the transfer finishes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Avoid locking your phone during large transfers.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It may not feel elegant, but it is better than pretending the browser has infinite control over the operating system.&lt;/p&gt;




&lt;h1&gt;
  
  
  Connection setup is not the same as file transfer
&lt;/h1&gt;

&lt;p&gt;WebRTC needs signaling before the peers can communicate.&lt;/p&gt;

&lt;p&gt;Usually that means exchanging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SDP offer&lt;/li&gt;
&lt;li&gt;SDP answer&lt;/li&gt;
&lt;li&gt;ICE candidates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your signaling server helps both sides find each other.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
  |
  | Offer
  v
Signaling server
  |
  | Forward
  v
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receiver
  |
  | Answer
  v
Signaling server
  |
  | Forward
  v
Sender
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ICE candidates are exchanged too.&lt;/p&gt;

&lt;p&gt;Once the peer connection is established, the file data can flow.&lt;/p&gt;

&lt;p&gt;The key architectural point is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The signaling server coordinates the connection. It does not need to store the file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That keeps the control plane separate from the data plane.&lt;/p&gt;




&lt;h1&gt;
  
  
  STUN and TURN are where the clean P2P story gets messy
&lt;/h1&gt;

&lt;p&gt;In the ideal case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser A &amp;lt;---------&amp;gt; Browser B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Direct connection.&lt;/p&gt;

&lt;p&gt;Nice.&lt;/p&gt;

&lt;p&gt;But users are behind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;routers&lt;/li&gt;
&lt;li&gt;NAT&lt;/li&gt;
&lt;li&gt;carrier-grade NAT&lt;/li&gt;
&lt;li&gt;enterprise firewalls&lt;/li&gt;
&lt;li&gt;VPNs&lt;/li&gt;
&lt;li&gt;restrictive mobile networks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So WebRTC uses ICE.&lt;/p&gt;

&lt;p&gt;STUN helps peers discover network information.&lt;/p&gt;

&lt;p&gt;TURN is used when a direct route cannot be established.&lt;/p&gt;

&lt;p&gt;Then you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser A
    |
    v
TURN server
    |
    v
Browser B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That transfer is still not being stored like a normal cloud-hosted file.&lt;/p&gt;

&lt;p&gt;But the traffic is relayed.&lt;/p&gt;

&lt;p&gt;And that matters financially.&lt;/p&gt;

&lt;p&gt;If a 100GB transfer goes through TURN, you are now relaying 100GB.&lt;/p&gt;

&lt;p&gt;Do that often enough and your infrastructure bill becomes very real.&lt;/p&gt;




&lt;h1&gt;
  
  
  P2P does not mean zero infrastructure cost
&lt;/h1&gt;

&lt;p&gt;This is a misconception I see a lot.&lt;/p&gt;

&lt;p&gt;You still need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signaling&lt;/li&gt;
&lt;li&gt;TURN&lt;/li&gt;
&lt;li&gt;STUN&lt;/li&gt;
&lt;li&gt;session management&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;abuse prevention&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;connection diagnostics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And TURN bandwidth can be expensive.&lt;/p&gt;

&lt;p&gt;A useful product needs to track:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;direct connection rate
relay rate
average file size
average transfer duration
failure rate
resume rate
browser distribution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, you are building blind.&lt;/p&gt;




&lt;h1&gt;
  
  
  Acknowledgements make progress more honest
&lt;/h1&gt;

&lt;p&gt;Another thing I learned:&lt;/p&gt;

&lt;p&gt;Do not call &lt;code&gt;send()&lt;/code&gt; and immediately assume those bytes reached the receiver.&lt;/p&gt;

&lt;p&gt;This:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;bytesSent&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only tells you that the browser accepted the data into its sending pipeline.&lt;/p&gt;

&lt;p&gt;It does not necessarily mean the receiver has written it.&lt;/p&gt;

&lt;p&gt;For a better progress model, the receiver can periodically acknowledge bytes received.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fileId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"receivedBytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5368709120&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the sender knows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5GB confirmed on receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much more meaningful than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5GB queued locally
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Don't acknowledge every chunk
&lt;/h1&gt;

&lt;p&gt;That sounds tempting.&lt;/p&gt;

&lt;p&gt;But if you send an ACK for every 64KB chunk, you create unnecessary protocol chatter.&lt;/p&gt;

&lt;p&gt;Instead, acknowledge periodically.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;every 4MB
every 16MB
every 100 chunks
every N milliseconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact strategy depends on your implementation.&lt;/p&gt;

&lt;p&gt;You want enough acknowledgement data to support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accurate progress&lt;/li&gt;
&lt;li&gt;resume&lt;/li&gt;
&lt;li&gt;fault recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without turning the protocol into an ACK storm.&lt;/p&gt;




&lt;h1&gt;
  
  
  Resume support changes everything
&lt;/h1&gt;

&lt;p&gt;If you're transferring 500MB, restarting is annoying.&lt;/p&gt;

&lt;p&gt;If you're transferring 100GB, restarting is unacceptable.&lt;/p&gt;

&lt;p&gt;Imagine this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transferred: 96.7GB
Connection lost.
Start again?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody wants that.&lt;/p&gt;

&lt;p&gt;Resume support means both sides need to agree on what has already been completed.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resume-state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fileId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"receivedBytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;103809024000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the sender resumes from that offset.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, a production system needs more checks than this.&lt;/p&gt;

&lt;p&gt;You need to ensure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same file&lt;/li&gt;
&lt;li&gt;same session&lt;/li&gt;
&lt;li&gt;same size&lt;/li&gt;
&lt;li&gt;correct offset&lt;/li&gt;
&lt;li&gt;receiver state is valid&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the concept is straightforward.&lt;/p&gt;

&lt;p&gt;Resume is not a nice extra for huge files.&lt;/p&gt;

&lt;p&gt;It is part of the core product.&lt;/p&gt;




&lt;h1&gt;
  
  
  File identity matters for resume
&lt;/h1&gt;

&lt;p&gt;You cannot blindly trust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two files can have the same name.&lt;/p&gt;

&lt;p&gt;So a real resume mechanism should use better identity information.&lt;/p&gt;

&lt;p&gt;Possibilities include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file size&lt;/li&gt;
&lt;li&gt;last-modified timestamp&lt;/li&gt;
&lt;li&gt;transfer-generated ID&lt;/li&gt;
&lt;li&gt;checksum&lt;/li&gt;
&lt;li&gt;chunk hashes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transfer-file-8f73"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"footage.mov"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;82471843840&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastModified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1788402394000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For stronger verification, add a digest.&lt;/p&gt;

&lt;p&gt;The exact level depends on your threat model and performance requirements.&lt;/p&gt;




&lt;h1&gt;
  
  
  Hashing huge files has a cost too
&lt;/h1&gt;

&lt;p&gt;Hashing a 100GB file before transfer sounds great.&lt;/p&gt;

&lt;p&gt;Until the user waits while you read the entire 100GB once just to calculate the hash.&lt;/p&gt;

&lt;p&gt;Then they wait again while you actually send it.&lt;/p&gt;

&lt;p&gt;That may be unacceptable.&lt;/p&gt;

&lt;p&gt;A better strategy can be incremental.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hash while transferring&lt;/li&gt;
&lt;li&gt;hash chunks&lt;/li&gt;
&lt;li&gt;hash groups of chunks&lt;/li&gt;
&lt;li&gt;calculate a rolling digest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you integrity verification without requiring a full extra pass before transfer.&lt;/p&gt;




&lt;h1&gt;
  
  
  Large file transfer makes small bugs expensive
&lt;/h1&gt;

&lt;p&gt;A 0.1% edge case sounds tiny.&lt;/p&gt;

&lt;p&gt;At scale, it is not.&lt;/p&gt;

&lt;p&gt;And for a user in the middle of a long transfer, that one bug can waste an hour.&lt;/p&gt;

&lt;p&gt;With huge transfers, things that seem minor suddenly matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate chunks&lt;/li&gt;
&lt;li&gt;missing chunks&lt;/li&gt;
&lt;li&gt;out-of-order logic&lt;/li&gt;
&lt;li&gt;reconnect races&lt;/li&gt;
&lt;li&gt;incorrect offsets&lt;/li&gt;
&lt;li&gt;final partial chunk handling&lt;/li&gt;
&lt;li&gt;file-close timing&lt;/li&gt;
&lt;li&gt;stale session state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 20KB mistake at the end of a 100GB file is still a corrupted file.&lt;/p&gt;




&lt;h1&gt;
  
  
  Ordered vs unordered data channels
&lt;/h1&gt;

&lt;p&gt;WebRTC data channels can be configured in different ways.&lt;/p&gt;

&lt;p&gt;The default is ordered delivery.&lt;/p&gt;

&lt;p&gt;For file transfer, ordered delivery is usually the simplest choice because file chunks naturally belong in sequence.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;peerConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file-transfer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unordered delivery can be useful for other real-time use cases.&lt;/p&gt;

&lt;p&gt;For a file-transfer system, ordered delivery generally makes reconstruction easier.&lt;/p&gt;

&lt;p&gt;That said, your application-level protocol should still know which chunk or byte range it is dealing with.&lt;/p&gt;

&lt;p&gt;Do not rely entirely on transport behavior as your only source of truth.&lt;/p&gt;




&lt;h1&gt;
  
  
  Reliable vs partially reliable delivery
&lt;/h1&gt;

&lt;p&gt;WebRTC lets you configure data channels with options such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;maxRetransmits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;maxPacketLifeTime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are useful for real-time data where dropping an old packet can be better than waiting.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;games&lt;/li&gt;
&lt;li&gt;live telemetry&lt;/li&gt;
&lt;li&gt;fast-changing state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;File transfer is different.&lt;/p&gt;

&lt;p&gt;If a chunk disappears, the file becomes corrupt.&lt;/p&gt;

&lt;p&gt;For file transfer, reliability usually matters more than latency.&lt;/p&gt;

&lt;p&gt;You do not want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;99.999% of the file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Chunk size is a tradeoff
&lt;/h1&gt;

&lt;p&gt;There is no universal best chunk size.&lt;/p&gt;

&lt;p&gt;Small chunks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lower memory pressure&lt;/li&gt;
&lt;li&gt;more granular progress&lt;/li&gt;
&lt;li&gt;easier retry boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more application overhead&lt;/li&gt;
&lt;li&gt;more messages&lt;/li&gt;
&lt;li&gt;more protocol bookkeeping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Large chunks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;less message overhead&lt;/li&gt;
&lt;li&gt;potentially better throughput&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;larger temporary memory&lt;/li&gt;
&lt;li&gt;less granular resume&lt;/li&gt;
&lt;li&gt;more painful retries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many implementations, something in the tens of kilobytes is a reasonable place to start.&lt;/p&gt;

&lt;p&gt;But benchmark on your target browsers.&lt;/p&gt;

&lt;p&gt;Do not copy a chunk size from a blog post and assume it is correct for your product.&lt;/p&gt;

&lt;p&gt;Including this one.&lt;/p&gt;




&lt;h1&gt;
  
  
  Throughput is not just network bandwidth
&lt;/h1&gt;

&lt;p&gt;Let's say the sender has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 Gbps connection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does not mean you will get a 1 Gbps browser transfer.&lt;/p&gt;

&lt;p&gt;Other bottlenecks may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wi-Fi&lt;/li&gt;
&lt;li&gt;CPU&lt;/li&gt;
&lt;li&gt;encryption overhead&lt;/li&gt;
&lt;li&gt;JavaScript execution&lt;/li&gt;
&lt;li&gt;disk read speed&lt;/li&gt;
&lt;li&gt;disk write speed&lt;/li&gt;
&lt;li&gt;browser implementation&lt;/li&gt;
&lt;li&gt;TURN path&lt;/li&gt;
&lt;li&gt;packet loss&lt;/li&gt;
&lt;li&gt;mobile hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need to measure the real end-to-end pipeline.&lt;/p&gt;

&lt;p&gt;A file transfer is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;disk read
   ↓
JavaScript
   ↓
WebRTC buffer
   ↓
network
   ↓
WebRTC receiver
   ↓
JavaScript
   ↓
disk write
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The slowest part wins.&lt;/p&gt;




&lt;h1&gt;
  
  
  Local network transfers can be extremely fast
&lt;/h1&gt;

&lt;p&gt;One thing that makes peer-to-peer transfer interesting is that nearby devices may be able to communicate efficiently over the local network.&lt;/p&gt;

&lt;p&gt;If two capable machines are on fast Wi-Fi or Ethernet, local transfers can perform very well.&lt;/p&gt;

&lt;p&gt;But again, there are no guarantees.&lt;/p&gt;

&lt;p&gt;Network topology matters.&lt;/p&gt;

&lt;p&gt;Browsers matter.&lt;/p&gt;

&lt;p&gt;Routing matters.&lt;/p&gt;

&lt;p&gt;You should never hardcode claims like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;always gigabit speed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;unless you can actually guarantee that.&lt;/p&gt;

&lt;p&gt;You usually cannot.&lt;/p&gt;

&lt;p&gt;A better claim is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Performance depends on the connection between the devices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Less exciting.&lt;/p&gt;

&lt;p&gt;More accurate.&lt;/p&gt;




&lt;h1&gt;
  
  
  Error messages matter
&lt;/h1&gt;

&lt;p&gt;Developers love errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ICE connection state failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users do not.&lt;/p&gt;

&lt;p&gt;They need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Could not establish a direct connection. Retrying...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The transfer was interrupted. Reconnecting from 42.8GB...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your device went offline. Check your connection and try again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good transfer product converts networking details into understandable states.&lt;/p&gt;




&lt;h1&gt;
  
  
  Connection state is more nuanced than connected/disconnected
&lt;/h1&gt;

&lt;p&gt;You will see states like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;new
checking
connected
completed
disconnected
failed
closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;disconnected&lt;/code&gt; does not always mean permanently dead.&lt;/p&gt;

&lt;p&gt;A network switch may briefly cause disconnects.&lt;/p&gt;

&lt;p&gt;That means your app should not immediately destroy the session when the connection flickers.&lt;/p&gt;

&lt;p&gt;You need grace periods.&lt;/p&gt;

&lt;p&gt;Retries.&lt;/p&gt;

&lt;p&gt;State transitions.&lt;/p&gt;

&lt;p&gt;This is why I eventually stopped using simple flags and started thinking in terms of a state machine.&lt;/p&gt;




&lt;h1&gt;
  
  
  A transfer state machine is worth it
&lt;/h1&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IDLE
  ↓
CREATING_ROOM
  ↓
WAITING_FOR_PEER
  ↓
CONNECTING
  ↓
READY
  ↓
TRANSFERRING
  ↓
COMPLETED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With side paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TRANSFERRING
  ↓
DISCONNECTED
  ↓
RECONNECTING
  ↓
RESUMING
  ↓
TRANSFERRING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RECONNECTING
  ↓
FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure makes your UI, analytics, and error recovery much cleaner.&lt;/p&gt;

&lt;p&gt;It is much better than managing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;isSending&lt;/span&gt;
&lt;span class="nx"&gt;isConnected&lt;/span&gt;
&lt;span class="nx"&gt;isReconnecting&lt;/span&gt;
&lt;span class="nx"&gt;isDone&lt;/span&gt;
&lt;span class="nx"&gt;hasError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and hoping every combination makes sense.&lt;/p&gt;




&lt;h1&gt;
  
  
  Don't tie your protocol directly to React state
&lt;/h1&gt;

&lt;p&gt;This one is more architectural.&lt;/p&gt;

&lt;p&gt;Your transfer engine should not exist as a pile of React state and hooks.&lt;/p&gt;

&lt;p&gt;The transfer process can live for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;minutes
hours
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It deserves its own abstraction.&lt;/p&gt;

&lt;p&gt;I prefer separating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI
Protocol
Transport
File IO
Session state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React UI
   ↓
Transfer Controller
   ↓
Transfer Protocol
   ↓
WebRTC Transport
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes debugging much easier.&lt;/p&gt;

&lt;p&gt;It also means the networking logic is not constantly being rewritten because the UI changed.&lt;/p&gt;




&lt;h1&gt;
  
  
  Multiple files are harder than one huge file
&lt;/h1&gt;

&lt;p&gt;A single 100GB file has one nice property:&lt;/p&gt;

&lt;p&gt;It is one continuous byte range.&lt;/p&gt;

&lt;p&gt;A folder containing 20,000 files is more complicated.&lt;/p&gt;

&lt;p&gt;Now you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file boundaries&lt;/li&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;li&gt;directory structure&lt;/li&gt;
&lt;li&gt;relative paths&lt;/li&gt;
&lt;li&gt;empty directories&lt;/li&gt;
&lt;li&gt;duplicate names&lt;/li&gt;
&lt;li&gt;skipped files&lt;/li&gt;
&lt;li&gt;per-file progress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The protocol becomes something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TRANSFER_START

FILE_START
CHUNKS...
FILE_END

FILE_START
CHUNKS...
FILE_END

...

TRANSFER_END
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For folders, you also need path information.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file-start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hero.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"relativePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"project/assets/images/hero.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;834021&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the receiver can reconstruct the structure.&lt;/p&gt;




&lt;h1&gt;
  
  
  Sending 20,000 tiny files can be worse than one huge file
&lt;/h1&gt;

&lt;p&gt;This is another surprise.&lt;/p&gt;

&lt;p&gt;A 50GB single video may be easier to handle than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;50GB spread across 100,000 tiny files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because every file adds overhead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;li&gt;open/close operations&lt;/li&gt;
&lt;li&gt;filesystem work&lt;/li&gt;
&lt;li&gt;protocol messages&lt;/li&gt;
&lt;li&gt;progress updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why bundling directories can sometimes make sense.&lt;/p&gt;

&lt;p&gt;But then you have tradeoffs around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ZIP creation time&lt;/li&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;browser support&lt;/li&gt;
&lt;li&gt;streaming archives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing is free.&lt;/p&gt;




&lt;h1&gt;
  
  
  The browser is capable, but not unlimited
&lt;/h1&gt;

&lt;p&gt;I love building serious applications in the browser.&lt;/p&gt;

&lt;p&gt;But it is important to respect the environment.&lt;/p&gt;

&lt;p&gt;A browser tab is not a native daemon.&lt;/p&gt;

&lt;p&gt;It can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;suspended&lt;/li&gt;
&lt;li&gt;throttled&lt;/li&gt;
&lt;li&gt;killed&lt;/li&gt;
&lt;li&gt;reloaded&lt;/li&gt;
&lt;li&gt;closed accidentally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your design needs to assume imperfect conditions.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;save session state where reasonable&lt;/li&gt;
&lt;li&gt;support reconnection&lt;/li&gt;
&lt;li&gt;avoid huge in-memory structures&lt;/li&gt;
&lt;li&gt;communicate clearly with the user&lt;/li&gt;
&lt;li&gt;test long-running transfers&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Test duration, not just file size
&lt;/h1&gt;

&lt;p&gt;This is a subtle one.&lt;/p&gt;

&lt;p&gt;You may test a 50GB transfer on a fast LAN and finish quickly.&lt;/p&gt;

&lt;p&gt;Then assume 50GB works.&lt;/p&gt;

&lt;p&gt;But a remote user with slow upload might take several hours.&lt;/p&gt;

&lt;p&gt;That means your system also needs to survive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;long duration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;not just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;large byte count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are different stress tests.&lt;/p&gt;

&lt;p&gt;I would test scenarios like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5GB over fast LAN
50GB over fast LAN
5GB over slow internet
50GB over throttled internet
2-hour transfer
4-hour transfer
mobile sender
mobile receiver
network interruption mid-transfer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You learn different things from each one.&lt;/p&gt;




&lt;h1&gt;
  
  
  Simulate bad networks
&lt;/h1&gt;

&lt;p&gt;Do not test only on your office Wi-Fi.&lt;/p&gt;

&lt;p&gt;Large-transfer software needs to survive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;latency&lt;/li&gt;
&lt;li&gt;packet loss&lt;/li&gt;
&lt;li&gt;slow uploads&lt;/li&gt;
&lt;li&gt;unstable Wi-Fi&lt;/li&gt;
&lt;li&gt;connection changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use network throttling where possible.&lt;/p&gt;

&lt;p&gt;Switch networks manually.&lt;/p&gt;

&lt;p&gt;Turn Wi-Fi off temporarily.&lt;/p&gt;

&lt;p&gt;Lock the phone.&lt;/p&gt;

&lt;p&gt;Background the browser.&lt;/p&gt;

&lt;p&gt;Try the things real users will accidentally do.&lt;/p&gt;

&lt;p&gt;The bugs you find this way are usually much more valuable than another perfect localhost test.&lt;/p&gt;




&lt;h1&gt;
  
  
  TURN fallback needs real-world testing
&lt;/h1&gt;

&lt;p&gt;It is easy to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TURN works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You need to test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;different NAT types&lt;/li&gt;
&lt;li&gt;corporate networks&lt;/li&gt;
&lt;li&gt;VPNs&lt;/li&gt;
&lt;li&gt;mobile carriers&lt;/li&gt;
&lt;li&gt;IPv4&lt;/li&gt;
&lt;li&gt;IPv6&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then monitor how often TURN is actually used.&lt;/p&gt;

&lt;p&gt;If your product starts growing and 40% of huge transfers are going through TURN, your cost model looks very different from what you expected.&lt;/p&gt;




&lt;h1&gt;
  
  
  Security is more than encrypted transport
&lt;/h1&gt;

&lt;p&gt;WebRTC uses encrypted transport, which is great.&lt;/p&gt;

&lt;p&gt;But your application still needs to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;room guessing&lt;/li&gt;
&lt;li&gt;unauthorized joining&lt;/li&gt;
&lt;li&gt;expired sessions&lt;/li&gt;
&lt;li&gt;signaling authentication&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;replay&lt;/li&gt;
&lt;li&gt;transfer links&lt;/li&gt;
&lt;li&gt;metadata exposure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your room code is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ABC123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;how hard is it to guess?&lt;/p&gt;

&lt;p&gt;How many attempts can someone make?&lt;/p&gt;

&lt;p&gt;How long does that room exist?&lt;/p&gt;

&lt;p&gt;Does the share URL include additional entropy?&lt;/p&gt;

&lt;p&gt;Security is a system property.&lt;/p&gt;

&lt;p&gt;You cannot just say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WebRTC is encrypted, therefore we're done.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Keep transfer sessions short-lived
&lt;/h1&gt;

&lt;p&gt;For a real-time transfer product, there is rarely a reason for room/session state to live forever.&lt;/p&gt;

&lt;p&gt;Short-lived sessions reduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stale data&lt;/li&gt;
&lt;li&gt;attack surface&lt;/li&gt;
&lt;li&gt;database clutter&lt;/li&gt;
&lt;li&gt;accidental reuse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A session can expire after inactivity.&lt;/p&gt;

&lt;p&gt;Once both peers leave, clean it up.&lt;/p&gt;

&lt;p&gt;This keeps the coordination layer simple.&lt;/p&gt;




&lt;h1&gt;
  
  
  Observability becomes essential
&lt;/h1&gt;

&lt;p&gt;When users tell you:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It stopped at 67%.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;that is not enough information.&lt;/p&gt;

&lt;p&gt;You need operational data that helps diagnose the failure.&lt;/p&gt;

&lt;p&gt;Useful telemetry might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser
browser version
OS
connection type
peer state transitions
ICE state
candidate type
direct vs relay
transfer size
bytes completed
transfer duration
error stage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do not need to inspect users' files.&lt;/p&gt;

&lt;p&gt;You do need to understand why the transport failed.&lt;/p&gt;




&lt;h1&gt;
  
  
  Candidate type is useful to measure
&lt;/h1&gt;

&lt;p&gt;Knowing whether the transfer used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;host
srflx
relay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can help explain performance.&lt;/p&gt;

&lt;p&gt;If poor-performing sessions are mostly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;relay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that tells you something.&lt;/p&gt;

&lt;p&gt;If failures are concentrated in one browser version, that tells you something else.&lt;/p&gt;

&lt;p&gt;Data helps you avoid guessing.&lt;/p&gt;




&lt;h1&gt;
  
  
  Success rate matters more than benchmark speed
&lt;/h1&gt;

&lt;p&gt;It is tempting to market:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 Mbps transfer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the metric I care about more is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What percentage of transfers actually complete?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would rather have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;95% completion at 200 Mbps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;70% completion at 600 Mbps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For huge files, reliability is the product.&lt;/p&gt;

&lt;p&gt;Speed only matters after reliability.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I learned about "no file size limit"
&lt;/h1&gt;

&lt;p&gt;Technically, with a streaming architecture, you do not need a traditional server-side file-size cap because the full file is not sitting in your application memory or object storage.&lt;/p&gt;

&lt;p&gt;But that does not mean there are no practical limits.&lt;/p&gt;

&lt;p&gt;The real constraints become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;browser capabilities&lt;/li&gt;
&lt;li&gt;device storage&lt;/li&gt;
&lt;li&gt;device memory&lt;/li&gt;
&lt;li&gt;network stability&lt;/li&gt;
&lt;li&gt;transfer duration&lt;/li&gt;
&lt;li&gt;filesystem support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the phrase:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;no file size limit&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;should mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the application does not impose an arbitrary product cap&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;physics and device limitations no longer exist&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction is important.&lt;/p&gt;




&lt;h1&gt;
  
  
  Large transfer UX needs constant reassurance
&lt;/h1&gt;

&lt;p&gt;A 100GB transfer can take a long time.&lt;/p&gt;

&lt;p&gt;Users need confidence that something is happening.&lt;/p&gt;

&lt;p&gt;Good UI should show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bytes transferred&lt;/li&gt;
&lt;li&gt;percentage&lt;/li&gt;
&lt;li&gt;current speed&lt;/li&gt;
&lt;li&gt;estimated time remaining&lt;/li&gt;
&lt;li&gt;connection state&lt;/li&gt;
&lt;li&gt;sender/receiver status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;47.8 GB of 100 GB
47.8%
18.4 MB/s
~47 minutes remaining
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That feels trustworthy.&lt;/p&gt;

&lt;p&gt;A spinner does not.&lt;/p&gt;




&lt;h1&gt;
  
  
  ETA is harder than it looks
&lt;/h1&gt;

&lt;p&gt;If you calculate ETA from the current instant speed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;eta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;remainingBytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;currentSpeed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it jumps all over the place.&lt;/p&gt;

&lt;p&gt;Better to smooth throughput over time.&lt;/p&gt;

&lt;p&gt;For example, using a rolling average.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;last 10 seconds
last 30 seconds
exponential moving average
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces a much calmer ETA.&lt;/p&gt;

&lt;p&gt;Nobody likes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12 minutes remaining
38 minutes remaining
8 minutes remaining
51 minutes remaining
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;every few seconds.&lt;/p&gt;




&lt;h1&gt;
  
  
  Progress should be receiver-confirmed when possible
&lt;/h1&gt;

&lt;p&gt;Again, this matters for trust.&lt;/p&gt;

&lt;p&gt;If the sender has queued 80GB but the receiver has only written 75GB, showing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;80%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may be misleading.&lt;/p&gt;

&lt;p&gt;For long transfers, I prefer basing progress as much as possible on confirmed receiver state.&lt;/p&gt;

&lt;p&gt;That becomes especially valuable for resumability.&lt;/p&gt;




&lt;h1&gt;
  
  
  Don't optimize too early
&lt;/h1&gt;

&lt;p&gt;There is a temptation to start with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parallel channels&lt;/li&gt;
&lt;li&gt;adaptive chunking&lt;/li&gt;
&lt;li&gt;compression&lt;/li&gt;
&lt;li&gt;exotic scheduling&lt;/li&gt;
&lt;li&gt;custom congestion tricks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before doing that, get the fundamentals right:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;stable connection&lt;/li&gt;
&lt;li&gt;bounded memory&lt;/li&gt;
&lt;li&gt;backpressure&lt;/li&gt;
&lt;li&gt;progressive writing&lt;/li&gt;
&lt;li&gt;accurate state&lt;/li&gt;
&lt;li&gt;reconnect&lt;/li&gt;
&lt;li&gt;resume&lt;/li&gt;
&lt;li&gt;integrity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only then optimize.&lt;/p&gt;

&lt;p&gt;A boring transfer engine that finishes is better than a clever one that breaks.&lt;/p&gt;




&lt;h1&gt;
  
  
  When parallel data channels might help
&lt;/h1&gt;

&lt;p&gt;It is tempting to open multiple data channels and send parts of the file concurrently.&lt;/p&gt;

&lt;p&gt;That can sometimes improve throughput in specific situations.&lt;/p&gt;

&lt;p&gt;But it also creates more complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ordering&lt;/li&gt;
&lt;li&gt;reassembly&lt;/li&gt;
&lt;li&gt;congestion&lt;/li&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;debugging&lt;/li&gt;
&lt;li&gt;resume state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would not start there.&lt;/p&gt;

&lt;p&gt;One well-managed reliable channel can go a long way.&lt;/p&gt;

&lt;p&gt;Optimize only after measurements show a real bottleneck.&lt;/p&gt;




&lt;h1&gt;
  
  
  WebRTC is not always the right answer
&lt;/h1&gt;

&lt;p&gt;This is important.&lt;/p&gt;

&lt;p&gt;If the sender does not need to stay online, WebRTC is probably not the right architecture.&lt;/p&gt;

&lt;p&gt;If the recipient needs to download tomorrow, use storage.&lt;/p&gt;

&lt;p&gt;If thousands of people need the same file, use a CDN.&lt;/p&gt;

&lt;p&gt;If the file needs permanent access, use object storage.&lt;/p&gt;

&lt;p&gt;WebRTC makes the most sense when:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;one sender
one receiver
both online
large file
real-time transfer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the sweet spot.&lt;/p&gt;




&lt;h1&gt;
  
  
  The architecture I prefer
&lt;/h1&gt;

&lt;p&gt;At a high level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        Signaling Server
          /        \
         /          \
        /            \
   Sender           Receiver
   Browser           Browser
      \               /
       \             /
        \           /
        WebRTC Channel
              |
              |
       Direct if possible
              |
              v
        File transfer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With TURN when necessary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   |
   v
TURN Relay
   |
   v
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the application protocol sits above the transport:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transfer Protocol
      |
      v
WebRTC Transport
      |
      v
Network
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation makes everything cleaner.&lt;/p&gt;




&lt;h1&gt;
  
  
  What ButterShare taught me
&lt;/h1&gt;

&lt;p&gt;Building ButterShare changed how I think about browser networking.&lt;/p&gt;

&lt;p&gt;At first, I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WebRTC can send binary data. Great. File transfer solved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now I think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WebRTC gives me a transport primitive. Everything else is engineering.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The hardest parts were not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;createDataChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hard parts were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;controlling memory&lt;/li&gt;
&lt;li&gt;handling backpressure&lt;/li&gt;
&lt;li&gt;surviving long transfers&lt;/li&gt;
&lt;li&gt;supporting mobile browsers&lt;/li&gt;
&lt;li&gt;recovering from disconnects&lt;/li&gt;
&lt;li&gt;making progress trustworthy&lt;/li&gt;
&lt;li&gt;keeping the protocol understandable&lt;/li&gt;
&lt;li&gt;avoiding cloud storage while still handling real-world network failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where the real work is.&lt;/p&gt;




&lt;h1&gt;
  
  
  If I were building it again
&lt;/h1&gt;

&lt;p&gt;I would do these things from day one:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Build a transfer protocol first
&lt;/h2&gt;

&lt;p&gt;Define the message types before wiring everything into the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Treat backpressure as a first-class feature
&lt;/h2&gt;

&lt;p&gt;Not an optimization.&lt;/p&gt;

&lt;p&gt;A requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Design resume early
&lt;/h2&gt;

&lt;p&gt;Do not wait until users complain about losing 80GB of progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Write progressively on the receiver
&lt;/h2&gt;

&lt;p&gt;Never assume the entire file can fit in memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Test mobile early
&lt;/h2&gt;

&lt;p&gt;Desktop success will give you false confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Measure direct vs relayed transfers
&lt;/h2&gt;

&lt;p&gt;TURN usage directly affects cost and performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Build a state machine
&lt;/h2&gt;

&lt;p&gt;Large-transfer state becomes too complex for a handful of booleans.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Separate UI from transport
&lt;/h2&gt;

&lt;p&gt;Do not make networking logic depend on component lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Track receiver-confirmed progress
&lt;/h2&gt;

&lt;p&gt;It makes resume and UX much better.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Optimize only after reliability
&lt;/h2&gt;

&lt;p&gt;A finished transfer beats a benchmark screenshot.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final thoughts
&lt;/h1&gt;

&lt;p&gt;WebRTC Data Channels are one of the most interesting browser technologies for direct file transfer.&lt;/p&gt;

&lt;p&gt;They let you build something that would have required native applications not that long ago.&lt;/p&gt;

&lt;p&gt;But the API itself is the easy part.&lt;/p&gt;

&lt;p&gt;The moment you move beyond small demo files, you start dealing with real systems problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;flow control&lt;/li&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;reliability&lt;/li&gt;
&lt;li&gt;networking&lt;/li&gt;
&lt;li&gt;browser behavior&lt;/li&gt;
&lt;li&gt;state management&lt;/li&gt;
&lt;li&gt;failure recovery&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly why I enjoy working on it.&lt;/p&gt;

&lt;p&gt;The user sees:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Send file&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The engineering underneath sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;discover peer
negotiate connection
traverse NAT
manage buffer
read chunks
send chunks
ack chunks
write chunks
track state
handle disconnect
reconnect
resume
verify
complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the gap between a demo and a product.&lt;/p&gt;

&lt;p&gt;I am building these ideas into &lt;strong&gt;ButterShare&lt;/strong&gt;, a browser-based peer-to-peer file transfer tool focused on moving large files directly between devices without relying on traditional cloud file storage.&lt;/p&gt;

&lt;p&gt;If you have worked on WebRTC Data Channels, large-file streaming, or browser networking, I would be interested to hear which problem caused you the most pain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ButterShare:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://buttershare.com/" rel="noopener noreferrer"&gt;https://buttershare.com/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you want to use WebRTC Data Channels for large file transfers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not load the whole file into memory.&lt;/li&gt;
&lt;li&gt;Chunk the file.&lt;/li&gt;
&lt;li&gt;Respect &lt;code&gt;bufferedAmount&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;bufferedAmountLowThreshold&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Build backpressure into the sender.&lt;/li&gt;
&lt;li&gt;Write progressively on the receiver.&lt;/li&gt;
&lt;li&gt;Design a real transfer protocol.&lt;/li&gt;
&lt;li&gt;Use acknowledgements.&lt;/li&gt;
&lt;li&gt;Make resume possible.&lt;/li&gt;
&lt;li&gt;Expect TURN.&lt;/li&gt;
&lt;li&gt;Test mobile browsers early.&lt;/li&gt;
&lt;li&gt;Track completion rate, not just speed.&lt;/li&gt;
&lt;li&gt;Treat 100GB transfers as long-running sessions, not oversized attachments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest lesson?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebRTC can move the bytes. Your job is making sure the transfer survives everything that happens around those bytes.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>programming</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Send 100GB+ Files Directly Between Browsers Without Cloud Storage</title>
      <dc:creator>Anirban Roy</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:24:40 +0000</pubDate>
      <link>https://dev.to/anirban00537/how-i-send-100gb-files-directly-between-browsers-without-cloud-storage-544j</link>
      <guid>https://dev.to/anirban00537/how-i-send-100gb-files-directly-between-browsers-without-cloud-storage-544j</guid>
      <description>&lt;p&gt;Most file-sharing products are built around the same basic idea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload the file to a server.&lt;/li&gt;
&lt;li&gt;Store it somewhere.&lt;/li&gt;
&lt;li&gt;Generate a link.&lt;/li&gt;
&lt;li&gt;Let the recipient download it later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That model is simple, reliable, and works extremely well for a lot of products.&lt;/p&gt;

&lt;p&gt;But it also creates a problem the moment you start dealing with very large files.&lt;/p&gt;

&lt;p&gt;A 20MB PDF is easy.&lt;/p&gt;

&lt;p&gt;A 500MB video is manageable.&lt;/p&gt;

&lt;p&gt;A 5GB project starts to become noticeable.&lt;/p&gt;

&lt;p&gt;At 50GB, 100GB, or more, suddenly everything becomes about storage quotas, upload times, bandwidth bills, server infrastructure, expired links, and paid plans.&lt;/p&gt;

&lt;p&gt;I kept coming back to one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the file already exists on one computer and the final destination is another computer, why does the entire file need to live on my server in the middle?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question eventually became the foundation for &lt;strong&gt;ButterShare&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;ButterShare is a browser-based peer-to-peer file transfer product. Instead of uploading the entire file to cloud storage first, the two browsers establish a connection and transfer the file directly.&lt;/p&gt;

&lt;p&gt;The basic architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Traditional file sharing:

Sender
   |
   v
Cloud Storage
   |
   v
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ButterShare is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
   |
   v
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, the real implementation is more complicated than that diagram suggests.&lt;/p&gt;

&lt;p&gt;There is signaling.&lt;/p&gt;

&lt;p&gt;There is WebRTC.&lt;/p&gt;

&lt;p&gt;There are NATs and firewalls.&lt;/p&gt;

&lt;p&gt;There is buffering.&lt;/p&gt;

&lt;p&gt;There is flow control.&lt;/p&gt;

&lt;p&gt;There are browser memory limits.&lt;/p&gt;

&lt;p&gt;There are mobile devices that decide your background tab is no longer important.&lt;/p&gt;

&lt;p&gt;And there is the very uncomfortable question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happens when a 100GB transfer fails at 97%?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is about what I learned while designing large browser-to-browser file transfer and why sending 100GB directly between browsers is a very different problem from sending a normal attachment.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first realization: file transfer and file storage are different products
&lt;/h2&gt;

&lt;p&gt;This sounds obvious, but a lot of file-sharing software combines the two.&lt;/p&gt;

&lt;p&gt;When someone says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Send this file to John.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;there are at least two possible interpretations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage-based sharing
&lt;/h3&gt;

&lt;p&gt;The sender uploads the file somewhere.&lt;/p&gt;

&lt;p&gt;The server keeps it.&lt;/p&gt;

&lt;p&gt;John downloads it whenever he wants.&lt;/p&gt;

&lt;p&gt;This is asynchronous.&lt;/p&gt;

&lt;p&gt;The sender can close their laptop and disappear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct transfer
&lt;/h3&gt;

&lt;p&gt;The sender and John are both online.&lt;/p&gt;

&lt;p&gt;The file moves from one device to the other.&lt;/p&gt;

&lt;p&gt;Once the transfer is complete, the infrastructure does not need to retain the actual file.&lt;/p&gt;

&lt;p&gt;This is synchronous.&lt;/p&gt;

&lt;p&gt;They solve different problems.&lt;/p&gt;

&lt;p&gt;Cloud storage is better when persistence matters.&lt;/p&gt;

&lt;p&gt;Direct transfer becomes interesting when both people are online and the only objective is moving data from A to B.&lt;/p&gt;

&lt;p&gt;For large files, removing the storage layer changes a lot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why 100GB changes the architecture
&lt;/h2&gt;

&lt;p&gt;Imagine a user wants to send a 100GB file.&lt;/p&gt;

&lt;p&gt;With a conventional cloud architecture, the file usually crosses your infrastructure twice.&lt;/p&gt;

&lt;p&gt;First:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender -&amp;gt; Your server/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your server/storage -&amp;gt; Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means a single 100GB transfer can result in roughly 200GB of file traffic moving through infrastructure you operate.&lt;/p&gt;

&lt;p&gt;And you still have to store the file somewhere in between.&lt;/p&gt;

&lt;p&gt;Now multiply that.&lt;/p&gt;

&lt;p&gt;100 users transferring 100GB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 TB uploaded
10 TB downloaded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1,000 users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 TB uploaded
100 TB downloaded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly your “simple file sharing app” becomes a storage and bandwidth business.&lt;/p&gt;

&lt;p&gt;That can absolutely be the correct business model.&lt;/p&gt;

&lt;p&gt;But it was not the model I wanted for ButterShare.&lt;/p&gt;

&lt;p&gt;I wanted the infrastructure to coordinate the connection without becoming the permanent warehouse for the actual file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why WebRTC fits this problem
&lt;/h2&gt;

&lt;p&gt;WebRTC is usually associated with video calls.&lt;/p&gt;

&lt;p&gt;But underneath the video/audio features is something extremely useful for file transfer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebRTC Data Channels.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A data channel lets two peers exchange arbitrary data.&lt;/p&gt;

&lt;p&gt;That can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;text&lt;/li&gt;
&lt;li&gt;JSON&lt;/li&gt;
&lt;li&gt;binary data&lt;/li&gt;
&lt;li&gt;file chunks&lt;/li&gt;
&lt;li&gt;application messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of streaming video frames between browsers, you can stream pieces of a file.&lt;/p&gt;

&lt;p&gt;Very simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;peerConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;peerConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file-transfer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onopen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Peer connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// receive file data&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is that once the peer connection is established, the data can often travel directly between the two devices.&lt;/p&gt;

&lt;p&gt;Your application server does not need to handle each file chunk.&lt;/p&gt;

&lt;p&gt;That is the key architectural difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  But WebRTC does not magically connect two browsers
&lt;/h2&gt;

&lt;p&gt;Before two browsers can communicate, they have to find each other.&lt;/p&gt;

&lt;p&gt;This is where signaling comes in.&lt;/p&gt;

&lt;p&gt;The signaling server exchanges connection metadata between the peers.&lt;/p&gt;

&lt;p&gt;Typical information includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SDP offers&lt;/li&gt;
&lt;li&gt;SDP answers&lt;/li&gt;
&lt;li&gt;ICE candidates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rough process looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender Browser
     |
     | create offer
     v
Signaling Server
     |
     | forward offer
     v
Receiver Browser

Receiver Browser
     |
     | create answer
     v
Signaling Server
     |
     | forward answer
     v
Sender Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sides also exchange ICE candidates.&lt;/p&gt;

&lt;p&gt;Eventually WebRTC attempts to establish the actual peer connection.&lt;/p&gt;

&lt;p&gt;The important distinction is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The signaling server helps the devices connect. It does not necessarily carry the file itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction matters enormously when the file is 100GB.&lt;/p&gt;

&lt;p&gt;A few KB of signaling traffic is trivial.&lt;/p&gt;

&lt;p&gt;100GB of file traffic is not.&lt;/p&gt;




&lt;h2&gt;
  
  
  STUN, TURN, and the part everyone leaves out
&lt;/h2&gt;

&lt;p&gt;This is where the nice architecture diagram starts becoming less clean.&lt;/p&gt;

&lt;p&gt;You cannot assume every pair of devices can establish a perfect direct connection.&lt;/p&gt;

&lt;p&gt;Users sit behind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;routers&lt;/li&gt;
&lt;li&gt;NAT&lt;/li&gt;
&lt;li&gt;carrier-grade NAT&lt;/li&gt;
&lt;li&gt;corporate networks&lt;/li&gt;
&lt;li&gt;restrictive firewalls&lt;/li&gt;
&lt;li&gt;mobile networks&lt;/li&gt;
&lt;li&gt;VPNs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebRTC uses ICE to figure out how the peers can communicate.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;STUN server&lt;/strong&gt; helps a device discover its public-facing network information.&lt;/p&gt;

&lt;p&gt;In ideal conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser A &amp;lt;----------------&amp;gt; Browser B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Direct.&lt;/p&gt;

&lt;p&gt;Beautiful.&lt;/p&gt;

&lt;p&gt;Cheap.&lt;/p&gt;

&lt;p&gt;Fast.&lt;/p&gt;

&lt;p&gt;But sometimes direct connectivity is impossible.&lt;/p&gt;

&lt;p&gt;That is where a &lt;strong&gt;TURN server&lt;/strong&gt; becomes important.&lt;/p&gt;

&lt;p&gt;A TURN server relays traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser A
    |
    v
TURN Relay
    |
    v
Browser B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, your infrastructure may be handling the traffic again.&lt;/p&gt;

&lt;p&gt;The important difference is that relaying encrypted traffic is not the same thing as permanently storing the file.&lt;/p&gt;

&lt;p&gt;The relay forwards packets.&lt;/p&gt;

&lt;p&gt;It does not need to create a downloadable cloud copy of the file.&lt;/p&gt;

&lt;p&gt;Still, from an infrastructure perspective, TURN traffic is real bandwidth and needs to be treated seriously.&lt;/p&gt;

&lt;p&gt;So “peer-to-peer” does not mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;100% of every transfer will always bypass every server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The system attempts direct peer connectivity and can avoid traditional cloud file storage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much more accurate description.&lt;/p&gt;




&lt;h2&gt;
  
  
  The naive implementation fails immediately
&lt;/h2&gt;

&lt;p&gt;When developers first build browser file transfer, the natural approach often looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fine for small files.&lt;/p&gt;

&lt;p&gt;It is a terrible idea for 100GB files.&lt;/p&gt;

&lt;p&gt;If the file is 100GB, you obviously do not want to load 100GB into browser memory.&lt;/p&gt;

&lt;p&gt;Even far smaller files can create problems.&lt;/p&gt;

&lt;p&gt;The browser may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;freeze&lt;/li&gt;
&lt;li&gt;consume huge amounts of memory&lt;/li&gt;
&lt;li&gt;kill the tab&lt;/li&gt;
&lt;li&gt;become unresponsive&lt;/li&gt;
&lt;li&gt;crash on mobile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Large file transfer needs to be streaming-oriented.&lt;/p&gt;

&lt;p&gt;The file should be processed incrementally.&lt;/p&gt;




&lt;h1&gt;
  
  
  Chunking the file
&lt;/h1&gt;

&lt;p&gt;Instead of treating the file as one huge object, divide it into manageable chunks.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100GB file

[chunk 1]
[chunk 2]
[chunk 3]
[chunk 4]
...
[chunk 1,500,000]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact number depends on your chunk size.&lt;/p&gt;

&lt;p&gt;A simplified sender might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This solves one major problem:&lt;/p&gt;

&lt;p&gt;You no longer need the whole file in memory.&lt;/p&gt;

&lt;p&gt;But this code still has another serious problem.&lt;/p&gt;

&lt;p&gt;It sends data as fast as JavaScript can produce it.&lt;/p&gt;

&lt;p&gt;The network cannot necessarily keep up.&lt;/p&gt;




&lt;h1&gt;
  
  
  Backpressure is one of the most important parts
&lt;/h1&gt;

&lt;p&gt;A WebRTC data channel has an internal send buffer.&lt;/p&gt;

&lt;p&gt;You can inspect it using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmount&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you continuously call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;faster than the connection can transmit data, the buffer keeps growing.&lt;/p&gt;

&lt;p&gt;Eventually you can create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;huge memory usage&lt;/li&gt;
&lt;li&gt;poor performance&lt;/li&gt;
&lt;li&gt;browser instability&lt;/li&gt;
&lt;li&gt;transfer failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you need backpressure.&lt;/p&gt;

&lt;p&gt;Instead of endlessly pushing data, you wait when the channel buffer gets too large.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_BUFFER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_BUFFER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A better implementation can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;bufferedAmountLowThreshold&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;onbufferedamountlow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than polling.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmountLowThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitUntilWritable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufferedAmountLowThreshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onbufferedamountlow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the biggest differences between a demo file-transfer project and something designed for genuinely large files.&lt;/p&gt;




&lt;h1&gt;
  
  
  Metadata comes before the bytes
&lt;/h1&gt;

&lt;p&gt;The receiver needs to know what is coming.&lt;/p&gt;

&lt;p&gt;Before transmitting file chunks, send metadata.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file-start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"project-final.mov"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;107374182400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mimeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"video/quicktime"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The receiver can then prepare for the incoming stream.&lt;/p&gt;

&lt;p&gt;You may also send information such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"relativePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"project/videos/project-final.mov"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chunkSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalChunks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1638400&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This becomes particularly important when sending folders containing multiple files.&lt;/p&gt;

&lt;p&gt;The transport layer should understand the difference between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;binary file chunk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transfer complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple message protocol might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TRANSFER_START
FILE_START
FILE_CHUNK
FILE_END
TRANSFER_END
ERROR
ACK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you start thinking in terms of a protocol rather than random WebRTC messages, the system becomes much easier to reason about.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why 100GB means you need BigInt thinking
&lt;/h1&gt;

&lt;p&gt;JavaScript numbers can safely represent integers up to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9,007,199,254,740,991
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So 100GB itself is nowhere near the safe-integer limit.&lt;/p&gt;

&lt;p&gt;But large-file systems still benefit from being disciplined about offsets, byte counts, chunk indexes, and serialization.&lt;/p&gt;

&lt;p&gt;You do not want something like this hidden deep inside your protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunksReceived&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without considering the final partial chunk, reconnect offsets, multiple files, and aggregated folder size.&lt;/p&gt;

&lt;p&gt;For real transfers, I prefer tracking actual byte counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;bytesSent&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then progress becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;percentage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytesSent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, but reliable.&lt;/p&gt;




&lt;h1&gt;
  
  
  Progress bars lie if you build them badly
&lt;/h1&gt;

&lt;p&gt;A file-transfer progress bar looks trivial.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;Suppose you have sent data into the WebRTC buffer.&lt;/p&gt;

&lt;p&gt;Does that mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript processed it?&lt;/li&gt;
&lt;li&gt;the browser queued it?&lt;/li&gt;
&lt;li&gt;the network transmitted it?&lt;/li&gt;
&lt;li&gt;the receiver received it?&lt;/li&gt;
&lt;li&gt;the receiver wrote it to disk?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are different stages.&lt;/p&gt;

&lt;p&gt;If you update progress immediately after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dataChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you are measuring how much data you queued, not necessarily how much arrived.&lt;/p&gt;

&lt;p&gt;For small files, nobody notices.&lt;/p&gt;

&lt;p&gt;For a 100GB transfer, the difference can become substantial.&lt;/p&gt;

&lt;p&gt;A more reliable system can use receiver acknowledgements.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender:
chunk 1000 sent

Receiver:
chunk 1000 received

Sender:
update confirmed progress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You probably do not want an acknowledgement for every tiny chunk because that adds overhead.&lt;/p&gt;

&lt;p&gt;Instead, acknowledge ranges or byte offsets periodically.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"receivedBytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5368709120&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the sender knows the receiver has confirmed roughly 5GB.&lt;/p&gt;

&lt;p&gt;That is much more meaningful.&lt;/p&gt;




&lt;h1&gt;
  
  
  The receiver has the same memory problem
&lt;/h1&gt;

&lt;p&gt;Chunking the sender is only half the solution.&lt;/p&gt;

&lt;p&gt;A naive receiver might do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, fine for small files.&lt;/p&gt;

&lt;p&gt;Terrible for 100GB.&lt;/p&gt;

&lt;p&gt;You have just moved the memory problem from the sender to the receiver.&lt;/p&gt;

&lt;p&gt;You do not want to accumulate 100GB of chunks in RAM.&lt;/p&gt;

&lt;p&gt;For truly large files, the receiver needs a way to write progressively.&lt;/p&gt;

&lt;p&gt;Modern browser capabilities can make this possible, but support varies.&lt;/p&gt;

&lt;p&gt;Depending on the browser and platform, you may use mechanisms such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File System Access API&lt;/li&gt;
&lt;li&gt;writable file streams&lt;/li&gt;
&lt;li&gt;browser-specific download strategies&lt;/li&gt;
&lt;li&gt;temporary storage&lt;/li&gt;
&lt;li&gt;service workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where browser compatibility becomes a major design constraint.&lt;/p&gt;

&lt;p&gt;Desktop Chromium gives you capabilities that mobile Safari may not.&lt;/p&gt;

&lt;p&gt;If your product promises cross-platform transfer, you cannot design only for Chrome on your MacBook.&lt;/p&gt;




&lt;h1&gt;
  
  
  Desktop browsers are easy mode compared with mobile
&lt;/h1&gt;

&lt;p&gt;Large file transfer on desktop is one problem.&lt;/p&gt;

&lt;p&gt;Large file transfer on phones is another.&lt;/p&gt;

&lt;p&gt;Mobile browsers have much tighter constraints around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;storage&lt;/li&gt;
&lt;li&gt;background execution&lt;/li&gt;
&lt;li&gt;battery&lt;/li&gt;
&lt;li&gt;tab suspension&lt;/li&gt;
&lt;li&gt;screen locking&lt;/li&gt;
&lt;li&gt;operating-system lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A user can start a transfer and then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lock their phone&lt;/li&gt;
&lt;li&gt;switch apps&lt;/li&gt;
&lt;li&gt;put the browser in the background&lt;/li&gt;
&lt;li&gt;receive a phone call&lt;/li&gt;
&lt;li&gt;enter low-power mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And suddenly your beautiful WebRTC connection is gone.&lt;/p&gt;

&lt;p&gt;This creates UX requirements you do not think about in a desktop-only prototype.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep this tab open until the transfer finishes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That message may sound primitive, but users need to understand the synchronous nature of peer-to-peer transfer.&lt;/p&gt;

&lt;p&gt;You also need to handle disconnects gracefully instead of pretending they will never happen.&lt;/p&gt;




&lt;h1&gt;
  
  
  The hardest question: what happens when the connection dies?
&lt;/h1&gt;

&lt;p&gt;Imagine transferring 100GB.&lt;/p&gt;

&lt;p&gt;You reach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;97.4GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then Wi-Fi drops.&lt;/p&gt;

&lt;p&gt;If your solution is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Please start again.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;the user is going to hate you.&lt;/p&gt;

&lt;p&gt;For genuinely large files, resumability becomes extremely valuable.&lt;/p&gt;

&lt;p&gt;That means the receiver and sender need to agree on how much data has already been transferred.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fileId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"receivedBytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;104689827840&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After reconnection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender:
Okay, you already have 104,689,827,840 bytes.

Resume from there.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you resume from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds easy.&lt;/p&gt;

&lt;p&gt;It becomes more complicated with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple files&lt;/li&gt;
&lt;li&gt;folders&lt;/li&gt;
&lt;li&gt;changed files&lt;/li&gt;
&lt;li&gt;chunk verification&lt;/li&gt;
&lt;li&gt;interrupted writes&lt;/li&gt;
&lt;li&gt;reconnecting to the wrong session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once again, what started as a file-transfer feature becomes a protocol-design problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  File integrity matters
&lt;/h1&gt;

&lt;p&gt;A transfer showing “100%” does not necessarily mean the resulting file is correct.&lt;/p&gt;

&lt;p&gt;Networks and transport protocols are reliable at lower layers, but once you build your own application-level chunking, reconnection, writing, and resume logic, verification becomes valuable.&lt;/p&gt;

&lt;p&gt;One option is hashing.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender hash:
abc123...

Receiver hash:
abc123...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If they match, great.&lt;/p&gt;

&lt;p&gt;For huge files, hashing the entire file before transfer can itself take time.&lt;/p&gt;

&lt;p&gt;So you may prefer chunk-level hashing or incremental hashing.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Chunk 1 -&amp;gt; hash
Chunk 2 -&amp;gt; hash
Chunk 3 -&amp;gt; hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or calculate a streaming digest while data moves.&lt;/p&gt;

&lt;p&gt;The right choice depends on how much integrity protection your product needs versus the CPU and complexity cost.&lt;/p&gt;




&lt;h1&gt;
  
  
  Folder transfer introduces another layer
&lt;/h1&gt;

&lt;p&gt;Sending one 100GB file is actually simpler than sending a folder containing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30,000 files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;even if the folder is only 20GB.&lt;/p&gt;

&lt;p&gt;Now you need to preserve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;filenames&lt;/li&gt;
&lt;li&gt;relative paths&lt;/li&gt;
&lt;li&gt;directory structure&lt;/li&gt;
&lt;li&gt;file ordering&lt;/li&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── assets/
│   ├── logo.png
│   └── hero.jpg
├── video/
│   └── final.mov
└── project.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You cannot just send bytes.&lt;/p&gt;

&lt;p&gt;You need a manifest.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assets/logo.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;204812&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assets/hero.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5410289&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"video/final.mov"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42342353433&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the receiver knows how to reconstruct the directory structure.&lt;/p&gt;

&lt;p&gt;This is why “send folder” is not just “send file but bigger.”&lt;/p&gt;




&lt;h1&gt;
  
  
  Encryption
&lt;/h1&gt;

&lt;p&gt;One advantage of WebRTC is that encryption is part of the transport.&lt;/p&gt;

&lt;p&gt;WebRTC data channels use secure transport mechanisms, so you are not sending arbitrary plaintext packets across the internet.&lt;/p&gt;

&lt;p&gt;But you still need to think carefully about the broader security model.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;h3&gt;
  
  
  How does someone join a transfer?
&lt;/h3&gt;

&lt;p&gt;Maybe you use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;room code&lt;/li&gt;
&lt;li&gt;secret link&lt;/li&gt;
&lt;li&gt;QR code&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Can someone guess the room?
&lt;/h3&gt;

&lt;p&gt;Your identifiers need enough entropy.&lt;/p&gt;

&lt;p&gt;A room system like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ABC123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is convenient, but the security model depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code length&lt;/li&gt;
&lt;li&gt;expiration&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;session lifetime&lt;/li&gt;
&lt;li&gt;whether links contain additional secrets&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What does the signaling server know?
&lt;/h3&gt;

&lt;p&gt;Ideally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;connection metadata&lt;/li&gt;
&lt;li&gt;temporary room information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;but not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;permanent copies of user files&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How long do sessions live?
&lt;/h3&gt;

&lt;p&gt;Temporary sessions should actually be temporary.&lt;/p&gt;

&lt;p&gt;Security is not just encryption.&lt;/p&gt;

&lt;p&gt;It is also minimizing what exists and for how long.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I did not want file storage in the architecture
&lt;/h1&gt;

&lt;p&gt;There are obvious infrastructure reasons.&lt;/p&gt;

&lt;p&gt;If ButterShare stored every file, I would eventually need to solve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;object storage&lt;/li&gt;
&lt;li&gt;storage lifecycle&lt;/li&gt;
&lt;li&gt;cleanup jobs&lt;/li&gt;
&lt;li&gt;bandwidth costs&lt;/li&gt;
&lt;li&gt;abuse&lt;/li&gt;
&lt;li&gt;copyright complaints&lt;/li&gt;
&lt;li&gt;malware storage&lt;/li&gt;
&lt;li&gt;retention policies&lt;/li&gt;
&lt;li&gt;privacy&lt;/li&gt;
&lt;li&gt;expired transfers&lt;/li&gt;
&lt;li&gt;storage quotas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Removing persistent file storage does not eliminate every problem.&lt;/p&gt;

&lt;p&gt;But it dramatically changes the product.&lt;/p&gt;

&lt;p&gt;The infrastructure becomes much more about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;connection coordination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hosting arbitrary user data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a very different operational model.&lt;/p&gt;




&lt;h1&gt;
  
  
  P2P does not mean free infrastructure
&lt;/h1&gt;

&lt;p&gt;This is worth emphasizing.&lt;/p&gt;

&lt;p&gt;People sometimes hear peer-to-peer and assume:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No server costs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not true.&lt;/p&gt;

&lt;p&gt;You still need infrastructure for things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signaling&lt;/li&gt;
&lt;li&gt;authentication if applicable&lt;/li&gt;
&lt;li&gt;room coordination&lt;/li&gt;
&lt;li&gt;STUN&lt;/li&gt;
&lt;li&gt;TURN&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;abuse prevention&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And TURN can become expensive if a meaningful percentage of transfers are relayed.&lt;/p&gt;

&lt;p&gt;If a 100GB transfer has to go through your TURN infrastructure, you are moving a lot of bandwidth.&lt;/p&gt;

&lt;p&gt;So the business model still needs to understand relay rates.&lt;/p&gt;

&lt;p&gt;You should measure things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;% direct peer connections
% relayed connections
average transfer size
average duration
connection failure rate
reconnect rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this data, you do not really know how your P2P architecture behaves in the real world.&lt;/p&gt;




&lt;h1&gt;
  
  
  Connection speed is not magic
&lt;/h1&gt;

&lt;p&gt;If two users have slow internet, P2P does not fix physics.&lt;/p&gt;

&lt;p&gt;Suppose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender upload: 20 Mbps
Receiver download: 500 Mbps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transfer is still limited by approximately the sender's available upload bandwidth.&lt;/p&gt;

&lt;p&gt;Likewise, Wi-Fi quality, mobile signal, congestion, TURN routing, and device performance can all affect throughput.&lt;/p&gt;

&lt;p&gt;“No server upload step” does not mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;instant 100GB transfer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;100GB is still 100GB.&lt;/p&gt;

&lt;p&gt;What changes is where the data goes.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100GB to cloud
then
100GB from cloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you are trying to make the destination the receiver from the beginning.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why this architecture becomes attractive for large files
&lt;/h1&gt;

&lt;p&gt;For small files, the benefits may not feel dramatic.&lt;/p&gt;

&lt;p&gt;If you are sending a 3MB image, uploading it to a server is basically free from the user's perspective.&lt;/p&gt;

&lt;p&gt;But consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5GB
20GB
50GB
100GB
500GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the file size increases, the architecture matters more.&lt;/p&gt;

&lt;p&gt;Cloud storage adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an intermediate upload&lt;/li&gt;
&lt;li&gt;storage requirements&lt;/li&gt;
&lt;li&gt;infrastructure bandwidth&lt;/li&gt;
&lt;li&gt;retention management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Direct transfer removes the need for permanent file hosting.&lt;/p&gt;

&lt;p&gt;That is why I think P2P becomes increasingly interesting as file size grows.&lt;/p&gt;




&lt;h1&gt;
  
  
  The browser is becoming an operating system
&lt;/h1&gt;

&lt;p&gt;One of the things I find most interesting about building ButterShare is how much browsers can now do.&lt;/p&gt;

&lt;p&gt;A web application can access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;files&lt;/li&gt;
&lt;li&gt;directories&lt;/li&gt;
&lt;li&gt;WebRTC&lt;/li&gt;
&lt;li&gt;streaming APIs&lt;/li&gt;
&lt;li&gt;cryptography&lt;/li&gt;
&lt;li&gt;persistent storage&lt;/li&gt;
&lt;li&gt;service workers&lt;/li&gt;
&lt;li&gt;filesystem capabilities on supported platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Years ago, a cross-platform file-transfer product almost certainly meant shipping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Windows app
macOS app
Android app
iOS app
Linux app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a surprising amount can be done from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That has huge distribution advantages.&lt;/p&gt;

&lt;p&gt;The recipient does not necessarily need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;download an installer&lt;/li&gt;
&lt;li&gt;create an account&lt;/li&gt;
&lt;li&gt;configure software&lt;/li&gt;
&lt;li&gt;update an application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They can open a URL.&lt;/p&gt;

&lt;p&gt;That is one reason I think browser-based file transfer is more interesting now than it used to be.&lt;/p&gt;




&lt;h1&gt;
  
  
  But browser compatibility keeps you humble
&lt;/h1&gt;

&lt;p&gt;Just because an API exists does not mean it behaves identically everywhere.&lt;/p&gt;

&lt;p&gt;Anyone building serious browser software learns this quickly.&lt;/p&gt;

&lt;p&gt;You have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Chrome
Edge
Firefox
Safari
iOS Safari
Android Chrome
in-app browsers
older devices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And each one comes with different behavior around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WebRTC&lt;/li&gt;
&lt;li&gt;file APIs&lt;/li&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;downloads&lt;/li&gt;
&lt;li&gt;filesystem writing&lt;/li&gt;
&lt;li&gt;background tabs&lt;/li&gt;
&lt;li&gt;device sleep&lt;/li&gt;
&lt;li&gt;permission prompts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real product is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“It works on my machine.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The real product is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can an iPhone user send a huge file to a Windows user without understanding WebRTC?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much harder standard.&lt;/p&gt;




&lt;h1&gt;
  
  
  UX matters as much as networking
&lt;/h1&gt;

&lt;p&gt;Users do not care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ICE candidates&lt;/li&gt;
&lt;li&gt;SCTP&lt;/li&gt;
&lt;li&gt;DTLS&lt;/li&gt;
&lt;li&gt;NAT traversal&lt;/li&gt;
&lt;li&gt;bufferedAmount&lt;/li&gt;
&lt;li&gt;TURN allocation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They care about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Is my file sending?”&lt;/p&gt;

&lt;p&gt;“How long will it take?”&lt;/p&gt;

&lt;p&gt;“Did it fail?”&lt;/p&gt;

&lt;p&gt;“Can I try again?”&lt;/p&gt;

&lt;p&gt;“Can I close this tab?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means a large part of the engineering work ends up in UX.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;h3&gt;
  
  
  Bad
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Peer connection state: disconnected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Better
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connection interrupted. Trying to reconnect...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bad
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bufferedAmount = 4194304
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Better
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;42.7GB of 100GB transferred
Estimated time remaining: 18 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The networking layer can be brilliant, but if the UI makes the user nervous, the product still feels broken.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I would build differently if I started again
&lt;/h1&gt;

&lt;p&gt;The biggest lesson is that I would treat the transfer engine as its own system from day one.&lt;/p&gt;

&lt;p&gt;Not as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;some WebRTC code inside a React component
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but as a proper state machine.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IDLE
  |
  v
CREATING_SESSION
  |
  v
WAITING_FOR_PEER
  |
  v
CONNECTING
  |
  v
READY
  |
  v
TRANSFERRING
  |
  +----&amp;gt; PAUSED
  |
  +----&amp;gt; RECONNECTING
  |
  +----&amp;gt; FAILED
  |
  v
COMPLETED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That structure makes everything easier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reconnection&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;UI&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;debugging&lt;/li&gt;
&lt;li&gt;resume support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Large file transfer has too many edge cases to manage with a couple of booleans like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt;
&lt;span class="nx"&gt;isSending&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eventually those booleans turn into chaos.&lt;/p&gt;




&lt;h1&gt;
  
  
  I would also separate protocol from transport
&lt;/h1&gt;

&lt;p&gt;Another design decision I strongly recommend:&lt;/p&gt;

&lt;p&gt;Do not make your application protocol depend too tightly on WebRTC.&lt;/p&gt;

&lt;p&gt;Think of WebRTC as the transport.&lt;/p&gt;

&lt;p&gt;Your transfer protocol should define concepts such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;session start
file metadata
chunk
acknowledgement
resume
file complete
transfer complete
error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the transport layer handles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send(message)
receive(message)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes the architecture easier to test and potentially easier to extend later.&lt;/p&gt;

&lt;p&gt;For example, you could theoretically support another transport in the future without rewriting the entire transfer model.&lt;/p&gt;




&lt;h1&gt;
  
  
  Measuring the right things
&lt;/h1&gt;

&lt;p&gt;A transfer product needs good telemetry.&lt;/p&gt;

&lt;p&gt;Not the files.&lt;/p&gt;

&lt;p&gt;Not private user data.&lt;/p&gt;

&lt;p&gt;But operational metrics.&lt;/p&gt;

&lt;p&gt;I want to know things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;connection success rate
time to connect
direct vs relayed connections
average throughput
transfer completion rate
disconnect rate
browser distribution
OS distribution
average file size
failure stage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, if Safari has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;62% completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while Chrome has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;94% completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that tells you exactly where to focus engineering effort.&lt;/p&gt;

&lt;p&gt;If transfers larger than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;40GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;fail disproportionately often, that tells you something else.&lt;/p&gt;

&lt;p&gt;Without metrics, you are debugging anecdotes.&lt;/p&gt;




&lt;h1&gt;
  
  
  The most important lesson: 100GB is not just a bigger 1GB
&lt;/h1&gt;

&lt;p&gt;This is probably the main thing I learned.&lt;/p&gt;

&lt;p&gt;You cannot build file transfer for 500MB and assume it will naturally scale to 100GB.&lt;/p&gt;

&lt;p&gt;At very large sizes, every small weakness becomes visible.&lt;/p&gt;

&lt;p&gt;A tiny memory leak matters.&lt;/p&gt;

&lt;p&gt;A slightly inaccurate progress bar matters.&lt;/p&gt;

&lt;p&gt;A reconnect bug matters.&lt;/p&gt;

&lt;p&gt;A 0.5% failure rate matters.&lt;/p&gt;

&lt;p&gt;A browser sleeping after 30 minutes matters.&lt;/p&gt;

&lt;p&gt;The transfer may be running for hours.&lt;/p&gt;

&lt;p&gt;The software has to survive that.&lt;/p&gt;

&lt;p&gt;Large-file engineering is less about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can bytes move?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and more about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can bytes keep moving reliably for a very long time under imperfect real-world conditions?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the actual problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  Where ButterShare fits
&lt;/h1&gt;

&lt;p&gt;ButterShare is my attempt to make this architecture useful to normal people.&lt;/p&gt;

&lt;p&gt;The user should not need to know what WebRTC is.&lt;/p&gt;

&lt;p&gt;They should be able to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select a file or folder.&lt;/li&gt;
&lt;li&gt;Get a link or QR code.&lt;/li&gt;
&lt;li&gt;Send it to someone.&lt;/li&gt;
&lt;li&gt;Transfer the file directly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The product is built around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;browser-to-browser transfer&lt;/li&gt;
&lt;li&gt;peer-to-peer networking&lt;/li&gt;
&lt;li&gt;no traditional cloud file storage&lt;/li&gt;
&lt;li&gt;cross-platform usage&lt;/li&gt;
&lt;li&gt;large files&lt;/li&gt;
&lt;li&gt;no mandatory account creation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The technical complexity should stay underneath.&lt;/p&gt;

&lt;p&gt;The user should just see:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Send file.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  When P2P is the wrong choice
&lt;/h1&gt;

&lt;p&gt;I do not think peer-to-peer is automatically superior.&lt;/p&gt;

&lt;p&gt;There are situations where cloud storage is clearly better.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;h3&gt;
  
  
  Sender needs to go offline
&lt;/h3&gt;

&lt;p&gt;Use cloud storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recipient will download tomorrow
&lt;/h3&gt;

&lt;p&gt;Use cloud storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  50 people need the same file
&lt;/h3&gt;

&lt;p&gt;Cloud/CDN distribution probably makes more sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  You need permanent storage
&lt;/h3&gt;

&lt;p&gt;Obviously use storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  You need versioning and collaboration
&lt;/h3&gt;

&lt;p&gt;Again, cloud storage.&lt;/p&gt;

&lt;p&gt;P2P becomes compelling when:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;one sender
+
one receiver
+
both online
+
large file
+
no need for permanent hosting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the sweet spot.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final architecture
&lt;/h1&gt;

&lt;p&gt;At a high level, the system looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             +----------------------+
             |   Signaling Server   |
             +----------------------+
                  ^             ^
                  |             |
                  | metadata    |
                  | only        |
                  |             |
                  v             v

          +-------------+   +-------------+
          |   Sender    |   |  Receiver   |
          |   Browser   |   |   Browser   |
          +-------------+   +-------------+
                 \             /
                  \           /
                   \         /
                    \       /
                WebRTC Data Channel
                       |
                       |
                Direct if possible
                       |
                       v

               File data moves
             device-to-device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When direct connectivity is impossible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sender
  |
  v
TURN relay
  |
  v
Receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important architectural goal remains:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The file does not need to become a permanently stored cloud object just to move between two people.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Closing thoughts
&lt;/h1&gt;

&lt;p&gt;Before building ButterShare, “file sharing” sounded like a solved problem.&lt;/p&gt;

&lt;p&gt;Then I started thinking about 100GB files.&lt;/p&gt;

&lt;p&gt;The moment files get that large, you discover that the problem is not just uploading.&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transport&lt;/li&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;backpressure&lt;/li&gt;
&lt;li&gt;browser limitations&lt;/li&gt;
&lt;li&gt;NAT traversal&lt;/li&gt;
&lt;li&gt;mobile lifecycle&lt;/li&gt;
&lt;li&gt;reconnection&lt;/li&gt;
&lt;li&gt;resume&lt;/li&gt;
&lt;li&gt;integrity&lt;/li&gt;
&lt;li&gt;filesystem writing&lt;/li&gt;
&lt;li&gt;progress reporting&lt;/li&gt;
&lt;li&gt;infrastructure economics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that is what makes the problem interesting.&lt;/p&gt;

&lt;p&gt;There is something satisfying about the simplicity of the final idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You have the file.

They need the file.

Connect the two devices.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else is engineering.&lt;/p&gt;

&lt;p&gt;I'm building this approach into &lt;strong&gt;ButterShare&lt;/strong&gt;, a browser-based large-file transfer tool focused on direct device-to-device transfers without traditional cloud file storage.&lt;/p&gt;

&lt;p&gt;If you work with WebRTC, browser streaming, file APIs, or large-file infrastructure, I'd genuinely be interested in hearing how you would approach the same problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ButterShare:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://buttershare.com/" rel="noopener noreferrer"&gt;https://buttershare.com/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;For very large browser-based transfers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't load the entire file into memory.&lt;/li&gt;
&lt;li&gt;Break files into manageable chunks.&lt;/li&gt;
&lt;li&gt;Respect WebRTC backpressure.&lt;/li&gt;
&lt;li&gt;Keep signaling separate from actual file transfer.&lt;/li&gt;
&lt;li&gt;Use STUN and TURN appropriately.&lt;/li&gt;
&lt;li&gt;Expect mobile browsers to behave differently.&lt;/li&gt;
&lt;li&gt;Design reconnection and resume early.&lt;/li&gt;
&lt;li&gt;Do not accumulate the complete file in receiver memory.&lt;/li&gt;
&lt;li&gt;Track confirmed progress, not just queued bytes.&lt;/li&gt;
&lt;li&gt;Treat file transfer as a protocol, not a loop calling &lt;code&gt;send()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remember that P2P does not mean servers disappear.&lt;/li&gt;
&lt;li&gt;Most importantly: &lt;strong&gt;100GB is not simply a larger version of 1GB.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point changes almost every engineering decision.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>networking</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built ButterShare: Free P2P File Transfer Without Cloud Storage</title>
      <dc:creator>Anirban Roy</dc:creator>
      <pubDate>Tue, 22 Sep 2026 05:57:32 +0000</pubDate>
      <link>https://dev.to/anirban00537/i-built-buttershare-free-p2p-file-transfer-without-cloud-storage-26bo</link>
      <guid>https://dev.to/anirban00537/i-built-buttershare-free-p2p-file-transfer-without-cloud-storage-26bo</guid>
      <description>&lt;p&gt;You have a large video on your phone. You need it on your laptop.&lt;/p&gt;

&lt;p&gt;You don’t need a shared drive, another account, or a permanently stored copy. You just need the file on the other device.&lt;/p&gt;

&lt;p&gt;That’s the problem I’m working on with &lt;a href="https://buttershare.com/" rel="noopener noreferrer"&gt;ButterShare&lt;/a&gt;: a free, browser-based file transfer tool built around peer-to-peer sharing.&lt;/p&gt;

&lt;p&gt;I’m the developer behind it. Here’s what it does, why I chose this approach, and the tradeoffs worth understanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is ButterShare?
&lt;/h2&gt;

&lt;p&gt;ButterShare lets you send files between devices through a web browser, without signing up or installing an app.&lt;/p&gt;

&lt;p&gt;The main features are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser-to-browser file transfers using WebRTC.&lt;/li&gt;
&lt;li&gt;No product-imposed file-size cap.&lt;/li&gt;
&lt;li&gt;No cloud storage of your transferred files.&lt;/li&gt;
&lt;li&gt;Sharing through a link or QR code.&lt;/li&gt;
&lt;li&gt;No compression of the files you send.&lt;/li&gt;
&lt;li&gt;Support for cross-platform workflows using compatible browsers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important distinction: &lt;strong&gt;this is a live transfer, not an upload-and-download-later service. Both devices need to stay connected until the transfer finishes.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to transfer files with ButterShare
&lt;/h2&gt;

&lt;p&gt;The workflow is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://buttershare.com/" rel="noopener noreferrer"&gt;ButterShare&lt;/a&gt; on the sending device.&lt;/li&gt;
&lt;li&gt;Select the files you want to share.&lt;/li&gt;
&lt;li&gt;Share the generated link or let the recipient scan the QR code.&lt;/li&gt;
&lt;li&gt;Open the link on the receiving device and connect.&lt;/li&gt;
&lt;li&gt;Keep both browser tabs open until the transfer completes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You don’t need to create an account for a one-time transfer.&lt;/p&gt;

&lt;p&gt;This is useful when moving photos to another device, sharing a screen recording, or sending an exported video to someone who is available to receive it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use WebRTC for file transfer?
&lt;/h2&gt;

&lt;p&gt;WebRTC is often associated with video calls, but its data channels can carry binary data too.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;RTCDataChannel&lt;/code&gt; lets browsers exchange data over a peer connection. Data channels use DTLS encryption, and their APIs expose buffering information that applications can use to manage outgoing data.&lt;/p&gt;

&lt;p&gt;For developers exploring the underlying technology, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Using_data_channels" rel="noopener noreferrer"&gt;MDN’s guide to WebRTC data channels&lt;/a&gt; is a useful starting point.&lt;/p&gt;

&lt;p&gt;Large-file transfers also involve practical engineering decisions: splitting files into manageable chunks, controlling how much data is queued, and handling the receiving browser’s storage and memory constraints.&lt;/p&gt;

&lt;p&gt;That’s why “no product-imposed file-size cap” shouldn’t be confused with “every browser can handle an arbitrarily large file under any conditions.”&lt;/p&gt;

&lt;h2&gt;
  
  
  No cloud storage does not mean no infrastructure
&lt;/h2&gt;

&lt;p&gt;These are different claims.&lt;/p&gt;

&lt;p&gt;A browser-to-browser application can still need infrastructure to help devices discover and connect to each other. Depending on a WebRTC application's configuration and network conditions, connectivity may also involve relay servers.&lt;/p&gt;

&lt;p&gt;Temporarily relaying encrypted traffic is different from storing a file for someone to download later.&lt;/p&gt;

&lt;p&gt;ButterShare’s file-sharing model is about live delivery without maintaining a cloud-stored copy—not about pretending that networking infrastructure disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  When browser-based file sharing makes sense
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Moving files between different operating systems
&lt;/h3&gt;

&lt;p&gt;If you’re looking for a browser-based alternative to an AirDrop-style workflow, ButterShare is designed for sharing across devices such as Windows PCs, Macs, Android phones, iPhones, and iPads.&lt;/p&gt;

&lt;p&gt;It does not use Apple’s AirDrop protocol. Both devices connect through their browsers instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending videos without compression
&lt;/h3&gt;

&lt;p&gt;Sometimes you need to send the original exported video, not a smaller version.&lt;/p&gt;

&lt;p&gt;ButterShare transfers the selected file without deliberately re-encoding it. That makes it useful when preserving the original file matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing files without creating accounts
&lt;/h3&gt;

&lt;p&gt;A quick handoff shouldn’t always require inviting someone into a shared workspace.&lt;/p&gt;

&lt;p&gt;A link-based session is useful when both people are already online and just need to exchange files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limitations are important
&lt;/h2&gt;

&lt;p&gt;Peer-to-peer transfer is not the right answer for every situation.&lt;/p&gt;

&lt;p&gt;Keep these constraints in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Both devices must remain connected.&lt;/strong&gt; Closing a tab, putting a device to sleep, or losing connectivity can interrupt the transfer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed depends on the connection.&lt;/strong&gt; Your network, the recipient’s network, and the connection path all matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser behavior matters.&lt;/strong&gt; Mobile background restrictions and available memory or storage can affect large transfers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no stored copy waiting for an offline recipient.&lt;/strong&gt; If someone needs to download tomorrow after you disconnect, a cloud-based transfer service is a better fit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don’t want to market this as a replacement for every file-sharing workflow. It solves a specific one: moving files between people or devices that can connect at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it and tell me what breaks
&lt;/h2&gt;

&lt;p&gt;ButterShare’s live file-transfer tool is free to use.&lt;/p&gt;

&lt;p&gt;If you try it, I’d appreciate concrete feedback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which devices and browsers did you use?&lt;/li&gt;
&lt;li&gt;What size and type of file did you transfer?&lt;/li&gt;
&lt;li&gt;Did the connection work on the first attempt?&lt;/li&gt;
&lt;li&gt;Was anything confusing about sending or receiving?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please don’t include private filenames or sharing links in your feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://buttershare.com/" rel="noopener noreferrer"&gt;Try ButterShare →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What’s the most frustrating part of transferring files between your devices?&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>5 Tools for Developers to Grow Their Social Presence</title>
      <dc:creator>Anirban Roy</dc:creator>
      <pubDate>Mon, 12 Feb 2024 05:11:22 +0000</pubDate>
      <link>https://dev.to/anirban00537/5-vital-tools-for-advancing-your-social-media-management-career-992</link>
      <guid>https://dev.to/anirban00537/5-vital-tools-for-advancing-your-social-media-management-career-992</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fei6ucjqcx8swpb2pj5gk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fei6ucjqcx8swpb2pj5gk.jpg" alt="Image description" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having the appropriate too&lt;br&gt;
ls at your disposal may make all the difference in the fast-paced world of social media marketing when it comes to expanding your online presence, interacting with your audience, and producing results. Utilizing the potential of these five vital tools will help you advance your social media management career, regardless of experience level.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://canva.com/"&gt;1. Canva&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Having the appropriate tools at your disposal may make all the difference in the fast-paced world of social media marketing when it comes to expanding your online presence, interacting with your audience, and producing results. Utilizing the potential of these five vital tools will help you advance your social media management career, regardless of experience level.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://buffer.com/"&gt;2. Buffer&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;A social media management application called Buffer makes it easier to plan, publish, and analyze your content on several social media networks. You can maximize your reach and maintain a consistent posting schedule by planning and scheduling your content ahead of time with Buffer. You may optimize your social media strategy by tracking the effectiveness of your postings, analyzing important indicators, and making data-driven decisions using its user-friendly dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.hootsuite.com/"&gt;3. Hootsuite&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Another well-known social media management tool is Hootsuite, which provides an extensive toolkit for planning, keeping track of, and evaluating your social media activities. With Hootsuite, you can automate scheduling to optimize your workflow, monitor discussions and mentions in real-time, and manage various social media accounts from a single dashboard. Regardless of your role in a larger marketing team or as a solopreneur, Hootsuite gives you the tools you need to remain productive, efficient, and organized on social media.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;a href="https://moiful.com"&gt;Moiful&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;For developers and designers who want to quickly and easily make beautiful screenshots and social media photographs, Moiful is an essential tool. Moiful's user-friendly interface and adaptable template options let you present your projects, application interfaces, or code snippets in an eye-catching manner. When it comes to promoting a blog post, revealing a new feature, or reporting a development milestone, Moiful assists you in producing eye-catching graphics that will grab the attention of your audience and improve your online visibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;a href="https://buzzsumo.com/"&gt;BuzzSumo&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;BuzzSumo is a potent tool for social media analytics and content discovery that enables you to track hot topics, evaluate the performance of competitors, and find information to guide your content strategy. BuzzSumo allows you to track the performance of your content in real-time, keep an eye on mentions of your brand or keywords, and find the most shared material across social media platforms. Using BuzzSumo's data-driven insights, you can expand your audience, provide content that is more engaging, and encourage genuine social media interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Having the appropriate tools is crucial for expanding your social media presence, interacting with your audience, and producing results in the cutthroat world of today. These five indispensable tools—Canva, Buffer, Hootsuite, Moiful, and BuzzSumo—can benefit solopreneurs, small business owners, and members of bigger marketing teams alike by streamlining workflows, maximizing strategies, and helping you accomplish social media objectives. Add these resources to your arsenal right now, and you'll see a dramatic increase in your social media presence.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>react</category>
    </item>
  </channel>
</rss>
