JSONL Performance

Benchmarks, metrics, and optimization strategies for high-performance JSONL processing

Performance Advantages

O(1) Memory

Constant memory usage regardless of file size. Process 100GB files with 10MB RAM.

Instant Start

Begin processing immediately without loading entire file. 0ms time-to-first-record.

Append Speed

Add records in O(1) time. No file rewriting required unlike JSON arrays.

Parse Speed Benchmarks

Python: JSON vs JSONL Parsing

Test setup: 100,000 records, ~50MB file, Python 3.11, MacBook Pro M1

Format Library Parse Time Memory Peak Time to First Record
JSON Array json (stdlib) 1,450ms 280MB 1,450ms
JSONL json (stdlib) 1,380ms 8MB <1ms
JSONL orjson 410ms 8MB <1ms
JSONL.gz orjson + gzip 680ms 12MB 15ms

Key takeaways:

  • JSONL uses 35x less memory than JSON array
  • orjson provides 3.4x speedup over standard library
  • Compressed JSONL still faster than uncompressed JSON array
# Benchmark code
import json
import orjson
import gzip
import time
import tracemalloc

def benchmark_json_array():
    tracemalloc.start()
    start = time.time()
    with open('data.json', 'r') as f:
        data = json.load(f)  # Load entire array
        for record in data:
            process(record)
    elapsed = time.time() - start
    peak = tracemalloc.get_traced_memory()[1] / 1024 / 1024
    tracemalloc.stop()
    print(f"JSON Array: {elapsed*1000:.0f}ms, {peak:.0f}MB")

def benchmark_jsonl_orjson():
    tracemalloc.start()
    start = time.time()
    with open('data.jsonl', 'rb') as f:
        for line in f:
            record = orjson.loads(line)
            process(record)
    elapsed = time.time() - start
    peak = tracemalloc.get_traced_memory()[1] / 1024 / 1024
    tracemalloc.stop()
    print(f"JSONL (orjson): {elapsed*1000:.0f}ms, {peak:.0f}MB")

JavaScript/Node.js: Streaming Performance

Test setup: 100,000 records, ~50MB file, Node.js 20.x, MacBook Pro M1

Approach Parse Time Memory Peak Throughput
JSON.parse() entire file 2,100ms 350MB 47 rec/ms
readline + JSON.parse() 1,820ms 12MB 55 rec/ms
ndjson stream 1,240ms 10MB 81 rec/ms
// Fast JSONL streaming with ndjson
const fs = require('fs');
const ndjson = require('ndjson');

const stream = fs.createReadStream('data.jsonl')
  .pipe(ndjson.parse())
  .on('data', (record) => {
    // Process each record as it arrives
    process(record);
  })
  .on('end', () => {
    console.log('Complete');
  });

Go: Concurrency Benchmark

Test setup: 1,000,000 records, ~500MB file, Go 1.21, MacBook Pro M1, 10 cores

Approach Parse Time Throughput CPU Usage
Single-threaded 3,200ms 313 rec/ms ~100% (1 core)
10 goroutines 420ms 2,381 rec/ms ~900% (9 cores)

7.6x speedup with parallel processing. JSONL's line-based format is trivially parallelizable.

// Go: Parallel JSONL processing
package main

import (
    "bufio"
    "encoding/json"
    "os"
    "sync"
)

func processChunk(lines [][]byte, wg *sync.WaitGroup) {
    defer wg.Done()
    for _, line := range lines {
        var record map[string]interface{}
        json.Unmarshal(line, &record)
        // Process record...
    }
}

func main() {
    file, _ := os.Open("data.jsonl")
    defer file.Close()

    scanner := bufio.NewScanner(file)
    const chunkSize = 10000
    var chunk [][]byte
    var wg sync.WaitGroup

    for scanner.Scan() {
        chunk = append(chunk, append([]byte(nil), scanner.Bytes()...))

        if len(chunk) >= chunkSize {
            wg.Add(1)
            go processChunk(chunk, &wg)
            chunk = nil
        }
    }

    if len(chunk) > 0 {
        wg.Add(1)
        go processChunk(chunk, &wg)
    }

    wg.Wait()
}

Memory Efficiency

Memory Usage: JSON Array vs JSONL

Memory consumption for various file sizes (streaming JSONL vs loading JSON array):

File Size Records JSON Array Memory JSONL Memory Savings
10 MB 20,000 ~60 MB ~5 MB 92%
100 MB 200,000 ~550 MB ~8 MB 99%
1 GB 2,000,000 ~5.5 GB ~10 MB 99.8%
10 GB 20,000,000 ~55 GB (OOM) ~12 MB 99.98%

Why the difference?

  • JSON arrays load entire structure into memory (all records + array overhead)
  • JSONL streams one record at a time, discarding after processing
  • JSON parser allocates temporary objects during deserialization
  • Memory overhead typically 5-6x file size for JSON arrays

Real-World Memory Profiling

Measure memory usage in your own applications:

# Python: Profile memory usage
import tracemalloc
import json

tracemalloc.start()

# Your processing code here
with open('data.jsonl', 'r') as f:
    for line in f:
        obj = json.loads(line)
        process(obj)

current, peak = tracemalloc.get_traced_memory()
print(f"Current: {current / 1024 / 1024:.1f} MB")
print(f"Peak: {peak / 1024 / 1024:.1f} MB")
tracemalloc.stop()

Compression Benchmarks

Compression Ratios

JSONL compresses extremely well due to repeated field names. Test file: 100MB JSONL with typical user records.

Compression Compressed Size Ratio Compress Time Decompress Time
None 100.0 MB - - -
gzip -6 12.4 MB 87.6% 3.2s 0.8s
gzip -9 11.8 MB 88.2% 8.1s 0.8s
bzip2 8.9 MB 91.1% 12.4s 4.2s
xz 7.2 MB 92.8% 28.6s 1.9s
zstd -3 11.2 MB 88.8% 0.6s 0.3s

Recommendations:

  • gzip -6 - Best balance of speed and compression (default choice)
  • zstd - Best for real-time pipelines (5x faster than gzip)
  • xz - Best for long-term archival (smallest size)

Streaming Compressed JSONL

Process compressed JSONL without decompressing entire file first:

Python

import gzip
import json

# Stream-decompress and process
with gzip.open('data.jsonl.gz', 'rt') as f:
    for line in f:
        obj = json.loads(line)
        # Memory stays constant!

Node.js

const fs = require('fs');
const zlib = require('zlib');
const readline = require('readline');

const stream = fs.createReadStream('data.jsonl.gz')
  .pipe(zlib.createGunzip())
  .pipe(readline.createInterface({ input: process.stdin }));

for await (const line of stream) {
  const obj = JSON.parse(line);
  // Process...
}

Command Line

# Decompress and process on-the-fly with jq
zcat data.jsonl.gz | jq '.name'

# Decompress, filter, compress again
zcat input.jsonl.gz | grep '"status":"active"' | gzip > filtered.jsonl.gz

Performance impact: Decompression adds 40-60% overhead, but file I/O reduction often makes it faster overall, especially with SSDs.

Streaming Efficiency

Time to First Record

How quickly can you start processing data? Test: 1GB file with 2M records.

JSON Array

12.4 seconds

Must parse entire file before accessing first record

JSONL

<1 millisecond

First record available immediately after reading first line

12,000x faster startup! Critical for real-time processing and interactive applications.

Processing Throughput

Records processed per second across different languages and approaches:

Language Library Records/sec Notes
Python json (stdlib) 72,000 Baseline
Python orjson 244,000 3.4x faster
Node.js JSON.parse() 55,000 Single-threaded
Node.js ndjson 81,000 Optimized streaming
Go encoding/json 313,000 Single goroutine
Go encoding/json (parallel) 2,381,000 10 goroutines
Rust serde_json 520,000 Zero-copy parsing
Command line jq 45,000 General purpose

Network Streaming Performance

Benchmark: HTTP endpoint returning 100,000 records over network.

JSON Array

18.2s

Client waits for full response

JSONL Streaming

0.15s

Time to first record

Total Transfer

18.5s

But processing starts immediately

Key benefit: User sees results in 150ms instead of 18 seconds, even though total transfer time is similar. Perceived performance is 120x better.

Real-World Performance Scenarios

Scenario 1: Log Processing Pipeline

Task: Process 50GB daily application logs (25M records), extract errors, write to database

JSON Array Approach

  • Parse time: 8+ minutes
  • Memory required: ~280GB
  • Result: Out of memory crash

JSONL Approach

  • Parse time: 4.2 minutes (streaming)
  • Memory required: 50MB
  • Result: Successfully completed

With gzip compression, file size reduced to 6GB, processing time 6.5 minutes.

Scenario 2: ML Training Data Preparation

Task: Transform 10M training examples for GPT fine-tuning

In-Memory Processing

  • Load all data: 45GB RAM
  • Transform time: 12 minutes
  • Write output: 8 minutes
  • Total: 20 minutes

Streaming Pipeline

  • Memory usage: 200MB
  • Stream + transform: 9 minutes
  • Simultaneous write: Included
  • Total: 9 minutes

2.2x faster, 225x less memory. Can run on small instance instead of memory-optimized.

Scenario 3: Real-Time Analytics Dashboard

Task: Display live events in web dashboard as they arrive

Polling JSON Endpoint

  • Poll every 5 seconds
  • Return full array (growing)
  • Client re-downloads all data
  • Latency: 5+ seconds

JSONL Streaming

  • Server-sent events (JSONL)
  • Push events as they occur
  • Client processes incrementally
  • Latency: <100ms

50x lower latency, 90% less bandwidth usage.

Performance Optimization Tips

Do

  • Use fast JSON libraries (orjson, simdjson, jsoniter)
  • Stream large files instead of loading into memory
  • Compress with gzip or zstd for storage
  • Parallelize processing across multiple cores
  • Use buffered I/O with large buffer sizes (1MB+)
  • Filter before parsing when possible
  • Build offset indexes for random access
  • Partition large datasets by date/category

Don't

  • Load entire file into array before processing
  • Use unbuffered file I/O
  • Parse every line if you only need subset
  • Store uncompressed JSONL in production
  • Read compressed files multiple times (cache if needed)
  • Ignore memory profiling in production
  • Process multi-GB files on single thread
  • Use JSONL for tiny datasets (<1MB)