Skip to content
Back to Learn
·5 min read

JSON Streaming with NDJSON: Newline-Delimited JSON for Big Data

NDJSON (Newline Delimited JSON) and streaming JSON are essential techniques for handling large datasets that would otherwise consume too much memory. Instead of loading an entire JSON document into memory, streaming processes records one at a time. This guide covers NDJSON format, JSON streaming parsers, real-world use cases, and implementation in multiple languages. Use our JSON Formatter to inspect NDJSON samples and JSON Validator to validate individual records.

What is NDJSON?

NDJSON (also called JSON Lines or newline-delimited JSON) is a variant where each line is a separate, complete JSON object. Multiple JSON objects are delimited by newline characters. Unlike a JSON array ([{...}, {...}]), NDJSON can be processed line-by-line without loading the entire dataset.

// NDJSON format: one JSON object per line
{"id": 1, "name": "Alice", "timestamp": "2025-01-15T10:00:00Z"}
{"id": 2, "name": "Bob", "timestamp": "2025-01-15T10:01:00Z"}
{"id": 3, "name": "Charlie", "timestamp": "2025-01-15T10:02:00Z"}

// Equivalent JSON array (requires loading all objects at once)
[
  {"id": 1, "name": "Alice", "timestamp": "2025-01-15T10:00:00Z"},
  {"id": 2, "name": "Bob", "timestamp": "2025-01-15T10:01:00Z"},
  {"id": 3, "name": "Charlie", "timestamp": "2025-01-15T10:02:00Z"}
]

JSON Array vs NDJSON Comparison

FeatureJSON ArrayNDJSON
Memory usageEntire file loaded at onceOne record at a time (constant)
ParsingSingle JSON.parse() callLine-by-line parsing
Append dataMust re-write entire arraySimple append: add new line
Partial readingFirst byte to last byte neededRead from any offset
File size overheadSquare brackets + commasOne newline per record
Error resilienceOne bad byte corrupts entire arrayBad lines can be skipped
Streaming supportComplex (must track array state)Natural (line-based)
Compression ratioSlightly better (shared structure)Slightly worse (per-line overhead)

Streaming NDJSON in Node.js

const fs = require('fs');
const readline = require('readline');
const { Transform } = require('stream');

// Stream NDJSON file line by line
async function processNDJSON(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  let lineCount = 0;
  let errorCount = 0;

  for await (const line of rl) {
    if (line.trim() === '') continue; // Skip empty lines

    try {
      const record = JSON.parse(line);
      // Process one record at a time
      await processRecord(record);
      lineCount++;
    } catch (error) {
      errorCount++;
      console.error('Error parsing line ' + (lineCount + 1) + ':', error.message);
      lineCount++;
    }
  }

  console.log('Processed ' + lineCount + ' records with ' + errorCount + ' errors');
}

// Transform stream for NDJSON
class NDJSONTransform extends Transform {
  constructor() {
    super({ readableObjectMode: true });
    this.buffer = '';
  }

  _transform(chunk, encoding, callback) {
    this.buffer += chunk.toString();
    const lines = this.buffer.split('
');
    this.buffer = lines.pop() || ''; // Keep incomplete line

    for (const line of lines) {
      if (line.trim()) {
        try {
          this.push(JSON.parse(line));
        } catch (e) {
          this.emit('parse-error', { line, error: e });
        }
      }
    }
    callback();
  }

  _flush(callback) {
    if (this.buffer.trim()) {
      try {
        this.push(JSON.parse(this.buffer));
      } catch (e) {
        this.emit('parse-error', { line: this.buffer, error: e });
      }
    }
    callback();
  }
}

NDJSON in Python

import json

def read_ndjson(file_path):
    with open(file_path, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                yield json.loads(line)
            except json.JSONDecodeError as e:
                print(f"Error parsing line: {e}")
                continue

def write_ndjson(records, file_path):
    with open(file_path, 'w', encoding='utf-8') as f:
        for record in records:
            f.write(json.dumps(record, ensure_ascii=False) + '
')

# Usage
for record in read_ndjson('large_dataset.ndjson'):
    process(record)

NDJSON Production Use Cases

Use CaseExampleWhy NDJSON
Log aggregationSplunk, ELK stack, DatadogLogs arrive one at a time, must be appendable
Data exportDatabase dumps, SaaS exportsMulti-GB datasets need streaming
Real-time analyticsClickstream data, sensor readingsRecords arrive continuously, no batching
ETL pipelinesData warehouse ingestionEach record can be transformed independently
Machine learningTraining data feedsProcess shards in parallel, skip bad records
API streamingTwitter API, GitHub eventsEvents arrive as stream of JSON objects

NDJSON vs JSONL vs JSON Lines

The terms are often used interchangeably, but there are subtle differences:

  • NDJSON — Newline Delimited JSON, the formal specification
  • JSON Lines — Popularized by the jsonlines.org specification
  • JSONL — Common file extension (.jsonl) for NDJSON files

All three follow the same principle: one JSON value per line, with a newline character as delimiter.

Best Practices for NDJSON

  • Always handle parse errors gracefully — skip bad lines, log them, continue
  • Use CR-LF compatible readers for cross-platform compatibility
  • Validate NDJSON with JSON Validator during development
  • For compression, gzip NDJSON files — gzip compresses repeated structures well
  • Include a trailing newline on the last line for compatibility
  • Sort or partition NDJSON files for efficient parallel processing
  • Use NDJSON instead of JSON arrays for files larger than 100MB

Next Steps

Format individual NDJSON records with JSON Formatter. Validate records with JSON Validator. Compress NDJSON files with JSON Compress.