Skip to content
Back to Learn
·6 min read

JSON in Node.js: Streams and Buffers for Large Data

Node.js Streams and Buffers are essential for handling JSON data efficiently, especially for large files, network responses, and real-time data processing. Instead of loading entire JSON payloads into memory, streams process data in chunks, enabling backpressure-aware pipelines that handle gigabytes of data with minimal memory. This guide covers streaming JSON parsing, transform streams for JSON, buffer management, and real-world patterns. Use our JSON Formatter for small payloads and JSON Validator for streaming validation.

When to Use Streams for JSON

ScenarioMemory (Standard)Memory (Streaming)Why Streams Win
1GB JSON file2-4 GB RAM50-100 MB40x memory reduction
API response > 100MBOOM riskConstant memoryProcessable regardless of size
Real-time log processingMust buffer all dataPer-line processingNo buffering delay
Download + parse pipelineDownload, then parseParse while downloadingLower total latency

Streaming JSON Parse (NDJSON)

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

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

  return new Transform({
    readableObjectMode: true,
    writableObjectMode: false,
    async transform(chunk, encoding, callback) {
      // Not used directly - readline emits 'line' events
    }
  });
}

// Example: Process large NDJSON file
async function processLargeJSON(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({ input: fileStream });

  const pipeline = rl.pipe(new Transform({
    readableObjectMode: true,
    writableObjectMode: false,
    transform(line, encoding, callback) {
      if (line.trim()) {
        try {
          const record = JSON.parse(line);
          callback(null, record);
        } catch (err) {
          console.error('Invalid JSON line:', err.message);
          callback(); // Skip bad lines
        }
      } else {
        callback();
      }
    }
  }));

  for await (const record of pipeline) {
    await processRecord(record);
  }
}

Streaming JSON Parse (Array)

// For standard JSON arrays, use clarinet or jsonstream2
import parser from 'clarinet';

function streamJSONArray(readableStream) {
  const jsonParser = parser.createStream();

  return new Transform({
    readableObjectMode: true,
    transform(chunk, encoding, callback) {
      jsonParser.write(chunk.toString());
      callback();
    }
  });
}

// Alternative: jsonstream2 for array streams
const JSONStream = require('jsonstream2');
const stream = fs.createReadStream('large-array.json')
  .pipe(JSONStream.parse('*'))  // Parse each array element
  .on('data', (item) => {
    // Process one array element at a time
    processItem(item);
  })
  .on('end', () => console.log('Done'));

Transform Stream for JSON Processing

// Transform stream that modifies JSON records
class JSONTransformStream extends Transform {
  constructor(transformFn, options = {}) {
    super({
      readableObjectMode: true,
      writableObjectMode: true,
      ...options
    });
    this.transformFn = transformFn;
  }

  _transform(record, encoding, callback) {
    try {
      const transformed = this.transformFn(record);
      callback(null, transformed);
    } catch (err) {
      callback(err);
    }
  }
}

// Usage: Add timestamp to each record
const addTimestamp = new JSONTransformStream((record) => ({
  ...record,
  processedAt: new Date().toISOString()
}));

// Pipeline: read NDJSON -> transform -> write NDJSON
const { pipeline } = require('stream');
pipeline(
  fs.createReadStream('input.ndjson'),
  new NDJSONLineReader(),
  addTimestamp,
  new NDJSONLineWriter(),
  fs.createWriteStream('output.ndjson'),
  (err) => console.log('Pipeline complete:', err)
);

Buffer Management for JSON

// Handling incomplete JSON in stream buffers
class JSONBuffer extends Transform {
  constructor(options = {}) {
    super(options);
    this.buffer = '';
    this.objectMode = false;
  }

  _transform(chunk, encoding, callback) {
    this.buffer += chunk.toString();

    // Try to find complete JSON objects
    let depth = 0;
    let start = -1;
    let complete = false;

    for (let i = 0; i < this.buffer.length; i++) {
      const char = this.buffer[i];

      if (char === '{' || char === '[') {
        if (depth === 0) start = i;
        depth++;
      } else if (char === '}' || char === ']') {
        depth--;
        if (depth === 0 && start >= 0) {
          const jsonStr = this.buffer.substring(start, i + 1);
          this.push(jsonStr);
          this.buffer = this.buffer.substring(i + 1);
          i = -1; // Reset to process remaining buffer
          start = -1;
        }
      }
    }
    callback();
  }

  _flush(callback) {
    if (this.buffer.trim()) {
      this.push(this.buffer);
    }
    callback();
  }
}

// Usage
const buffer = new JSONBuffer({ readableObjectMode: true });
buffer.on('data', (jsonStr) => {
  const data = JSON.parse(jsonStr);
  console.log('Complete JSON:', data);
});

Memory-Efficient Patterns

  • Use Buffer.alloc() instead of new Buffer() for explicit memory allocation
  • Set highWaterMark on streams to control buffer sizes (default 16KB, increase for throughput)
  • Use pipeline() instead of .pipe() for automatic cleanup and error handling
  • For large JSON objects, consider MongoDB or PostgreSQL JSONB instead of in-memory processing
  • Use JSON Minifier to reduce file size before streaming
  • Validate JSON structure with JSON Validator before expensive streaming operations

Buffer Size Benchmark

Buffer SizeThroughput (1GB file)MemoryUse Case
16 KB (default)45s50 MBLow-memory environments
256 KB28s80 MBBalanced
1 MB22s150 MBHigh-throughput, plenty of RAM

Next Steps

Format small JSON with JSON Formatter. Validate with JSON Validator. Minify large files with JSON Minifier before streaming.