Lessons → Performance → PHP Memory Management

PHP Memory Management

PHP
⏱ 22 min read📖 IntermediateNot completed

One of the most common errors in PHP when working with large datasets is the dreaded "Allowed memory size exhausted" error. In this lesson you'll learn why it happens and how to fix it properly.

Why Does PHP Run Out of Memory?

PHP loads everything into RAM when you fetch data. If you fetch 100,000 database rows at once, all of them sit in memory simultaneously.

The Problem
// Default PHP memory limit: 128MB
// Each database row takes ~1-10KB in memory

// 10,000 rows × 5KB = ~50MB  ← okay
// 100,000 rows × 5KB = ~500MB ← CRASH! Memory exceeded
// 1,000,000 rows × 5KB = ~5GB ← Never do this!

Check Current Memory Usage

PHP
<?php
// Check memory usage
echo memory_get_usage();                    // Current usage in bytes
echo memory_get_usage(true);               // Real usage in bytes
echo memory_get_peak_usage();              // Peak usage in bytes

// Format nicely
function formatBytes(int $bytes): string {
    return round($bytes / 1024 / 1024, 2) . ' MB';
}

echo formatBytes(memory_get_usage());      // e.g. 12.5 MB
echo formatBytes(memory_get_peak_usage()); // e.g. 45.2 MB

// Check memory limit
echo ini_get('memory_limit');              // e.g. 128M
?>

Option 1: Increase Memory Limit (Quick Fix — Not Recommended)

PHP
<?php
// Temporarily increase memory limit (not a real solution!)
ini_set('memory_limit', '512M');
ini_set('memory_limit', '-1'); // Unlimited — DANGEROUS!

// Or in php.ini:
// memory_limit = 256M

// Why this is bad:
// - Hides the real problem
// - Server can crash if many users hit the same page
// - Not scalable
?>
⚠️
Never use memory_limit = -1 in production! Unlimited memory can crash your entire server if multiple users trigger memory-heavy operations simultaneously.

Option 2: Process Data in Chunks (Correct Way)

Instead of loading all data at once, process it in small batches:

PHP — Manual Chunking
<?php
// BAD — loads ALL users into memory at once
$users = $pdo->query("SELECT * FROM users")->fetchAll();
foreach ($users as $user) {
    sendEmail($user); // 100,000 emails = memory crash!
}

// GOOD — process in chunks of 1000
$offset = 0;
$limit  = 1000;

while (true) {
    $stmt = $pdo->prepare("SELECT * FROM users LIMIT ? OFFSET ?");
    $stmt->execute([$limit, $offset]);
    $users = $stmt->fetchAll(PDO::FETCH_ASSOC);

    if (empty($users)) break; // No more records

    foreach ($users as $user) {
        sendEmail($user);
    }

    $offset += $limit;

    // Free memory after each chunk
    unset($users);
    gc_collect_cycles(); // Force garbage collection
}

echo "Done! Processed all users.";
?>

Option 3: Use Generators (Most Memory Efficient)

Generators process one row at a time — only ONE record in memory at any moment:

PHP — Generator
<?php
// Generator function — uses yield instead of return
function getAllUsers(PDO $pdo): Generator {
    $stmt = $pdo->query("SELECT * FROM users");

    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        yield $row; // Returns ONE row at a time, pauses execution
    }
}

// Usage — only 1 row in memory at a time!
foreach (getAllUsers($pdo) as $user) {
    sendEmail($user);
    // After this loop iteration, $user is discarded from memory
}

// Memory comparison:
// fetchAll()  = ALL rows in memory at once (e.g. 500MB)
// Generator   = 1 row in memory at a time  (e.g. 2KB)
?>

Option 4: Stream Large Files

PHP — Stream CSV
<?php
// BAD — loads entire file into memory
$content = file_get_contents('large_file.csv'); // 500MB file = crash!

// GOOD — stream line by line
$handle = fopen('large_file.csv', 'r');

if ($handle) {
    $lineNumber = 0;
    while (($line = fgets($handle)) !== false) {
        $lineNumber++;

        // Process one line at a time
        $data = str_getcsv($line);
        processRow($data);

        // Free memory every 1000 lines
        if ($lineNumber % 1000 === 0) {
            gc_collect_cycles();
        }
    }
    fclose($handle);
}

echo "Processed $lineNumber lines";
?>

Option 5: Export Large Data as CSV Download

PHP — Stream CSV to Browser
<?php
// Stream large CSV directly to browser without loading all into memory
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="users.csv"');

$output = fopen('php://output', 'w');

// Write header row
fputcsv($output, ['ID', 'Name', 'Email', 'Created At']);

// Stream data in chunks
$offset = 0;
$limit  = 1000;

while (true) {
    $stmt = $pdo->prepare("SELECT id, name, email, created_at FROM users LIMIT ? OFFSET ?");
    $stmt->execute([$limit, $offset]);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

    if (empty($rows)) break;

    foreach ($rows as $row) {
        fputcsv($output, $row);
    }

    $offset += $limit;
    unset($rows);

    // Flush output buffer to browser
    ob_flush();
    flush();
}

fclose($output);
?>

Memory Best Practices

SituationBest Approach
Process 1,000+ DB rowsUse chunks or generators
Read large filesStream line by line with fgets()
Export large CSVStream directly to browser
Process imagesProcess one at a time, unset after
API with large responsePaginate the API
Send bulk emailsQueue jobs, process in background

Free Memory Manually

PHP
<?php
$data = getLargeDataset(); // 50MB of data

// Process data...
processData($data);

// Free memory immediately when done
unset($data);              // Remove variable
gc_collect_cycles();       // Force garbage collection

echo formatBytes(memory_get_usage()); // Memory drops back down
?>
💡
This is a real problem you'll face! Any time you process more than 10,000 database rows, exports, or imports in PHP — always use chunking or generators. Never fetch everything at once.
💡
In Laravel there are even better tools! chunk(), cursor(), lazy() — see the next lesson for Laravel-specific solutions that are even easier to use.
← All Lessons Next Lesson →
🧠

Test your knowledge!

Take a quiz to reinforce what you learned.

Take Quiz →