<?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: Abiodun Longe</title>
    <description>The latest articles on DEV Community by Abiodun Longe (@odunlemi).</description>
    <link>https://dev.to/odunlemi</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F254981%2F39c2eb72-ec8e-4013-b590-14388b52d4df.jpg</url>
      <title>DEV Community: Abiodun Longe</title>
      <link>https://dev.to/odunlemi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/odunlemi"/>
    <language>en</language>
    <item>
      <title>Zero-Copy Buffer Manipulation: Parsing Market Data at Memory Speed</title>
      <dc:creator>Abiodun Longe</dc:creator>
      <pubDate>Fri, 20 Mar 2026 07:51:00 +0000</pubDate>
      <link>https://dev.to/odunlemi/zero-copy-buffer-manipulation-parsing-market-data-at-memory-speed-1ol5</link>
      <guid>https://dev.to/odunlemi/zero-copy-buffer-manipulation-parsing-market-data-at-memory-speed-1ol5</guid>
      <description>&lt;p&gt;Price feeds hit your server. About forty thousand messages per second, each one carrying an order ID, timestamp, price and quantity. Node.js receives them over TCP, and before your app logic gets to touch them, you're already racking up a cost, and several times over as well. Understanding what that cost is, and how to stop it, is what separates a node.js service that struggles under load from one that holds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when data arrives?
&lt;/h2&gt;

&lt;p&gt;When a TCP packet lands, your OS writes it into a kernel buffer. &lt;code&gt;libuv&lt;/code&gt; (makes Node not &lt;a href="https://libuv.org/" rel="noopener noreferrer"&gt;single lane&lt;/a&gt;), copies that packet from the kernel buffer to a Node buffer. The data event fires for one. So far, so good. Most code then does something like this:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;




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

&lt;p&gt;&lt;a href="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%2Farticles%2Fa373nyzs8sgzmcdhfsl5.webp" class="article-body-image-wrapper"&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%2Farticles%2Fa373nyzs8sgzmcdhfsl5.webp" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under load, it's a problem. &lt;code&gt;chunk.toString&lt;/code&gt; allocates a new String, fire for two, &lt;code&gt;JSON.parse&lt;/code&gt; goes through that string for each character, allocating new Objects for each key-value pair, fire three, four, five, and counting. You get the gist. Each allocation goes on the V8 heap and each object is something the garbage collector has to deal with.&lt;/p&gt;

&lt;p&gt;For a low-frequency API, this is a non-issue, but for a market feed with tens of thousands of messages per second, you're working the memory faster than you're processing data and your garbage collector will screech.&lt;/p&gt;

&lt;p&gt;We have to stop copying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buffers, ArrayBuffers and the Memory model
&lt;/h2&gt;

&lt;p&gt;Before working with binary data directly, let's understand how Node organizes memory. A buffer in Node is a piece of memory allocated outside the V8 heap. Important info because off-heap memory doesn't trigger the same garbage collection cycles as regular JS objects. Internally, every buffer is backed by an ArrayBuffer.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;




&lt;p&gt;&lt;code&gt;allocUnsafe&lt;/code&gt; skips setting the memory to zero, and appropriate when you are about to overwrite every byte anyway. &lt;code&gt;ArrayBuffer&lt;/code&gt; itself has no methods for reading or writing. It's just raw memory. What gives you access to it are TypedArrays and &lt;code&gt;DataView&lt;/code&gt;. These are views over an existing &lt;code&gt;ArrayBuffer&lt;/code&gt;, they don't own or copy data, they simply reference a region of memory and give you typed access to the bytes in it.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;




&lt;p&gt;This single pattern which constructs a &lt;code&gt;DataView&lt;/code&gt; over a &lt;code&gt;Buffer&lt;/code&gt;'s underlying &lt;code&gt;ArrayBuffer&lt;/code&gt; is the base of zero-copy parsing in Node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing a binary frame without copying
&lt;/h2&gt;

&lt;p&gt;A simplified binary order on a market feed might look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Message type&lt;/td&gt;
&lt;td&gt;Uint8&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestamp&lt;/td&gt;
&lt;td&gt;BigUint64&lt;/td&gt;
&lt;td&gt;8 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order id&lt;/td&gt;
&lt;td&gt;Uint32&lt;/td&gt;
&lt;td&gt;4 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;Float64&lt;/td&gt;
&lt;td&gt;8 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantity&lt;/td&gt;
&lt;td&gt;Uint32&lt;/td&gt;
&lt;td&gt;4 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total: 25 bytes for each message&lt;/p&gt;

&lt;p&gt;The simpler approach would be to convert this string and split on delimiters, or to wrap it in JSON on the producer side (that is, where the data is coming from). Neither help performance at scale. Here's the zero-copy approach:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;




&lt;p&gt;There is no string conversion or JSON parsing. &lt;code&gt;DataView&lt;/code&gt; reads bytes directly from the memory allocation, interpreting them as the type you specify. &lt;code&gt;getUint32&lt;/code&gt;, &lt;code&gt;getFloat64&lt;/code&gt; and similar methods handle &lt;a href="https://en.wikipedia.org/wiki/Endianness" rel="noopener noreferrer"&gt;endianness&lt;/a&gt; for you.&lt;/p&gt;

&lt;p&gt;A caveat is the returned object literal itself is still allocated on the V8 heap. If you're allocating a new object on every message,  at forty thousand messages per second, you still have load on the heap, just less of it. &lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing memory across threads with SharedArrayBuffer
&lt;/h2&gt;

&lt;p&gt;Zero-copy becomes truly powerful in a parallelism context. Normally, when you pass data to a &lt;code&gt;worker_thread&lt;/code&gt;, Node serialises it, which is the same as &lt;code&gt;JSON.stringify&lt;/code&gt;or &lt;code&gt;.parse&lt;/code&gt;, with the same copy cost. For a 25-byte message that cost is small, but dealing with a large orderbook or a batch of historical tick data, it compounds fast.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SharedArrayBuffer&lt;/code&gt; removes that cost entirely. Instead of copying data into the worker, both threads access the same physical memory.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;




&lt;p&gt;The &lt;code&gt;postMessage&lt;/code&gt; call transfers the &lt;code&gt;SharedArrayBuffer&lt;/code&gt; reference, an object pointer, not 25 bytes of market data. The worker reads directly from the same physical memory the main thread wrote into. No serialization and no copy.&lt;/p&gt;

&lt;p&gt;If you need true zero-copy transfer (relinquishing ownership rather than sharing it), a regular &lt;code&gt;ArrayBuffer&lt;/code&gt; can be transferred with &lt;code&gt;postMessage&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;




&lt;p&gt;The tradeoff is transferred buffers are immediately unusable in the main thread. Shared buffers require you to manage concurrent access between the main and worker threads, if they both read and write simultaneously, you get data races. For a consumer pattern where the main thread writes and the worker thread reads, data races are typically avoidable by design&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-allocation: The other half of the equation
&lt;/h2&gt;

&lt;p&gt;Zero-copy parsing eliminates unnecessary data duplication, but the allocation cost is still real. If &lt;code&gt;parseOrderMessage&lt;/code&gt; returns a new object literal on every call, V8 is creating and discarding forty thousand objects per second. The garbage collector will pause to collect them.&lt;/p&gt;

&lt;p&gt;The solution is pre-allocation: create your result object once and mutate it in place.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;




&lt;p&gt;Now your hot path mutates a stable object rather than building and discarding new ones. V8's hidden class optimizations also apply more to objects with a fixed shape; property reads and writes become significantly cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;Zero-copy buffer manipulation in Node doesn't make V8's garbage collector disappear, the garbage collector pauses are still the limit on what latency guarantees you can make. What zero-copy does is remove the overhead you can control: serialization costs, redundant data copies, and allocation churn on the hot path.&lt;/p&gt;

&lt;p&gt;For a high-throughput market data pipeline, that matters. &lt;code&gt;DataView&lt;/code&gt; over a Buffer's underlying &lt;code&gt;ArrayBuffer&lt;/code&gt; gives you direct binary access at almost zero cost. &lt;code&gt;SharedArrayBuffer&lt;/code&gt; lets you pass that data to worker threads and pre-allocated result objects stop you generating garbage collector work on every message.&lt;/p&gt;

&lt;p&gt;Together, these patterns get Node as close to its latency floor as the runtime allows. It is not a replacement for C++ where deterministic microsecond execution is a hard requirement, but a substantial, practical improvement over the string-and-JSON default that most services start with.&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
