<?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: ihar ivanuto</title>
    <description>The latest articles on DEV Community by ihar ivanuto (@ihar_ivanuto).</description>
    <link>https://dev.to/ihar_ivanuto</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%2F4011433%2F9ecc6976-5ad0-4418-b65c-337e59d4ad19.jpg</url>
      <title>DEV Community: ihar ivanuto</title>
      <link>https://dev.to/ihar_ivanuto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ihar_ivanuto"/>
    <language>en</language>
    <item>
      <title>How to split 10GB JSON files in seconds without hitting RAM limits</title>
      <dc:creator>ihar ivanuto</dc:creator>
      <pubDate>Wed, 01 Jul 2026 22:20:35 +0000</pubDate>
      <link>https://dev.to/ihar_ivanuto/how-to-split-10gb-json-files-in-seconds-without-hitting-ram-limits-obk</link>
      <guid>https://dev.to/ihar_ivanuto/how-to-split-10gb-json-files-in-seconds-without-hitting-ram-limits-obk</guid>
      <description>&lt;p&gt;Hi Everyone!&lt;/p&gt;

&lt;p&gt;We had this classic pain point on our project: constantly chewing through massive JSON arrays. Catalogs, analytics dumps, ML datasets — files ranging from a couple of hundred megabytes to tens of gigabytes.&lt;/p&gt;

&lt;p&gt;The task was stupidly simple: split a giant JSON array into individual elements so we could chunk them or throw them into parallel processing. No data transformation, no querying by keys. We literally just needed to find where each chunk starts and ends.&lt;/p&gt;

&lt;p&gt;Naturally, we started with the classic approach: &lt;code&gt;json.Unmarshal&lt;/code&gt; -&amp;gt; slice -&amp;gt; &lt;code&gt;json.Marshal&lt;/code&gt;. On a 10GB file, memory consumption went to the moon 🚀. We ended up spending more time fighting the Go garbage collector (GC) than doing actual work.&lt;/p&gt;

&lt;p&gt;And then it clicked: to just move the data around, we don't need to understand what's inside it. We just need to find the boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop parsing, start scanning 🛑
&lt;/h2&gt;

&lt;p&gt;Every parser out there (even the ultra-fast ones like &lt;code&gt;sonic&lt;/code&gt; or &lt;code&gt;simdjson&lt;/code&gt;) still builds a tree in memory. Instead, you can just treat the JSON as a raw byte stream. Look for structural markers, find the edges, and cut.&lt;/p&gt;

&lt;p&gt;The entire logic boils down to a tiny state machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nesting counter:&lt;/strong&gt; &lt;code&gt;{&lt;/code&gt; and &lt;code&gt;[&lt;/code&gt; go &lt;code&gt;+1&lt;/code&gt;, &lt;code&gt;}&lt;/code&gt; and &lt;code&gt;]&lt;/code&gt; go &lt;code&gt;-1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;String tracking:&lt;/strong&gt; keep track of when you enter &lt;code&gt;"..."&lt;/code&gt; so you don't accidentally react to brackets inside a text field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escapes:&lt;/strong&gt; a &lt;code&gt;\"&lt;/code&gt; inside a string is a trap, not the end of the string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The boundary:&lt;/strong&gt; whenever your nesting depth is exactly &lt;code&gt;0&lt;/code&gt;, any comma &lt;code&gt;,&lt;/code&gt; is where you split.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it. We don't care about keys or values. We don't allocate a single byte, we just return memory views (slices) of the original buffer.&lt;/p&gt;

&lt;p&gt;Here’s what the concept looks like in Go (oversimplified, ignoring string logic):&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="n"&gt;findElements&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="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Chunk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Chunk&lt;/span&gt;
    &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&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;switch&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="sc"&gt;'{'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'['&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="sc"&gt;'}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;']'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="sc"&gt;','&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;depth&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;chunks&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;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Chunk&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;End&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&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;if&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;chunks&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;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Chunk&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;End&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;len&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Obviously, this naive code will break on the first tricky whitespace or string, but you get the point. &lt;strong&gt;We aren't parsing. We are scanning.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is this so damn fast? ⚡
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero allocations in the hot loop.&lt;/strong&gt; You're just handing back &lt;code&gt;data[start:end]&lt;/code&gt;. No new objects, no copying strings, no building hash maps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware absolutely loves it.&lt;/strong&gt; Your entire working state is basically two integers. It easily fits in L1 cache, and memory reads are strictly sequential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The branch predictor is happy.&lt;/strong&gt; A simple state machine with highly predictable transitions is infinitely easier for the CPU to digest than a full parser juggling dozens of token types.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Look at how much work we are skipping:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Standard Parser&lt;/th&gt;
&lt;th&gt;Boundary Scanner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read bytes&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Classify tokens&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;{}[]"\&lt;/code&gt; and &lt;code&gt;,&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build hash maps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allocate strings&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allocate slices&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type conversion&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What you get back&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[]MyStruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[][]byte&lt;/code&gt; (pointers to original buffer)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We are literally throwing away 80% of the overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  But how fast is it actually? 🏎️
&lt;/h2&gt;

&lt;p&gt;I got a bit carried away and polished this into a production-ready tool. I added proper string handling, escape tracking, and rewrote the hot loop in &lt;strong&gt;AVX2 assembly&lt;/strong&gt; (chewing through 32 bytes per cycle using SIMD bitmasks).&lt;/p&gt;

&lt;p&gt;Tbh, the results surprised even me:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;Memory Overhead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;encoding/json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full parse → Go structs&lt;/td&gt;
&lt;td&gt;~107 MB/s&lt;/td&gt;
&lt;td&gt;3-4x input size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sonic&lt;/code&gt; / &lt;code&gt;simdjson-go&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Optimized parse → structs/AST&lt;/td&gt;
&lt;td&gt;~400–700 MB/s&lt;/td&gt;
&lt;td&gt;~1.1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;My AVX2 scanner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Just finds boundaries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~4.1 GB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1.0x (zero extra)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At &lt;strong&gt;4.1 GB/s&lt;/strong&gt;, the algorithm isn't even the bottleneck anymore. It's bottlenecked by the RAM's read bandwidth. The CPU is just sitting there waiting for the next cache line to arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch (Tradeoffs) ⚠️
&lt;/h2&gt;

&lt;p&gt;Because I went the unsafe and raw assembly route for maximum speed, you have to pay the price:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform-specific:&lt;/strong&gt; The AVX2 branch only works on &lt;code&gt;amd64&lt;/code&gt;. For ARM (hello MacBooks), you need a pure Go fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory lifecycle danger:&lt;/strong&gt; You are getting slices that point directly to the original buffer. If that &lt;code&gt;[]byte&lt;/code&gt; gets overwritten or GC'd while you're still working with the chunks... it's going to hurt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No validation:&lt;/strong&gt; The scanner takes your word that the JSON is valid. Feed it garbage, and it will silently slice up garbage.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The biggest insight was stupidly simple: stop thinking &lt;em&gt;"I need to parse this JSON"&lt;/em&gt; and start thinking &lt;em&gt;"I need to find boundaries in a byte stream"&lt;/em&gt;. Once I changed my perspective, the code wrote itself and the performance gap was massive.&lt;/p&gt;

&lt;p&gt;Has anyone else suffered through this? How do you guys route or chunk massive JSON payloads in production when you simply can't fit them into RAM?&lt;/p&gt;

&lt;p&gt;👇 If anyone wants to poke around the assembly or run the benchmarks, the repo is here:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;🔗 GitHub: &lt;a href="https://github.com/GenshIv/silentjson" rel="noopener noreferrer"&gt;GenshIv/silentjson&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>performance</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
