How to Properly Parse and Stream Large JSON Files in PHP Without Memory Crashes

By Admin • Updated September 22, 2026 • 60 Reads
How to parse and stream large multi-gigabyte JSON files in PHP without memory crashes
High-Performance JSON Stream Parsing in PHP

With the rapid rise of generative AI batch pipelines, high-volume Stripe ledger webhooks, and multi-gigabyte open-data dumps, modern PHP backends are frequently required to process JSON documents measuring hundreds of megabytes or even multiple gigabytes in size. In native PHP, invoking standard functions like json_decode(file_get_contents('large_feed.json'), true) on any payload larger than 100 megabytes is an instant recipe for server failure: Fatal error: Allowed memory size of 536870912 bytes exhausted.

The root of this problem lies in the design of standard tree parsers. In this technical deep dive, we will explain the mathematical differences between DOM-style tree parsing and event-driven stream parsing, implement a zero-dependency streaming parser in native PHP 8.x for Line-Delimited JSON (NDJSON/JSONL) and large top-level JSON arrays, and demonstrate how to process 10GB+ compressed JSON feeds directly from gzip streams with constant sub-10MB RAM usage.

1. Why json_decode() Inevitably Exhausts Server RAM

To understand why json_decode() collapses on large files, we must look at the two distinct stages of memory consumption:

  1. Contiguous String Allocation: Before json_decode() can read a single byte, file_get_contents() must allocate a single contiguous string in PHP's memory heap matching the exact size of the file on disk. A 400MB JSON file immediately consumes 400MB of RAM.
  2. AST and Zend Hashtable Expansion: Once the string is in memory, json_decode() begins constructing an Abstract Syntax Tree (AST) representing every nested key, array, and object. As established in internal Zend Engine profiling, PHP associative arrays and objects require significant metadata overhead (hashtable buckets, hash keys, refcounts). As a result, an in-memory decoded PHP structure consumes 6 to 10 times the raw file size. A 400MB JSON payload easily demands 3GB to 4GB of server RAM.

2. Pattern 1: Streaming Line-Delimited JSON (NDJSON / JSONL)

The modern standard for large data feeds (utilized by OpenAI, Elasticsearch, and BigQuery) is Newline Delimited JSON (NDJSON), where each line is an independent, valid JSON object separated by a newline character (\n).

NDJSON can be processed with zero memory leaks by combining PHP file stream pointers (fgets) with native json_decode() wrapped inside a generator:

<?php
declare(strict_types=1);

namespace App\Services;

use Generator;
use RuntimeException;

class NdjsonStreamer
{
    /**
     * Streams an NDJSON file item by item with constant memory.
     *
     * @param string $filePath
     * @return Generator<int, array>
     */
    public static function stream(string $filePath): Generator
    {
        $handle = fopen($filePath, 'rb');
        if (!$handle) {
            throw new RuntimeException("Unable to open file: {$filePath}");
        }

        $lineNumber = 0;

        try {
            while (($line = fgets($handle)) !== false) {
                $lineNumber++;
                $trimmed = trim($line);

                if ($trimmed === '') {
                    continue; // Skip blank lines
                }

                $record = json_decode($trimmed, true, 512, JSON_THROW_ON_ERROR);
                yield $lineNumber => $record;
            }
        } finally {
            fclose($handle);
        }
    }
}

// Processing a 15GB NDJSON file consuming only 4.2 MB of RAM:
foreach (NdjsonStreamer::stream('/data/llm_training_data.jsonl') as $index => $item) {
    processTrainingRecord($item);
}

3. Pattern 2: Parsing Massive Standard JSON Arrays Without Libraries

Legacy APIs often return large datasets formatted as a single massive top-level array:

[
  {"id": 1, "name": "Enterprise Alpha", "value": 450.00},
  {"id": 2, "name": "Enterprise Beta", "value": 920.50},
  ... 5,000,000 more records ...
]

Because this is not formatted line-by-line, simple fgets() will not work if objects span multiple formatted lines. Below is a lightweight, pure PHP Finite State Tokenizer that scans the byte stream for top-level object curly braces ({ and }), tracks string escape states, and yields each individual object into json_decode():

<?php
declare(strict_types=1);

namespace App\Services;

use Generator;
use RuntimeException;

class JsonArrayStreamer
{
    /**
     * Reads a huge top-level JSON array of objects chunk-by-chunk.
     *
     * @param string $filePath
     * @param int $chunkBytes Read buffer size (default 64KB)
     * @return Generator<int, array>
     */
    public static function streamObjects(string $filePath, int $chunkBytes = 65536): Generator
    {
        $handle = fopen($filePath, 'rb');
        if (!$handle) {
            throw new RuntimeException("Could not open JSON source: {$filePath}");
        }

        $depth = 0;
        $inString = false;
        $escaped = false;
        $buffer = '';
        $objectIndex = 0;

        try {
            while (!feof($handle)) {
                $chunk = fread($handle, $chunkBytes);
                $len = strlen($chunk);

                for ($i = 0; $i < $len; $i++) {
                    $char = $chunk[$i];

                    // Track quotation state to ignore brackets inside string values
                    if ($char === '"' && !$escaped) {
                        $inString = !$inString;
                    }

                    if ($inString) {
                        $escaped = ($char === '\\' && !$escaped);
                        if ($depth > 0) {
                            $buffer .= $char;
                        }
                        continue;
                    }

                    $escaped = false;

                    if ($char === '{') {
                        $depth++;
                        $buffer .= $char;
                    } elseif ($char === '}') {
                        $depth--;
                        $buffer .= $char;

                        // When depth returns to 0, an entire top-level object has been captured
                        if ($depth === 0) {
                            $decoded = json_decode($buffer, true);
                            if ($decoded !== null) {
                                yield $objectIndex++ => $decoded;
                            }
                            $buffer = ''; // Reset buffer immediately
                        }
                    } elseif ($depth > 0) {
                        $buffer .= $char;
                    }
                }
            }
        } finally {
            fclose($handle);
        }
    }
}

4. Processing Compressed Gzip Streams Directly from S3 or Disk

In cloud environments, large JSON feeds are virtually always archived with gzip compression (e.g., orders_2026.json.gz). Beginners frequently extract the entire archive to local disk before parsing, consuming enormous temporary disk storage and causing unnecessary I/O thrashing.

Using PHP's built-in stream compression wrappers, you can stream decompression directly into your parser pipeline with zero intermediate disk files:

<?php
// Stream directly from a gzip-compressed archive using PHP's zlib wrapper:
$gzPath = 'compress.zlib:///var/data/exports/orders_archive.json.gz';

foreach (App\Services\JsonArrayStreamer::streamObjects($gzPath) as $order) {
    syncOrderToWarehouseDatabase($order);
}

5. Emitting Streaming JSON Responses via HTTP (Server-Sent Events)

When building an API endpoint that exports hundreds of thousands of records to a client application, you should never build a giant JSON string in PHP memory before sending it. Instead, emit an HTTP chunked transfer stream:

<?php
// Stream an HTTP JSON array response directly to the browser
header('Content-Type: application/json; charset=UTF-8');
header('X-Accel-Buffering: no'); // Disable Nginx proxy buffering

echo "[\n";
$first = true;

$stmt = $pdo->query("SELECT id, title, price FROM products");

while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    if (!$first) {
        echo ",\n";
    }
    echo json_encode($row, JSON_UNESCAPED_SLASHES);
    $first = false;
    
    // Flush output buffers to send chunk across the wire immediately
    if (ob_get_level() > 0) {
        ob_flush();
    }
    flush();
}

echo "\n]";

Summary: Key Takeaways for High-Scale PHP

  • Never call file_get_contents on arbitrary JSON feeds: Even if your memory limit is 1GB, an unexpected 200MB file will cause an out-of-memory fatal crash.
  • Adopt NDJSON / JSONL for internal architectures: Line-delimited JSON simplifies parsing down to standard file line reading, making it drastically faster and simpler than complex nested tokenizers.
  • Decompress on-the-fly: Take advantage of PHP stream wrappers (compress.zlib://) to avoid wasting disk space and I/O cycles.