<?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: Marius-Florin Cristian</title>
    <description>The latest articles on DEV Community by Marius-Florin Cristian (@mfc_keibisoft).</description>
    <link>https://dev.to/mfc_keibisoft</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%2F3844364%2F3d4c1171-f666-46a9-81b9-157ec5f41c94.jpeg</url>
      <title>DEV Community: Marius-Florin Cristian</title>
      <link>https://dev.to/mfc_keibisoft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mfc_keibisoft"/>
    <language>en</language>
    <item>
      <title>gRPC over QUIC: faster seeks, slower bulk</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Sun, 19 Jul 2026 16:33:05 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/grpc-over-quic-faster-seeks-slower-bulk-3ia6</link>
      <guid>https://dev.to/mfc_keibisoft/grpc-over-quic-faster-seeks-slower-bulk-3ia6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Abstract.&lt;/strong&gt; Notes from testing whether to move KeibiDrop, a post-quantum encrypted peer-to-peer filesystem, from gRPC over TCP to gRPC over QUIC. The product reads remote files on demand, so the goal is to survive an IP change, keep seek latency low, stay correct, then go fast, in that order. Measured on a 12-core macOS laptop, a 4-core Linux VPS, and a real link between a laptop in Barcelona and the VPS at 72 ms round trip. Findings: on macOS the QUIC transport is syscall-bound at about 137 MB/s because the platform has no UDP segmentation offload, and the undocumented &lt;code&gt;sendmsg_x&lt;/code&gt; batching syscall lifts it to about 700 MB/s; a teardown deadlock traced to closing a QUIC stream concurrently with a write, fixed with a stream reset; connection migration moves a live gRPC channel to a new socket, verified with packet counters; on the real WAN a single QUIC stream loses to TCP on bulk transfer by up to 16x under Wi-Fi loss, but a random seek under saturating prefetch lands in 109 ms on QUIC against about 3 seconds on TCP, a 28x difference, because the seek gets its own stream instead of queuing behind the prefetch. The design that follows: the interactive on-demand path over QUIC for seek isolation and migration, bulk on lossy links over TCP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The question, and what we optimize for
&lt;/h2&gt;

&lt;p&gt;KeibiDrop is a peer-to-peer filesystem. You mount a remote peer's files and read them on demand. Open a 40 GB video, seek to the middle, and only the bytes you touch cross the wire. The transport underneath is gRPC over our own encrypted TCP connection.&lt;/p&gt;

&lt;p&gt;TCP has one problem we cannot fix from userspace. When the 5-tuple dies, the connection dies. A laptop that moves from café Wi-Fi onto a phone hotspot changes its IP, and the transfer stops. QUIC identifies a connection by a connection ID rather than the 5-tuple, so it can migrate across an address change with the stream intact. We wanted to know whether gRPC could run over QUIC and keep that property.&lt;/p&gt;

&lt;p&gt;We optimize for the experience of an on-demand filesystem, not for bulk throughput in MB/s. The order of priorities is: survive an IP change, keep seek latency low, stay correct, and then speed. All the numbers below are measured. Where a number surprised us, we looked for the cause before believing it. The hardware is a 12-core macOS laptop, a 4-core Linux VPS, and a real link between a laptop in Barcelona and that VPS at 72 ms round trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting gRPC onto a QUIC stream
&lt;/h2&gt;

&lt;p&gt;gRPC already speaks &lt;code&gt;net.Conn&lt;/code&gt;. In KeibiDrop it runs over our encrypted &lt;code&gt;SecureConn&lt;/code&gt;, which is a &lt;code&gt;net.Conn&lt;/code&gt; wrapping a &lt;code&gt;net.Conn&lt;/code&gt;, so QUIC slots in underneath the same seam. The adapter that makes one QUIC stream look like a &lt;code&gt;net.Conn&lt;/code&gt; is small, because a &lt;code&gt;quic.Stream&lt;/code&gt; is already a reliable, ordered byte stream with no framing to add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;quicConn&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;quic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stream&lt;/span&gt;            &lt;span class="c"&gt;// Read/Write/deadlines come from the stream&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;quic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Conn&lt;/span&gt;         &lt;span class="c"&gt;// for the two address methods the stream lacks&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;quicConn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;LocalAddr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Addr&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalAddr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;quicConn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;RemoteAddr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Addr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoteAddr&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;One detail is worth stating now, because it returns in the deadlock section. &lt;code&gt;quic.Stream.Close()&lt;/code&gt; is a graceful, send-only half-close, and quic-go documents that it must not be called concurrently with Write. A &lt;code&gt;net.Conn.Close&lt;/code&gt; has to tear the whole connection down. The correct call is &lt;code&gt;CancelWrite&lt;/code&gt;, a stream reset, not &lt;code&gt;Close&lt;/code&gt;. We used &lt;code&gt;Close&lt;/code&gt; first and paid for it later.&lt;/p&gt;

&lt;p&gt;With the adapter, a listener that hands one stream up as a conn, and gRPC's &lt;code&gt;NewClient&lt;/code&gt; pointed at a &lt;code&gt;passthrough:///&lt;/code&gt; target so it skips DNS, a gRPC call completes over QUIC. Unary, server-streaming, and concurrent calls all work, verified byte for byte and clean under the race detector. gRPC's own TLS stays off. QUIC's mandatory TLS 1.3 sits underneath with a throwaway certificate, because the real confidentiality is a post-quantum handshake we add on top (ML-KEM-1024 and X25519, the same primitives KeibiDrop already uses; QUIC's TLS 1.3 is not post-quantum and ours is).&lt;/p&gt;

&lt;h2&gt;
  
  
  Throughput on macOS
&lt;/h2&gt;

&lt;p&gt;The first benchmark on the macOS laptop streamed a large file both ways on loopback:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;transport&lt;/th&gt;
&lt;th&gt;throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gRPC over TCP&lt;/td&gt;
&lt;td&gt;1650 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gRPC over QUIC&lt;/td&gt;
&lt;td&gt;140 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Twelve times slower. That is large enough to want an explanation, so we profiled it. A raw QUIC stream with no gRPC gave 137 MB/s, so the gRPC layer was not the cause; the QUIC transport was. The CPU profile showed where the time went:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;56.67%  syscall.rawsyscalln          # the syscall itself
33.33%  ...sendmsg   (one WriteMsgUDP per ~1400-byte packet)
23.44%  ...recvmsg
 0.6 %  crypto (AES-GCM)             # negligible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifty-seven percent of the time was in &lt;code&gt;sendmsg&lt;/code&gt; and &lt;code&gt;recvmsg&lt;/code&gt;. Crypto, which people assume is the cost of an encrypted transport, was 0.6 percent. The reason is that macOS has no UDP Generic Segmentation Offload. On Linux, quic-go hands the kernel a 64 KB buffer and the kernel splits it into packets, so one syscall sends dozens. On macOS there is no such offload, so quic-go sends one &lt;code&gt;sendmsg&lt;/code&gt; per 1400-byte packet. At 137 MB/s that is about 100,000 send syscalls per second and as many receives.&lt;/p&gt;

&lt;p&gt;We checked this by running the same code on the Linux VPS, which has GSO. The VPS is the slower machine (its loopback TCP is 3.4x slower than the laptop's), and its QUIC still ran at 272 MB/s against the laptop's 137. Normalized against each machine's own TCP, QUIC went from 2 percent on macOS to 14 percent on Linux. So the macOS number is a platform limitation, not a design or crypto cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batching sends with sendmsg_x
&lt;/h2&gt;

&lt;p&gt;macOS has no GSO and no &lt;code&gt;sendmmsg&lt;/code&gt;. The syscall-batching primitives that fix this on Linux do not exist here. It does have &lt;code&gt;sendmsg_x&lt;/code&gt;, a Darwin syscall that sends many datagrams in one call, similar to Linux &lt;code&gt;sendmmsg&lt;/code&gt;. It is undocumented. The syscall number is in the SDK (&lt;code&gt;SYS_sendmsg_x = 481&lt;/code&gt;), but the &lt;code&gt;struct msghdr_x&lt;/code&gt; it takes is not in any header. You declare it yourself from the XNU source and get the byte layout right, or the kernel reads garbage.&lt;/p&gt;

&lt;p&gt;We wrote a small program that prints the struct offsets, checks them with a real batched send the receiver verifies, and benchmarks batch size against raw UDP send throughput:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;batch&lt;/th&gt;
&lt;th&gt;syscalls&lt;/th&gt;
&lt;th&gt;throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 (one per packet)&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;390 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;62,500&lt;/td&gt;
&lt;td&gt;675 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;15,625&lt;/td&gt;
&lt;td&gt;701 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Batching lifts the ceiling from 390 to about 700 MB/s and cuts the syscall count by 32 times. It levels off at batch 32, where the cost moves from syscall count to per-packet kernel work. So there is a real macOS lever here, about 1.8 times, with no root access. It helps in a second way. The 137 MB/s QUIC transfer used about five CPU cores, all of it the per-packet work TCP hands to the kernel, and in KeibiDrop that CPU competes with the FUSE filesystem, which is busy with encryption and chunking. Fewer syscalls per byte means fewer cores taken from the filesystem.&lt;/p&gt;

&lt;p&gt;The primitive is now in our quic-go fork as the Darwin counterpart of the Linux GSO path, with the verified struct layout, ready to wire into the send loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The teardown deadlock
&lt;/h2&gt;

&lt;p&gt;The test suite passed under the race detector and hung about half the time without it. That combination points at a timing-dependent bug rather than a random flake, because the race detector slows the code down. The goroutine dump named the test, a client cancelling a server-stream, and showed &lt;code&gt;Server.Stop&lt;/code&gt; blocked while a QUIC write stayed stuck.&lt;/p&gt;

&lt;p&gt;Two guesses were wrong, and both were quick to disprove by measuring. Sizing the download to fit the flow-control window did not help; it still wedged. A short idle timeout had no effect, because a connection that is actively trying to send is not idle. Reading the source settled it.&lt;/p&gt;

&lt;p&gt;gRPC's &lt;code&gt;http2Server.Close&lt;/code&gt; closes the underlying &lt;code&gt;net.Conn&lt;/code&gt;, which is our &lt;code&gt;quicConn.Close&lt;/code&gt;, which called &lt;code&gt;quic.Stream.Close()&lt;/code&gt;, the graceful half-close from the adapter section, the one quic-go says must not be called concurrently with Write. gRPC's writer goroutine was concurrently blocked in a Write on that stream. The contract violation meant the write never returned, the conn never finished closing, and &lt;code&gt;Server.Stop&lt;/code&gt; waited forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;quicConn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CancelRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;// STOP_SENDING&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CancelWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// RESET_STREAM: safe against a concurrent Write, and unblocks it&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CloseWithError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&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;Replacing &lt;code&gt;Close&lt;/code&gt; with &lt;code&gt;CancelWrite&lt;/code&gt; turned a 50 percent hang into ten green runs out of ten. The general point: when the race detector flips a test from pass to fail, look for the timing bug rather than adding sleeps. And a lightweight atomic counter is better than a print for diagnosing one, because the print changes the timing you are measuring. Ours made the test pass for a while and pointed us the wrong way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration
&lt;/h2&gt;

&lt;p&gt;Migration is the reason we started. quic-go exposes it directly: &lt;code&gt;AddPath&lt;/code&gt; a new socket, &lt;code&gt;Probe&lt;/code&gt; to validate the path, &lt;code&gt;Switch&lt;/code&gt;. Getting it to fire took reading quic-go's own test. The client has to keep sending after the switch so the peer migrates too, and you must not close the old transport to prove it, because that tears the connection down.&lt;/p&gt;

&lt;p&gt;We checked it with per-socket packet counters, moving a live gRPC channel to a new UDP socket mid-transfer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;migration: local addr 127.0.0.1:52598 -&amp;gt; 127.0.0.1:59126
path 1 writes +0        # old path goes silent
path 2 writes +90, reads +92   # new path carries both directions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the switch the old path sent nothing and the new path carried both directions. The gRPC channel kept running. On the real product this is a laptop changing networks during playback without a stall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real WAN test
&lt;/h2&gt;

&lt;p&gt;Loopback measures a transport's overhead, not its behaviour on a real network. So we ran it for real: a laptop on hotel Wi-Fi in Barcelona, about 45 Mbps down and 72 ms round trip, downloading from the VPS. No emulation and no offload tricks, so both TCP and QUIC send ordinary packets end to end.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;concurrent streams&lt;/th&gt;
&lt;th&gt;QUIC&lt;/th&gt;
&lt;th&gt;TCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;32.8 s&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5.5 s&lt;/td&gt;
&lt;td&gt;1.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;4.7 s&lt;/td&gt;
&lt;td&gt;1.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A single QUIC stream dropped to about 2 Mbps of a 45 Mbps link while TCP filled it. The cause is real Wi-Fi packet loss and quic-go's congestion control, which backs off hard, while Linux kernel TCP handles the same loss well. We had gone in half-expecting the common claim that QUIC does better on lossy links. On a real lossy link the opposite held. For plain bulk transfer on a bad link, TCP won.&lt;/p&gt;

&lt;p&gt;On its own this result argues against QUIC. We nearly stopped here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seek latency under prefetch
&lt;/h2&gt;

&lt;p&gt;An on-demand filesystem is not used by streaming a file end to end. It is used by seeking. You open a video, jump to a position, jump again. Each jump is a small read at a random offset while a prefetch pulls bulk data in the background. The number that matters is how long that small read takes while the prefetch saturates the link.&lt;/p&gt;

&lt;p&gt;On TCP, the prefetch and the seek share one byte stream, so the seek's bytes sit behind the prefetch already queued in the send buffer. That is head-of-line blocking, and it is the playback stutter we had seen before as self-congestion. On QUIC the seek gets its own stream, and the library interleaves its packets with the bulk, so it does not queue behind it.&lt;/p&gt;

&lt;p&gt;We measured it on the same Barcelona link. Thirty small 16 KB reads under a saturating bulk stream:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;p99&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;QUIC (seek on its own stream)&lt;/td&gt;
&lt;td&gt;109 ms&lt;/td&gt;
&lt;td&gt;322 ms&lt;/td&gt;
&lt;td&gt;387 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TCP (seek behind prefetch)&lt;/td&gt;
&lt;td&gt;3066 ms&lt;/td&gt;
&lt;td&gt;4827 ms&lt;/td&gt;
&lt;td&gt;4924 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A seek lands in about 109 ms on QUIC, roughly one round trip. On TCP it takes about three seconds, behind the prefetch. For someone scrubbing through a video the first is usable and the second is not, and the gap is larger than the bulk-throughput gap from the previous section.&lt;/p&gt;

&lt;p&gt;This fits the earlier results together. QUIC's advantage for us is latency isolation, a seek that does not wait behind a prefetch, together with migration, a transfer that survives a network change. For an on-demand seekable filesystem that trade is worth it. TCP can match the fast lane only with a second connection per read, which is the data-channels idea we had been circling; QUIC gives it per stream, with shared congestion control.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we build
&lt;/h2&gt;

&lt;p&gt;The measurements point at a specific design. The interactive on-demand path runs over QUIC. Seeks and prefetch on separate streams is where the seek result comes from, and migration comes with it. Plain bulk on a lossy link stays on TCP, where quic-go's congestion control loses. On fast, clean links the macOS syscall ceiling is real, and &lt;code&gt;sendmsg_x&lt;/code&gt; batching is the lever that lifts QUIC to about 700 MB/s while freeing CPU for the filesystem. Control traffic such as heartbeats and metadata rides QUIC, since it is small and it is the channel that has to survive a network change.&lt;/p&gt;

&lt;p&gt;None of this was obvious at the start, and several steps were the opposite of what we expected. Measuring, rather than reasoning from what QUIC is supposed to be good at, changed the answer twice: once when the slow macOS transport turned out to be a syscall cost with a fix, and once when the throughput test's verdict flipped as soon as we measured seek latency.&lt;/p&gt;

&lt;p&gt;The experiments are small and self-contained: the gRPC-over-QUIC adapter, the &lt;code&gt;sendmsg_x&lt;/code&gt; program, and the two WAN benchmarks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Measured on a 12-core macOS laptop, a 4-core Linux VPS, and a real link between a laptop in Barcelona and the VPS at 72 ms round trip. Originally published at &lt;a href="https://keibisoft.com/blog/grpc-over-quic-seek-latency.html" rel="noopener noreferrer"&gt;keibisoft.com/blog/grpc-over-quic-seek-latency.html&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>filesystems</category>
      <category>go</category>
      <category>performance</category>
      <category>networking</category>
    </item>
    <item>
      <title>KEIBIDROP: Instant file transfer Mac to Windows</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Sun, 28 Jun 2026 09:20:32 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/keibidrop-instant-file-transfer-mac-to-windows-3a1e</link>
      <guid>https://dev.to/mfc_keibisoft/keibidrop-instant-file-transfer-mac-to-windows-3a1e</guid>
      <description>&lt;p&gt;(Left side macos, rightside windows)&lt;/p&gt;

&lt;p&gt;Work on shared files instantly.&lt;br&gt;
No more waiting for uploads or downloads.&lt;/p&gt;

&lt;p&gt;Open a 40 GB file and start working right away.&lt;br&gt;
Your edits, available instantly.&lt;/p&gt;

&lt;p&gt;This being said, this post is short, and its about the video :D&lt;/p&gt;

</description>
      <category>keibidrop</category>
      <category>go</category>
      <category>cryptography</category>
      <category>filesystems</category>
    </item>
    <item>
      <title>KEIBIDROP: Romania to Singapore Benchmarks</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Sun, 14 Jun 2026 08:57:37 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/keibidrop-romania-to-singapore-benchmarks-5cc4</link>
      <guid>https://dev.to/mfc_keibisoft/keibidrop-romania-to-singapore-benchmarks-5cc4</guid>
      <description>&lt;p&gt;Measuring an encrypted peer-to-peer filesystem over a 200 to 330 ms intercontinental link to a Windows host: throughput, connect time, and FUSE on-demand latency, and what bounds them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Abstract.&lt;/strong&gt; Measurement of KeibiDrop, a post-quantum encrypted peer-to-peer filesystem, over a high-latency intercontinental link. Two Linux peers in Romania run against one Windows Server peer in Singapore, at 200 ms (datacenter to datacenter) and 330 ms (residential) round-trip. The Windows host is IPv4 only, so all traffic traverses the TCP bridge. Three dimensions are reported: bulk throughput, connect time, and FUSE on-demand read latency. Each cell is the median over repeated trials, with the per-table trial count n shown in the tables, all peers on commit &lt;a href="https://github.com/KeibiSoft/KeibiDrop/commit/b5a7a3e2b38efd29a963a1abf6fe1c827afec493" rel="noopener noreferrer"&gt;b5a7a3e&lt;/a&gt;. Findings: 30 to 38 MB/s into the Windows host and 13 to 25 MB/s out; connect time about 6.3 s, dominated by an IPv6 dial timeout before bridge fallback; FUSE first byte under 1.5 s with sequential on-demand read near 1 MB/s. The throughput ceiling is the bandwidth-delay product, not link capacity or CPU. FUSE mode did not start on the Windows host on this build; a fix is in review.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Three machines, two pairings. One end is always the Singapore box.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;machine&lt;/th&gt;
&lt;th&gt;details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Singapore, Windows Server, 4 vCPU / 8 GB, WinFsp installed. IPv4 only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Timisoara, Linux. Also runs the relay and the TCP bridge.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;laptop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Iasi, Linux, residential link about 500 to 600 Mbps wired (62 to 75 MB/s).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pairing A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;VPS to SG, about 200 ms RTT, datacenter to datacenter.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pairing B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;laptop to SG, about 330 ms RTT, home to datacenter.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Method:&lt;/strong&gt; each cell is the median across repeated trials, with the trial count n shown in every table: 5 at 10 and 100 MB, 3 at 1 GB, 1 at 5 and 10 GB for bulk; 10 for handshake; 3 for FUSE. All three peers run the same commit. &lt;code&gt;X to Y&lt;/code&gt; means X sends and Y receives. Bulk transfer uses &lt;code&gt;kd add&lt;/code&gt; then a timed pull, no FUSE in the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bulk throughput
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pairing A, VPS to SG, about 200 ms&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transfer&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Median MB/s&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SG to VPS&lt;/td&gt;
&lt;td&gt;10 MB&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;25.6&lt;/td&gt;
&lt;td&gt;28.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPS to SG&lt;/td&gt;
&lt;td&gt;100 MB&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;35.5&lt;/td&gt;
&lt;td&gt;41.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPS to SG&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;37.6&lt;/td&gt;
&lt;td&gt;40.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SG to VPS&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;13.2&lt;/td&gt;
&lt;td&gt;13.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPS to SG&lt;/td&gt;
&lt;td&gt;5 GB&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;34.6&lt;/td&gt;
&lt;td&gt;34.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SG to VPS&lt;/td&gt;
&lt;td&gt;5 GB&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPS to SG&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;18.8&lt;/td&gt;
&lt;td&gt;18.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SG to VPS&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;12.9&lt;/td&gt;
&lt;td&gt;12.9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pairing B, laptop to SG, about 330 ms&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transfer&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Median MB/s&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SG to laptop&lt;/td&gt;
&lt;td&gt;10 MB&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;20.4&lt;/td&gt;
&lt;td&gt;21.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;laptop to SG&lt;/td&gt;
&lt;td&gt;100 MB&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;29.6&lt;/td&gt;
&lt;td&gt;30.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;laptop to SG&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;29.0&lt;/td&gt;
&lt;td&gt;29.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SG to laptop&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;13.4&lt;/td&gt;
&lt;td&gt;15.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;laptop to SG&lt;/td&gt;
&lt;td&gt;5 GB&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;32.9&lt;/td&gt;
&lt;td&gt;32.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;laptop to SG&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;31.3&lt;/td&gt;
&lt;td&gt;31.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Handshake
&lt;/h2&gt;

&lt;p&gt;Time to connect to the Singapore box, parsed from the joiner's own log (relay lookup to connected).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Connects to SG from&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Median ms&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;min&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VPS (pairing A)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;6316&lt;/td&gt;
&lt;td&gt;6473&lt;/td&gt;
&lt;td&gt;6094&lt;/td&gt;
&lt;td&gt;6476&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;laptop (pairing B)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;6220&lt;/td&gt;
&lt;td&gt;6481&lt;/td&gt;
&lt;td&gt;5977&lt;/td&gt;
&lt;td&gt;6507&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FUSE on-demand
&lt;/h2&gt;

&lt;p&gt;A Linux peer mounts the virtual drive and reads a file hosted on SG. Content streams on demand over the link. &lt;code&gt;ttfb&lt;/code&gt; is the time to the first byte (the first 512 KB chunk). The mount is Linux because the Windows mount has a bug, covered below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mount reads from SG&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;ttfb median ms&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;read MB/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VPS mount (pairing A)&lt;/td&gt;
&lt;td&gt;10 MB&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;632&lt;/td&gt;
&lt;td&gt;1242&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPS mount (pairing A)&lt;/td&gt;
&lt;td&gt;100 MB&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;509&lt;/td&gt;
&lt;td&gt;635&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;laptop mount (pairing B)&lt;/td&gt;
&lt;td&gt;10 MB&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;709&lt;/td&gt;
&lt;td&gt;1423&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;laptop mount (pairing B)&lt;/td&gt;
&lt;td&gt;100 MB&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;482&lt;/td&gt;
&lt;td&gt;549&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;p&gt;The limit is round-trip time, not bandwidth and not CPU. The home link is 500 to 600 Mbps, which is 62 to 75 MB/s, and the best run used about half of it. The box is a 4 vCPU machine that stayed near idle, and ChaCha20 runs at gigabytes per second per core, so encryption is not the wall either. The wall is the bandwidth-delay product: one gRPC stream with a 16 MiB window cannot keep a 330 ms pipe full, because filling it needs roughly 18 MB in flight at the home link rate. A bigger window or parallel pull streams would close that gap.&lt;/p&gt;

&lt;p&gt;The Singapore box sends at about half the rate it receives. At 1 GB on pairing A it pulls in 37.6 MB/s but pushes out 13.2 MB/s, and the same shape holds across sizes and on pairing B. The receive path is fine. The send path is where the work is.&lt;/p&gt;

&lt;p&gt;The 6.3 second connect is one timeout. The box has no IPv6, so the direct IPv6 dial always fails and waits out its deadline before the bridge takes over. Skipping the direct dial when a peer advertises no IPv6 removes most of that. The same change helps phones, which are usually IPv4 only and hit the same wait.&lt;/p&gt;

&lt;p&gt;On-demand FUSE is for opening and seeking, not for streaming a whole large file. The first byte comes back in half a second to a second and a half, which is fine for opening a document, scrubbing a video, or reading part of a dataset. Sequential read settles near 1 MB/s because each 512 KB chunk costs one to three round trips at this latency. For a whole large file, a bulk pull at 30 MB/s or a prefetch is the right path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defects identified
&lt;/h2&gt;

&lt;p&gt;FUSE mode did not start on the Windows host on this build. &lt;code&gt;host.Mount&lt;/code&gt; returned false and the daemon fell back to no-FUSE: &lt;code&gt;kd list&lt;/code&gt; showed the files, but the mount point stayed an empty folder and neither local nor remote files surfaced. The cause is the mount point being created before WinFsp runs. WinFsp takes the mount point as a drive letter (for example &lt;code&gt;K:&lt;/code&gt;), an auto-assigned drive (&lt;code&gt;*&lt;/code&gt;), or a directory it creates itself, and it rejects a path that already exists. &lt;a href="https://github.com/KeibiSoft/KeibiDrop/pull/181" rel="noopener noreferrer"&gt;PR #181&lt;/a&gt; removes the pre-creation on Windows. No-FUSE transfer is unaffected, which is why the FUSE tables above use Linux mounts.&lt;/p&gt;

&lt;p&gt;One 10 GB transfer looked like it stalled at 12 MB. A re-run moved 3.7 GB in 4 minutes at a steady 15.6 MB/s, so it was a blip on the residential link, not a defect. Not filed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planned changes
&lt;/h2&gt;

&lt;p&gt;Skip the direct dial when a peer advertises no IPv6. This removes most of the 6.3 s connect time on the common case and applies to mobile, which is also IPv4 only.&lt;/p&gt;

&lt;p&gt;Land the Windows mount fix (&lt;a href="https://github.com/KeibiSoft/KeibiDrop/pull/181" rel="noopener noreferrer"&gt;PR #181&lt;/a&gt;) so FUSE starts on the Windows host and surfaces files.&lt;/p&gt;

&lt;p&gt;Add a push-based send to remove the asymmetry where the host sends at half its receive rate.&lt;/p&gt;

&lt;p&gt;For bulk throughput, raise the window or pull with several streams. Tradeoff: saturating the link degrades on-demand read latency, so this stays opt-in for bulk transfers rather than the default for the interactive mount.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Medians over the per-table trial counts (n), all peers on commit &lt;a href="https://github.com/KeibiSoft/KeibiDrop/commit/b5a7a3e2b38efd29a963a1abf6fe1c827afec493" rel="noopener noreferrer"&gt;b5a7a3e&lt;/a&gt;. Pairing A is VPS to SG at about 200 ms. Pairing B is laptop to SG at about 330 ms. All traffic over the TCP bridge.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>filesystems</category>
      <category>network</category>
      <category>keibidrop</category>
      <category>performance</category>
    </item>
    <item>
      <title>KEIBIDROP: Also on r/selfhosted</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Mon, 01 Jun 2026 19:20:41 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/keibidrop-also-on-rselfhosted-2jm0</link>
      <guid>https://dev.to/mfc_keibisoft/keibidrop-also-on-rselfhosted-2jm0</guid>
      <description>&lt;p&gt;I made an open source tool that allows two people to directly connect via WAN, or LAN, and mount a virtual mountpoint.&lt;/p&gt;

&lt;p&gt;(The first video in link URL shows side by side my, and my mac and brothers ubuntu use the shared mounted filesystem)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/KeibiSoft/KeibiDrop" rel="noopener noreferrer"&gt;https://github.com/KeibiSoft/KeibiDrop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea is that two people establish a connection, and mount virtual folders. Metadata is synced automatically when things are dragged in the virtual folder, and it supports reading with offset, which means that if Alice shares a 10GB video, or folder with files, Bob can see it in a few seconds, and start playing it.&lt;br&gt;
(Here is a fresher video where I add a Popeye 600 MB Video on my iphone, and start playing it with QuickTime and skim through it before the download completes)&lt;br&gt;
&lt;a href="https://youtu.be/UHw1WtkZ8dE" rel="noopener noreferrer"&gt;https://youtu.be/UHw1WtkZ8dE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tool has been in development for a little over a year, and just made it public last month.&lt;/p&gt;

&lt;p&gt;There are a series of technical blog posts about the pain points, optimization paths, and how we set benchmarks in order to make work fast at&lt;br&gt;
&lt;a href="https://keibisoft.com/blog.html#keibidrop" rel="noopener noreferrer"&gt;https://keibisoft.com/blog.html#keibidrop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most notable thing is that once Alice and Bob are connected they can use programs as if the filesystems were native. (thus Alice and git clone a repo, and bob git commit, or checkout instantly on the same repo).&lt;/p&gt;

&lt;p&gt;It is still in development, and as me and my brother use it and polish it we also fix any issues that arrise.&lt;/p&gt;

&lt;p&gt;Happy to discuss about it, and things that I learned while grinding on the problem :D&lt;/p&gt;

</description>
      <category>networking</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>KEIBIDROP: Every tool just works</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Mon, 01 Jun 2026 03:48:28 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/keibidrop-every-tool-just-works-1fia</link>
      <guid>https://dev.to/mfc_keibisoft/keibidrop-every-tool-just-works-1fia</guid>
      <description>&lt;p&gt;We connected two machines with &lt;strong&gt;KEIBI&lt;/strong&gt;DROP and ran every program we could think of on the shared virtual folder. Compilers, version control, video processing, databases, scripting languages, security scanners. Everything worked. File integrity held across every operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KEIBI&lt;/strong&gt;DROP presents the peer's files through the operating system's filesystem interface. Programs do not know the files are remote. They read and write to a folder. The data arrives from the other machine over an encrypted channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the shared folder works between two machines
&lt;/h2&gt;

&lt;p&gt;Machine A adds files to the shared folder. They appear on Machine B within a second. Machine B opens them, compiles them, processes them, and saves the output back to the shared folder. Machine A sees the output immediately. No upload, no download, no sync button. Both machines read and write to the same folder in real time, over an encrypted peer-to-peer connection.&lt;/p&gt;

&lt;p&gt;We tested this with 45+ programs. Alice writes source code, Bob compiles it. Bob writes analysis results, Alice reads them. Alice has a video, Bob creates thumbnails from it. Every tool that reads files from a folder works. The programs do not know the files are on another machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;git&lt;/td&gt;
&lt;td&gt;Clone repo, run git status and git log from the other side&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;make (C)&lt;/td&gt;
&lt;td&gt;Compile 87 source files inside the shared folder&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cargo (Rust)&lt;/td&gt;
&lt;td&gt;cargo build --release on the shared folder&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;go build&lt;/td&gt;
&lt;td&gt;Init module, build binary on the shared folder&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;swiftc&lt;/td&gt;
&lt;td&gt;Compile Swift program on the shared folder&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xelatex&lt;/td&gt;
&lt;td&gt;Compile LaTeX slides across mounts&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Media tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ffprobe&lt;/td&gt;
&lt;td&gt;Inspect 192 MB video from peer's folder&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg&lt;/td&gt;
&lt;td&gt;Extract clip, create thumbnail grid from peer's video&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ImageMagick&lt;/td&gt;
&lt;td&gt;Read PNG, resize, identify 2048x5568 tileset&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Data and databases
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;Create DB on one side, JOIN query from the other&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite (multi-table)&lt;/td&gt;
&lt;td&gt;RPG save with players + inventory, JOIN from peer&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jq&lt;/td&gt;
&lt;td&gt;Write JSON on one side, parse from the other&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python csv&lt;/td&gt;
&lt;td&gt;Write CSV, read with DictReader from peer&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;Write file on one side, read from the other&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jupyter&lt;/td&gt;
&lt;td&gt;Write .ipynb on one side, parse from the other&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Languages
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Write module on Alice, import from Bob&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perl&lt;/td&gt;
&lt;td&gt;SHA256 hash with Digest::SHA across mounts&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;JSON + Digest::MD5 across mounts&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;npm install on one side, require from the other&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bash&lt;/td&gt;
&lt;td&gt;Write script on one side, execute from the other&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swift&lt;/td&gt;
&lt;td&gt;Compile and run across mounts&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  File operations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;zip / unzip&lt;/td&gt;
&lt;td&gt;Zip repo on one side, unzip on the other&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tar / gzip&lt;/td&gt;
&lt;td&gt;Create archive, extract on the other side&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rsync --checksum&lt;/td&gt;
&lt;td&gt;Checksummed copy from shared folder&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dd&lt;/td&gt;
&lt;td&gt;1 MB + 100 MB random data, SHA256 verified both sides&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;split + cat&lt;/td&gt;
&lt;td&gt;Split file on one side, reassemble on the other&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;base64&lt;/td&gt;
&lt;td&gt;Encode on one side, decode on the other, hash match&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Integrity verification
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sha256&lt;/td&gt;
&lt;td&gt;Same file hashed from both sides&lt;/td&gt;
&lt;td&gt;Identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sha512&lt;/td&gt;
&lt;td&gt;Same file hashed from both sides&lt;/td&gt;
&lt;td&gt;Identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openssl dgst&lt;/td&gt;
&lt;td&gt;SHA256 digest from both sides&lt;/td&gt;
&lt;td&gt;Identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;md5 (bulk)&lt;/td&gt;
&lt;td&gt;All 71 .c files hashed, aggregate compared&lt;/td&gt;
&lt;td&gt;Identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;diff&lt;/td&gt;
&lt;td&gt;Source files compared across mounts&lt;/td&gt;
&lt;td&gt;Identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crc32&lt;/td&gt;
&lt;td&gt;CRC32 checksum from both sides&lt;/td&gt;
&lt;td&gt;Identical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Security tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;golangci-lint&lt;/td&gt;
&lt;td&gt;Lint Go source from peer's mount&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gosec&lt;/td&gt;
&lt;td&gt;Security scan from peer's mount&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openssl enc/dec&lt;/td&gt;
&lt;td&gt;AES-256-CBC encrypt on one side, decrypt from the other&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exiftool&lt;/td&gt;
&lt;td&gt;Read image metadata from peer's file&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Collaborative workflows
&lt;/h2&gt;

&lt;p&gt;The shared folder is bidirectional. Both sides can write. Two people can each handle a different step of the same project.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;Alice does&lt;/th&gt;
&lt;th&gt;Bob does&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;C compilation&lt;/td&gt;
&lt;td&gt;Writes main.c + headers&lt;/td&gt;
&lt;td&gt;Compiles from his view&lt;/td&gt;
&lt;td&gt;Alice runs the binary Bob built&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LaTeX slides&lt;/td&gt;
&lt;td&gt;Writes .tex source&lt;/td&gt;
&lt;td&gt;Compiles PDF with xelatex&lt;/td&gt;
&lt;td&gt;Alice opens the PDF Bob produced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data pipeline&lt;/td&gt;
&lt;td&gt;Writes 100-row CSV&lt;/td&gt;
&lt;td&gt;Runs Python analysis, writes JSON&lt;/td&gt;
&lt;td&gt;Alice reads Bob's results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video processing&lt;/td&gt;
&lt;td&gt;Has a 190 MB video&lt;/td&gt;
&lt;td&gt;Creates thumbnail grid with ffmpeg&lt;/td&gt;
&lt;td&gt;Alice sees the thumbnails&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Large files
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100 MB random file, SHA256 from both sides&lt;/td&gt;
&lt;td&gt;100 MB&lt;/td&gt;
&lt;td&gt;Identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192 MB video, ffprobe from peer's mount&lt;/td&gt;
&lt;td&gt;192 MB&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg transcode: Bob processes Alice's video, output appears on Alice&lt;/td&gt;
&lt;td&gt;192 MB in, 153 KB out&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 files written by Alice, aggregate hash verified by Bob&lt;/td&gt;
&lt;td&gt;50 files&lt;/td&gt;
&lt;td&gt;Identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;71 source files tar'd, extracted on other side, all hashes verified&lt;/td&gt;
&lt;td&gt;71 files&lt;/td&gt;
&lt;td&gt;All match&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What does not work
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Symlinks / hardlinks&lt;/td&gt;
&lt;td&gt;Not implemented. Tools like uv, npm with bin links fail. Use --no-bin-links or install outside the shared folder.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execute binaries from mount (macOS)&lt;/td&gt;
&lt;td&gt;macOS blocks execution from FUSE mounts. Copy the binary locally first. Linux does not have this restriction.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;KEIBI&lt;/strong&gt;DROP presents the peer's files through the OS filesystem interface (FUSE on macOS/Linux, WinFsp on Windows). Every program that reads files through standard system calls works without modification. The file metadata syncs when the connection establishes. File contents stream on demand as programs read them. The encryption overhead is under 2%.&lt;/p&gt;

&lt;p&gt;The connection is end-to-end encrypted with post-quantum cryptography (ML-KEM-1024 + X25519). No server stores your files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install: &lt;a href="https://keibidrop.com/install.html" rel="noopener noreferrer"&gt;keibidrop.com/install&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/KeibiSoft/KeibiDrop" rel="noopener noreferrer"&gt;github.com/KeibiSoft/KeibiDrop&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>linux</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>KeibiDrop: Instant share 600 MB video from phone to Destkop</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Sat, 30 May 2026 18:07:51 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/keibidrop-instant-share-600-mb-video-from-phone-to-destkop-3i3b</link>
      <guid>https://dev.to/mfc_keibisoft/keibidrop-instant-share-600-mb-video-from-phone-to-destkop-3i3b</guid>
      <description>&lt;p&gt;Here I add a 600MB Popeye video from my iPhone and can open it instantly on my desktop and jump through it, while the downloads happen in the background.&lt;/p&gt;

&lt;p&gt;It works with all types of files and programs, including git clone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://keibidrop.com" rel="noopener noreferrer"&gt;https://keibidrop.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and on &lt;a href="https://github.com/KeibiSoft/KeibiDrop/" rel="noopener noreferrer"&gt;github:&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Mobile apps coming soon!</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Sat, 09 May 2026 15:41:01 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/mobile-apps-coming-soon-3gf0</link>
      <guid>https://dev.to/mfc_keibisoft/mobile-apps-coming-soon-3gf0</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/mfc_keibisoft/keibidrop-secure-file-transfer-132n" class="crayons-story__hidden-navigation-link"&gt;KEIBIDROP: Secure File Transfer!&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/mfc_keibisoft" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3844364%2F3d4c1171-f666-46a9-81b9-157ec5f41c94.jpeg" alt="mfc_keibisoft profile" class="crayons-avatar__image" width="800" height="799"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/mfc_keibisoft" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Marius-Florin Cristian
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Marius-Florin Cristian
                
              
              &lt;div id="story-author-preview-content-3558201" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/mfc_keibisoft" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3844364%2F3d4c1171-f666-46a9-81b9-157ec5f41c94.jpeg" class="crayons-avatar__image" alt="" width="800" height="799"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Marius-Florin Cristian&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/mfc_keibisoft/keibidrop-secure-file-transfer-132n" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 27&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/mfc_keibisoft/keibidrop-secure-file-transfer-132n" id="article-link-3558201"&gt;
          KEIBIDROP: Secure File Transfer!
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/go"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;go&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/filesystem"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;filesystem&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/rust"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;rust&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/mfc_keibisoft/keibidrop-secure-file-transfer-132n#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            1 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>KEIBIDROP: Secure File Transfer!</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Mon, 27 Apr 2026 15:30:09 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/keibidrop-secure-file-transfer-132n</link>
      <guid>https://dev.to/mfc_keibisoft/keibidrop-secure-file-transfer-132n</guid>
      <description>&lt;p&gt;&lt;strong&gt;KEIBI&lt;/strong&gt;DROP encrypted file transfer and shared filesystem between two devices&lt;/p&gt;

&lt;p&gt;&lt;a href="https://keibidrop.com" rel="noopener noreferrer"&gt;https://keibidrop.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;KeibiDrop connects two devices directly and lets them share files over an encrypted channel. It works over the internet or a local network.&lt;/p&gt;

&lt;p&gt;Two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct transfer&lt;/strong&gt; - drag files in, your peer saves what they need&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual folder (FUSE)&lt;/strong&gt; - your peer's files appear as a real folder on your machine. git, rsync, your IDE, all work on it. Files stream on access, so you can open a 4 GB video before it finishes downloading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Post-quantum encryption (ML-KEM-1024 + X25519). The relay, if used, cannot decrypt anything.&lt;/p&gt;

&lt;p&gt;macOS, Windows, Linux. Desktop GUI, interactive CLI, agent CLI for automation.&lt;/p&gt;

&lt;p&gt;Free, open source (MPL-2.0): &lt;a href="https://github.com/KeibiSoft/KeibiDrop" rel="noopener noreferrer"&gt;github.com/KeibiSoft/KeibiDrop&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>security</category>
      <category>filesystem</category>
      <category>rust</category>
    </item>
    <item>
      <title>KEIBIDROP v0.1.0 is out! https://github.com/KeibiSoft/KeibiDrop/releases/tag/v0.1.0

Now you can send files, or mount files in real time as a distributed system :D 
Direct peer to peer or fallback via datarelay.
Open source, and no transfer limits!</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Mon, 27 Apr 2026 09:59:44 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/keibidrop-v010-is-out-httpsgithubcomkeibisoftkeibidropreleasestagv010-now-you-can-3ea8</link>
      <guid>https://dev.to/mfc_keibisoft/keibidrop-v010-is-out-httpsgithubcomkeibisoftkeibidropreleasestagv010-now-you-can-3ea8</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/KeibiSoft/KeibiDrop/releases/tag/v0.1.0" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fopengraph.githubassets.com%2Ff5c68af7c9589f25f7f61e140cf5e75862521b00f5ceb4a6b75ca5da43bc8f2a%2FKeibiSoft%2FKeibiDrop%2Freleases%2Ftag%2Fv0.1.0" height="400" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/KeibiSoft/KeibiDrop/releases/tag/v0.1.0" rel="noopener noreferrer" class="c-link"&gt;
            Release KeibiDrop v0.1.0 · KeibiSoft/KeibiDrop · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            First public release. Peer-to-peer encrypted file sharing across Linux, macOS, and Windows.
What's in this release

End-to-end encrypted transfer (AES-256-GCM / ChaCha20-Poly1305, auto-negotiated b...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg" width="32" height="32"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Can We Compute the Minimum Spanning Tree in Linear Time?</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Wed, 01 Apr 2026 11:43:03 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/can-we-compute-the-minimum-spanning-tree-in-linear-time-5170</link>
      <guid>https://dev.to/mfc_keibisoft/can-we-compute-the-minimum-spanning-tree-in-linear-time-5170</guid>
      <description>&lt;p&gt;&lt;em&gt;Easter egg at the end of how the cover photo is related to the post&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Given a connected graph with weighted edges, the minimum spanning tree (MST) is the cheapest set of edges that keeps all vertices connected. Boruvka gave the first algorithm in 1926. Kruskal, Prim, and others refined it. The best randomized algorithm runs in O(m) expected time (Karger-Klein-Tarjan, 1995). The best deterministic algorithm runs in O(m α(m,n)) (Chazelle, 2000), where α is the inverse Ackermann function.&lt;/p&gt;

&lt;p&gt;α(m,n) ≤ 4 for any graph that could physically exist. In practice, the problem is solved. In theory, the question "can we do O(m) deterministically?" has been open for over forty years.&lt;/p&gt;

&lt;p&gt;This post covers the three attack vectors from &lt;a href="https://keibisoft.com/blog/mst-road-to-linear-complexity.html" rel="noopener noreferrer"&gt;my 2018 thesis at DIKU&lt;/a&gt;, updated with connections to research published since then.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the bottleneck is
&lt;/h2&gt;

&lt;p&gt;Every MST algorithm uses some form of the disjoint-set (union-find) data structure. You process edges, and for each edge you ask: are these two vertices already connected? If not, merge their components.&lt;/p&gt;

&lt;p&gt;With path compression and union by rank, m operations on n elements cost O(m α(m,n)). That α factor is the bottleneck. It comes from the analysis of path compression on a pointer machine.&lt;/p&gt;

&lt;p&gt;On a Word RAM, Gabow and Tarjan (1983) showed you can do O(m + n) — if you know the union tree structure in advance. You precompute lookup tables for small components and answer FIND queries in O(1).&lt;/p&gt;

&lt;p&gt;The catch: knowing the union tree means knowing the MST, which is what you're computing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two computation models
&lt;/h2&gt;

&lt;p&gt;This matters because the bottleneck lives in the gap between them.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;pointer machine&lt;/strong&gt; can follow pointers, compare values, and create new nodes. No arithmetic on addresses, no random access. This is how most graph algorithms actually work. On this model, the α factor from path compression cannot be removed.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Word RAM&lt;/strong&gt; has O(1) random access and can do arithmetic and bitwise operations on words. This enables table lookups — precomputing answers for small inputs and reading them in constant time.&lt;/p&gt;

&lt;p&gt;The MST gap is a model gap. On a pointer machine, O(m α(m,n)) seems tight. On a Word RAM, O(m + n) is possible if the structure is known. The question is whether you can learn enough structure during computation to exploit the RAM model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack vector 1: Approximate union-find
&lt;/h2&gt;

&lt;p&gt;What if you approximately know the union tree?&lt;/p&gt;

&lt;p&gt;Build a predicted tree T' using some fast heuristic (a few Boruvka phases, random sampling, anything that runs in O(m)). Use Gabow-Tarjan lookup tables based on T'. When a FIND query returns an answer that disagrees with the true structure, pay for a correction.&lt;/p&gt;

&lt;p&gt;If the prediction is close enough to the true MST, most queries hit the lookup tables and cost O(1). The faulty queries create "phantom cycles" — the algorithm temporarily believes two vertices are connected when they aren't, or vice versa. These phantoms are bounded by the prediction error and can be cleaned up in a linear-time verification pass using &lt;a href="https://dl.acm.org/doi/10.1007/BF01294131" rel="noopener noreferrer"&gt;King's MST verification algorithm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This approach fits a framework that didn't exist when the thesis was written: &lt;strong&gt;learning-augmented algorithms&lt;/strong&gt; (Mitzenmacher and Vassilvitskii, 2020). The formal question is: what prediction quality guarantees O(m + n) total cost?&lt;/p&gt;

&lt;p&gt;The thesis proves the disjoint-set operations can be bounded to O(m + n) on the RAM model when allowing a bounded degree of error. The precise bounds on prediction quality needed for this to work are stated but not tight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack vector 2: Density partition
&lt;/h2&gt;

&lt;p&gt;Different algorithms are linear at different densities. Prim is linear on dense graphs (m ≥ n^p for p &amp;gt; 1). Kruskal-style algorithms are nearly linear on sparse graphs. Neither covers all graphs.&lt;/p&gt;

&lt;p&gt;The density partition classifies vertices by degree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HD&lt;/strong&gt; (high degree): degree &amp;gt; c · log³(n)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LD&lt;/strong&gt; (low degree): degree &amp;lt; c · log³(n)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then by neighbor types: &lt;strong&gt;HDN&lt;/strong&gt; (high-degree neighbors), &lt;strong&gt;LDN&lt;/strong&gt; (low-degree neighbors), &lt;strong&gt;MDN&lt;/strong&gt; (mixed). This classification takes O(m + n) — two passes over the adjacency lists.&lt;/p&gt;

&lt;p&gt;For HD-HDN components (dense), run Prim. For LD-LDN components (sparse), run modified Kruskal with the approximate union-find from attack vector 1. The MDN boundary edges form a small graph — O(n / log³(n)) edges — that can be solved recursively.&lt;/p&gt;

&lt;p&gt;At each recursion level, total work is O(m + n). The recursion depth is bounded. This is the glue that combines the other two subproblems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack vector 3: Cycle hierarchy
&lt;/h2&gt;

&lt;p&gt;Consider a sparse graph that is almost a tree. Most vertices have degree 1 or 2. The MST must include all edges connecting degree-1 vertices — there is no alternative path. So you can trim those edges and contract the leaves.&lt;/p&gt;

&lt;p&gt;The problem: trimming cascades. Removing a leaf can create a new leaf, which creates another, propagating across the graph. Naive trimming is O(n²).&lt;/p&gt;

&lt;p&gt;If you know the cycle structure, you know where each cascade stops — at a vertex that participates in a cycle. With this information, trimming runs in O(n).&lt;/p&gt;

&lt;p&gt;The cycle hierarchy organizes cycles by nesting depth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Level 0: triangles&lt;/li&gt;
&lt;li&gt;Level 1: 4-cycles not decomposable into triangles&lt;/li&gt;
&lt;li&gt;Level k: (k+3)-cycles that may contain lower-level cycles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building the hierarchy seems circular — you need a spanning tree to find cycles, but you're computing the MST. The escape: cycle detection doesn't require edge weights. Any spanning tree (from DFS or BFS, computed in O(m + n) without weight comparisons) reveals the cycle structure. Each non-tree edge closes exactly one fundamental cycle.&lt;/p&gt;

&lt;p&gt;You compute the topological hierarchy weight-free, then apply it to the weighted MST computation. The hierarchy tells trimming where to stop.&lt;/p&gt;

&lt;p&gt;This is the hardest subproblem. The construction is clear in outline, but the formal proof that it runs in O(m + n) for arbitrary graphs is incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  How they compose
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Subproblem 1                    Subproblem 3
(Approximate Union-Find)        (Cycle Hierarchy)
         \                          /
          v                        v
           Subproblem 2
           (Density Partition)
                  |
                  v
          Full MST Algorithm
              O(m + n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subproblems 1 and 3 are independent. Subproblem 2 combines their results. If all three are solved, they compose into a deterministic O(m + n) MST algorithm on the Word RAM.&lt;/p&gt;

&lt;p&gt;On a pointer machine, this approach does not help. The disjoint-set operations without table lookup are bounded by O(m α(m,n)), and that factor cannot be removed without random access.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's incomplete
&lt;/h2&gt;

&lt;p&gt;Subproblem 1 has a theorem and proof sketch. The formal bounds on prediction quality are not tight. Subproblem 3 has a clear construction but the O(m + n) proof for arbitrary graphs is missing. Subproblem 2 depends on both.&lt;/p&gt;

&lt;p&gt;Since 2018, each subproblem has connected to active research:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Approximate union-find → learning-augmented algorithms&lt;/li&gt;
&lt;li&gt;Density partition → local graph algorithms and graph sparsification&lt;/li&gt;
&lt;li&gt;Cycle hierarchy → dynamic connectivity and top trees&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The three subproblems remain open. Whether O(m) MST is achievable deterministically, or whether α(m,n) is fundamental, is one of the oldest open problems in computer science.&lt;/p&gt;




&lt;p&gt;The full technical writeup with proofs, definitions, and historical context is on my blog: &lt;a href="https://keibisoft.com/blog/mst-road-to-linear-complexity.html" rel="noopener noreferrer"&gt;The MST Problem: Three Subproblems to Linear Time&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The thesis was written at &lt;a href="https://di.ku.dk/" rel="noopener noreferrer"&gt;DIKU, University of Copenhagen&lt;/a&gt;, supervised by Jyrki Katajainen.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Easter egg time&lt;/em&gt;: Bernard Chazelle the CS professor from Toronto who has the best algorithm for the problem up to date is the father of Damien Chazelle, the director of Whiplash and LaLa Land.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>computerscience</category>
      <category>complexity</category>
    </item>
    <item>
      <title>Optimizing encrypted P2P file transfer - from 225 to 441 MB/s</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Sun, 29 Mar 2026 08:36:21 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/optimizing-encrypted-p2p-file-transfer-from-225-to-441-mbs-1mm1</link>
      <guid>https://dev.to/mfc_keibisoft/optimizing-encrypted-p2p-file-transfer-from-225-to-441-mbs-1mm1</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of the &lt;a href="https://keibisoft.com/tools/keibidrop.html" rel="noopener noreferrer"&gt;KEIBIDROP&lt;/a&gt; development blog. KEIBIDROP is in active development. Release is coming soon.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;KEIBIDROP transfers files between two peers over encrypted gRPC. The full stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Disk I/O -&amp;gt; FUSE kernel -&amp;gt; FUSE daemon -&amp;gt; gRPC framing -&amp;gt; ChaCha20-Poly1305 -&amp;gt; TCP -&amp;gt; Peer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We built micro-benchmarks for each layer and measured throughput with 1GB files on an Intel MacBook Pro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Baseline numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;Overhead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw disk (SSD)&lt;/td&gt;
&lt;td&gt;~5 GB/s&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw gRPC (no encryption)&lt;/td&gt;
&lt;td&gt;981 MB/s&lt;/td&gt;
&lt;td&gt;5x vs disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted gRPC (ChaCha20)&lt;/td&gt;
&lt;td&gt;437 MB/s&lt;/td&gt;
&lt;td&gt;2.2x vs raw gRPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FUSE end-to-end&lt;/td&gt;
&lt;td&gt;225 MB/s&lt;/td&gt;
&lt;td&gt;1.9x vs encrypted gRPC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The encryption layer costs 2.2x. FUSE adds another 1.9x.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six optimizations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Cache the AEAD cipher.&lt;/strong&gt; The original code created a new ChaCha20-Poly1305 cipher for every message. Caching it in the constructor is safe because the nonce is a monotonic counter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Before: creating cipher per-message&lt;/span&gt;
&lt;span class="n"&gt;aead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;chacha20poly1305&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// expensive!&lt;/span&gt;

&lt;span class="c"&gt;// After: created once in constructor&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SecureWriter&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;aead&lt;/span&gt; &lt;span class="n"&gt;cipher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AEAD&lt;/span&gt;  &lt;span class="c"&gt;// created once&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Single combined TCP write.&lt;/strong&gt; Two syscalls per message (header + payload) became one. At 512KB chunks, this halves the &lt;code&gt;write()&lt;/code&gt; call count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. In-place decryption.&lt;/strong&gt; &lt;code&gt;aead.Open(ciphertext[:0], nonce, ciphertext, nil)&lt;/code&gt; reuses the ciphertext buffer instead of allocating a new one. ~2000 fewer allocations per 1GB transfer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Async cache writes in FUSE Read.&lt;/strong&gt; FUSE needs the data returned to the kernel. Writing that data to the local cache file can happen in a background goroutine. A WaitGroup ensures everything completes before file close. Cache write overhead dropped from 16% to 1.8%.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cacheFD&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CacheWg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CacheWg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;cacheFD&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cacheOffset&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Push-based StreamFile RPC.&lt;/strong&gt; The original streaming used request-response per chunk. A new server-streaming RPC pushes all chunks without waiting. On-demand reads (random access) still use bidirectional streaming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Silencing hot-path logs.&lt;/strong&gt; ~120 &lt;code&gt;slog.Info/Debug/Warn&lt;/code&gt; calls in FUSE handlers. Even structured logging allocates. Commenting out non-error logs on Read/Write/Getattr gave 2-6% throughput improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  After optimizations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted gRPC&lt;/td&gt;
&lt;td&gt;437 MB/s&lt;/td&gt;
&lt;td&gt;441 MB/s&lt;/td&gt;
&lt;td&gt;+0.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FUSE end-to-end&lt;/td&gt;
&lt;td&gt;225 MB/s&lt;/td&gt;
&lt;td&gt;231 MB/s&lt;/td&gt;
&lt;td&gt;+2.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The encryption tax: 1.49x
&lt;/h2&gt;

&lt;p&gt;We built a controlled benchmark comparing identical gRPC transfers over plain TCP vs ChaCha20-Poly1305:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transport&lt;/th&gt;
&lt;th&gt;MB/s&lt;/th&gt;
&lt;th&gt;Duration (100MB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain TCP + gRPC&lt;/td&gt;
&lt;td&gt;657&lt;/td&gt;
&lt;td&gt;152ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted (ChaCha20-Poly1305)&lt;/td&gt;
&lt;td&gt;441&lt;/td&gt;
&lt;td&gt;227ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;33% of transfer time goes to encryption. Our earlier 2.2x estimate was comparing across different test runs with different system load. The controlled A/B benchmark showed the real cost is much lower.&lt;/p&gt;

&lt;h2&gt;
  
  
  The irreducible 48%
&lt;/h2&gt;

&lt;p&gt;The gap between encrypted gRPC (441 MB/s) and FUSE end-to-end (231 MB/s) is 48%. Every FUSE Read requires two kernel-to-userspace context switches. We confirmed this by measuring raw FUSE overhead (local file read through FUSE vs direct): ~48%, matching exactly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plain gRPC:     657 MB/s  (ceiling)
  -33% encryption
Encrypted gRPC: 441 MB/s
  -48% FUSE kernel overhead
FUSE E2E:       231 MB/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The FUSE overhead is nearly twice the encryption cost. If we wanted to improve throughput, reducing FUSE transitions would have more impact than switching ciphers. But FUSE is the cost of running a userspace filesystem, and for secure P2P file sharing the tradeoff is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Without isolating each layer, we would have optimized the wrong thing. The biggest win (async cache writes, 16% to 1.8%) was only visible when measured independently.&lt;/li&gt;
&lt;li&gt;Caching the AEAD cipher and in-place decryption are tiny code changes with measurable impact at 2000+ messages per second.&lt;/li&gt;
&lt;li&gt;Structured logging is not free on hot paths. Even checking the log level has overhead.&lt;/li&gt;
&lt;li&gt;FUSE has an inherent 48% overhead from context switches. No amount of userspace optimization can fix this.&lt;/li&gt;
&lt;li&gt;Server-streaming eliminates round-trip latency for sequential prefetch. Request-response is still needed for random access.&lt;/li&gt;
&lt;li&gt;Our initial 2.2x encryption estimate was wrong. A controlled A/B benchmark (same test, same system) showed 1.49x. Always compare apples to apples.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;More from the KEIBIDROP blog: &lt;a href="https://keibisoft.com/blog.html" rel="noopener noreferrer"&gt;full series&lt;/a&gt; | &lt;a href="https://keibisoft.com/tools/keibidrop.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt; | &lt;a href="https://keibisoft.com/tools/keibidrop-faq.html" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>performance</category>
      <category>networking</category>
      <category>filesystem</category>
    </item>
    <item>
      <title>Make git clone work between encrypted P2P FUSE peers (7 bugs)</title>
      <dc:creator>Marius-Florin Cristian</dc:creator>
      <pubDate>Sun, 29 Mar 2026 08:34:07 +0000</pubDate>
      <link>https://dev.to/mfc_keibisoft/make-git-clone-work-between-encrypted-p2p-fuse-peers-7-bugs-1pb0</link>
      <guid>https://dev.to/mfc_keibisoft/make-git-clone-work-between-encrypted-p2p-fuse-peers-7-bugs-1pb0</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of the &lt;a href="https://keibisoft.com/tools/keibidrop.html" rel="noopener noreferrer"&gt;KEIBIDROP&lt;/a&gt; development blog. KEIBIDROP is in active development. Release is coming soon.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;KEIBIDROP mounts a virtual FUSE filesystem for each peer. When Peer A creates or modifies files, the changes sync to Peer B's mount in real-time through encrypted gRPC. We already made git work on a single FUSE mount (&lt;a href="https://keibisoft.com/blog/keibidrop-git-inside-fuse.html" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;). The question was: what happens when Peer A clones a repo into their mount, and Peer B tries to use it through theirs?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bob (FUSE mount)                     Alice (FUSE mount)
+-- .git/                            +-- .git/          &amp;lt;- synced from Bob
|   +-- HEAD                         |   +-- HEAD
|   +-- config                       |   +-- config
|   +-- objects/pack/                |   +-- objects/pack/
|   |   +-- pack-abc123.pack (2MB)   |   |   +-- pack-abc123.pack
|   +-- refs/heads/main              |   +-- refs/heads/main
+-- go.mod                           +-- go.mod
+-- README.md                        +-- README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven bugs appeared across three debugging sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: The rename race
&lt;/h2&gt;

&lt;p&gt;Git uses atomic writes: create &lt;code&gt;config.lock&lt;/code&gt;, write content, close, rename to &lt;code&gt;config&lt;/code&gt;. KEIBIDROP sends &lt;code&gt;ADD_FILE&lt;/code&gt; on close and &lt;code&gt;RENAME_FILE&lt;/code&gt; on rename. The prefetch goroutine would download &lt;code&gt;config.lock&lt;/code&gt;, finish before the &lt;code&gt;RENAME_FILE&lt;/code&gt; arrived, and skip the disk rename because the in-memory path hadn't changed yet.&lt;/p&gt;

&lt;p&gt;Fix: the &lt;code&gt;RENAME_FILE&lt;/code&gt; handler now does &lt;code&gt;os.Rename&lt;/code&gt; on disk before updating maps. The prefetch cleanup becomes a redundant safety net.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: Pack file corruption (20 missing bytes)
&lt;/h2&gt;

&lt;p&gt;Alice's pack file was 2,182,186 bytes. Bob's was 2,182,206 bytes. Git's &lt;code&gt;index-pack&lt;/code&gt; appends a 20-byte SHA-1 checksum &lt;em&gt;after&lt;/em&gt; the file is closed but &lt;em&gt;before&lt;/em&gt; the rename. KEIBIDROP sent the &lt;code&gt;ADD_FILE&lt;/code&gt; notification at close time with the pre-checksum size.&lt;/p&gt;

&lt;p&gt;Fix: after renaming on disk, compare local file size with the size in the &lt;code&gt;RENAME_FILE&lt;/code&gt; notification. If they differ, trigger a re-download.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: macFUSE cache poisoning
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git status&lt;/code&gt; showed &lt;code&gt;deleted: go.mod&lt;/code&gt; even though the file existed on disk. macFUSE's &lt;code&gt;negative_vncache&lt;/code&gt; option caches ENOENT results. macOS probed Alice's mount (Spotlight, fsevents) before the file arrived, the kernel cached "doesn't exist", and subsequent reads never reached our FUSE handler.&lt;/p&gt;

&lt;p&gt;Fix: remove &lt;code&gt;negative_vncache&lt;/code&gt; from mount options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 4: The 612-file hang
&lt;/h2&gt;

&lt;p&gt;Cloning a large repo (612 files) hung after checkout completed. Every FUSE &lt;code&gt;Release&lt;/code&gt; sent a gRPC notification. 612 files closing in rapid succession overwhelmed the connection.&lt;/p&gt;

&lt;p&gt;Three-part fix:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;BatchNotify&lt;/code&gt; RPC -- batch 64 notifications into a single gRPC call&lt;/li&gt;
&lt;li&gt;Per-path debounce (200ms deadline, reset on each update) on the sender side&lt;/li&gt;
&lt;li&gt;Prefetch semaphore (max 8 concurrent) on the receiver side
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;BatchNotify&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BatchNotifyRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BatchNotifyResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;BatchNotifyRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="n"&gt;NotifyRequest&lt;/span&gt; &lt;span class="na"&gt;notifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;uint64&lt;/span&gt; &lt;span class="na"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;uint64&lt;/span&gt; &lt;span class="na"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;h2&gt;
  
  
  Bug 5: 190,000 log lines
&lt;/h2&gt;

&lt;p&gt;After fixing the notification flood, the clone still hung for 30 seconds. Turns out we had 207,000 getattr calls and 190,000 ENOENT error logs, all written synchronously. macOS probes hundreds of paths per directory (Spotlight, fsevents, &lt;code&gt;.DS_Store&lt;/code&gt;, resource forks) and without &lt;code&gt;negative_vncache&lt;/code&gt; every probe hits our handler.&lt;/p&gt;

&lt;p&gt;Fix: remove trace logs from hot-path handlers. Stop logging ENOENT as an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"FUSE getattr"&lt;/span&gt; Log_Bob.txt    207,137
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Failed to lstat"&lt;/span&gt; Log_Bob.txt  190,614
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bug 6: Hook permission denied
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fatal: cannot exec '.git/hooks/post-checkout': Operation not permitted&lt;/code&gt;. macOS Gatekeeper blocks executing scripts from FUSE mounts.&lt;/p&gt;

&lt;p&gt;Fix: add &lt;code&gt;defer_permissions&lt;/code&gt; to mount options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 7: LFS corruption (intermediate sizes + stale notifications)
&lt;/h2&gt;

&lt;p&gt;Git LFS downloads a 420MB XML file incrementally into &lt;code&gt;.git/lfs/incomplete/&amp;lt;hash&amp;gt;&lt;/code&gt;. Each intermediate close triggers &lt;code&gt;ADD_FILE&lt;/code&gt; with the current size. The peer starts prefetching at 100MB, restarts at 200MB, restarts at 300MB. Then LFS renames &lt;code&gt;incomplete/&amp;lt;hash&amp;gt;&lt;/code&gt; to &lt;code&gt;objects/&amp;lt;sha&amp;gt;/&amp;lt;hash&amp;gt;&lt;/code&gt;, but the debounced ADD_FILE for the old path fires after the rename, pointing to a file that no longer exists.&lt;/p&gt;

&lt;p&gt;Fix: per-path debounce with RENAME retargeting. When a RENAME arrives, the pending ADD_FILE for the old path gets its path rewritten to the new path instead of being deleted or sent stale.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;bindings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotifyType_RENAME_FILE&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OldPath&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OldPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;  &lt;span class="c"&gt;// retarget to new path&lt;/span&gt;
        &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;immediate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We tried three approaches: deleting the pending notification (broke content delivery), sending it stale (wrong path), retargeting (correct).&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;After all fixes, the full git workflow works between encrypted P2P FUSE peers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Bob clones a repo
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;MountBob &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git clone git@github.com:user/repo.git
&lt;span class="go"&gt;Cloning into 'repo'... done.

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Alice sees it immediately
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;MountAlice/repo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git status
&lt;span class="go"&gt;On branch main -- nothing to commit, working tree clean

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Alice creates a branch and commits
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; test_me
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; tst.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git add tst.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Commit for test"&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Bob sees the branch and commit
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;MountBob/repo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git log
&lt;span class="gp"&gt;commit 38e23bc (HEAD -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;test_me&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;    Commit for test
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tested with a small repo (24 objects, 2MB, 8 files) and a large repo (2339 objects, 257MB + 3.71 GiB LFS, 612 files).&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;FUSE handlers must rename on disk, not just in memory. Goroutines race with notification handlers, especially for tiny files that download in milliseconds.&lt;/li&gt;
&lt;li&gt;Git writes data, closes the file, then renames. Between close and rename, other processes can modify the file (index-pack appending checksums). The rename notification has the correct size. Use it.&lt;/li&gt;
&lt;li&gt;macFUSE's &lt;code&gt;negative_vncache&lt;/code&gt; silently caches ENOENT results. Our handler was never called. These bugs don't show up in FUSE logs because the kernel short-circuits the call.&lt;/li&gt;
&lt;li&gt;612 files = 612 goroutines = 612 gRPC streams = connection collapse. A bounded channel + batching worker gives backpressure without blocking the caller.&lt;/li&gt;
&lt;li&gt;190,000 synchronous log writes caused a 30-second hang. The logging seemed harmless with 20 files. 612 files with macOS probing turned it into 400,000 disk writes.&lt;/li&gt;
&lt;li&gt;For debounce + rename: you can't delete the pending notification (peer never gets content) and you can't send it stale (wrong path). Retarget it to the new path. Three attempts to get this right.&lt;/li&gt;
&lt;li&gt;None of these bugs are git-specific. Any application that creates files through one peer's mount and reads them through another would hit the same problems. Git just exercises every edge case.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;More from the KEIBIDROP blog: &lt;a href="https://keibisoft.com/blog.html" rel="noopener noreferrer"&gt;full series&lt;/a&gt; | &lt;a href="https://keibisoft.com/tools/keibidrop.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt; | &lt;a href="https://keibisoft.com/tools/keibidrop-faq.html" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>git</category>
      <category>filesystems</category>
      <category>fml</category>
    </item>
  </channel>
</rss>
