PHP Memory Management
PHPOne 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
| Situation | Best Approach |
|---|---|
| Process 1,000+ DB rows | Use chunks or generators |
| Read large files | Stream line by line with fgets() |
| Export large CSV | Stream directly to browser |
| Process images | Process one at a time, unset after |
| API with large response | Paginate the API |
| Send bulk emails | Queue 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.Test your knowledge!
Take a quiz to reinforce what you learned.