Why does a website crash while processing a PDF? Why can PHP report a memory error even when the server still has free RAM? Can Rust or Go completely prevent memory problems?
This guide explains out-of-memory errors, also called OOM errors, in simple words. You will learn how different languages respond, what visitors may experience, and how to reduce failures in web applications.
Examples use illustrative sizes. Error messages vary by runtime and environment. Outputs labeled “possible” are examples, not results from executing the code. Deliberately exhausting memory belongs in an isolated test environment.
1. What Is an Out-of-Memory Error?
RAM is the temporary workspace used by a running program. Imagine a desk where you place documents while working. If you keep adding documents without removing any, the desk eventually fills up.
A program faces a similar problem when it cannot obtain the memory needed for its next operation.
For example:
- A PDF worker collects the text of thousands of pages.
- A web request loads a large CSV file into one string.
- An API keeps every previous request in an unlimited list.
- Many image-processing jobs run at the same time.
Disk storage and RAM are different. Having 500 GB of free disk space does not mean an application has enough RAM to process a large file.
Three different limits to understand
| Limit | Example | What it means |
|---|---|---|
| Runtime or script limit | PHP memory_limit = 128M
|
PHP restricts memory allocated through its tracked allocator for the script |
| Container or service limit | A worker container has a 512 MiB hard limit | The process can be killed if its workload exceeds the enforced boundary |
| Machine resources | Several services compete for the server's available memory | System-wide memory pressure can slow services or cause processes to be killed |
A process can hit one limit before reaching another. Allocation can also fail because of address-space restrictions or other allocator constraints.
2. PHP vs Python vs Node.js vs Rust vs Go: Quick Comparison
| Language | Typical response to allocation failure or its memory limit | Can ordinary application handling recover? |
|---|---|---|
| PHP | Memory-limit fatal error; some failures can terminate a worker | Do not rely on ordinary try/catch for memory-limit exhaustion |
| Python | May raise MemoryError; process may instead be killed externally |
Sometimes, if an exception is raised and enough resources remain |
| Node.js | V8 heap exhaustion can terminate the process | Ordinary JavaScript try/catch does not recover a fatal V8 OOM |
| Rust | Ordinary allocation failure commonly aborts; fallible allocation APIs can return errors | Yes for a reported failure from an API such as try_reserve
|
| Go | Runtime allocation failure can be fatal | Ordinary recover() does not recover a fatal runtime OOM |
Any of these processes can also be killed by the operating system. Application exception handling cannot intercept a forced process kill.
The official runtime references linked at the end explain these behaviors in more detail.
3. PHP Memory Limit Exceeded: Example and Explanation
PHP's memory_limit controls the amount of memory a script may allocate through PHP's tracked allocator. It is not a complete hard limit on every byte of a worker's total operating-system memory usage.
Scenario A: The server has RAM, but the PHP request exceeds its limit
Suppose the server has 8 GB RAM, but the request has:
memory_limit = 128M
The request attempts to read a 200 MB file:
<?php
// Demonstrates a risky whole-file read for a large file.
$data = file_get_contents('large-report.csv');
echo 'File loaded';
Possible output:
Fatal error: Allowed memory size of 134217728 bytes exhausted ...
Meaning: “This request exceeded its PHP memory budget.”
The request may fail even though the server still has free RAM. In a typical PHP-FPM or Apache PHP deployment, one failed request does not necessarily stop every other worker. A fatal request error and a worker process crash are different events.
A visitor might see a generic server error. The exact HTTP response depends on the application, web server, and whether response data was already sent.
Scenario B: Process a CSV one row at a time
Instead of loading the complete CSV into a string:
<?php
$file = fopen('large-report.csv', 'r');
if ($file === false) {
exit('Cannot open file');
}
$count = 0;
while (($row = fgetcsv($file)) !== false) {
// Validate and process this row here.
// Avoid collecting every row in another array.
$count++;
}
fclose($file);
echo "Processed $count rows";
Example output for a file containing 50,000 CSV records:
Processed 50000 rows
Why it helps: Memory is reused while processing records. However, one exceptionally large record can still require substantial memory, so record-size limits also matter.
Scenario C: A Laravel query loads too many records
Risky for a large table:
$users = User::all();
This loads the matching models into memory together.
For a job that only reads users, process bounded batches instead:
$count = 0;
User::select('id', 'email')->chunkById(500, function ($users) use (&$count) {
foreach ($users as $user) {
// Process this user without retaining all batches.
$count++;
}
});
echo "Processed $count users";
Example output:
Processed 25000 users
The batch size of 500 is an example, not a universal recommendation. Measure with your model sizes and workload. This example requires an existing Laravel application and its User model.
4. Python MemoryError: Example and Explanation
Python may raise MemoryError when an allocation fails. Recovery is sometimes possible, but it is not guaranteed.
Scenario A: Understand the exception without exhausting RAM
This example deliberately simulates the exception:
try:
raise MemoryError("Simulated allocation failure")
except MemoryError:
print("Not enough memory to finish this task")
Expected output:
Not enough memory to finish this task
This tests the handler, not real memory exhaustion. Under a real OOM, even logging or building an error response may need memory that is unavailable.
Scenario B: Count lines without reading the entire file
Risky for a very large file:
with open("large-log.txt", encoding="utf-8") as file:
lines = file.readlines()
print(len(lines))
A streaming alternative:
count = 0
with open("large-log.txt", encoding="utf-8") as file:
for line in file:
count += 1
print(f"Processed {count} lines")
Example output:
Processed 100000 lines
Why it helps: The program does not retain every line together. As with CSV records, one extremely long line still needs a limit or a chunk-based reader.
Scenario C: A PDF expands during processing
A 10 MB PDF does not guarantee a 10 MB memory requirement. Parsing objects, decoding images, extracting text, and making copies can use much more memory.
Even a page-by-page loop may leave parser data or caches in memory. For untrusted PDFs, combine input limits, page limits, and an isolated worker with memory and time limits.
If the operating system kills that worker, Python cannot execute its except MemoryError handler. The API should detect the failed job and report a controlled failure.
5. Node.js JavaScript Heap Out of Memory
Node.js uses V8 to run JavaScript. Many JavaScript objects live in an area called the heap.
Scenario A: An unlimited request history
This Express route retains every submitted body:
const savedRequests = [];
app.post('/save', (req, res) => {
savedRequests.push(req.body);
res.send('Saved');
});
Here, app represents an already configured Express application with body parsing. Express is an external dependency; the following cache and file examples use Node.js built-ins only.
As the retained collection grows, a possible failure is:
FATAL ERROR: Reached heap limit
Allocation failed - JavaScript heap out of memory
Meaning: “V8 cannot allocate enough heap space for the objects still being retained.”
The process can terminate, interrupting requests served by that instance.
Scenario B: Bound a small cache
const cache = new Map();
const MAX_ENTRIES = 100;
function remember(key, value) {
if (!cache.has(key) && cache.size >= MAX_ENTRIES) {
const oldestKey = cache.keys().next().value;
cache.delete(oldestKey);
}
cache.set(key, value);
}
for (let i = 1; i <= 150; i++) {
remember(`item-${i}`, 'small value');
}
console.log(cache.size);
Expected output:
100
Why it helps: The number of retained entries is bounded. This is a simple insertion-order cache, not a full LRU cache. Also limit value size: 100 enormous values can still exhaust memory.
Scenario C: Copy a file using streams
const fs = require('node:fs');
const { pipeline } = require('node:stream/promises');
async function main() {
await pipeline(
fs.createReadStream('large-report.csv'),
fs.createWriteStream('report-copy.csv')
);
console.log('Copy completed');
}
main().catch((error) => {
console.error('Copy failed:', error.message);
process.exitCode = 1;
});
Expected output when the source exists and the destination is writable:
Copy completed
The pipeline transfers data progressively and manages backpressure, so a slow destination does not require buffering the complete file.
Does increasing the Node.js heap solve the problem?
An example setting is:
node --max-old-space-size=512 app.js
This sets V8's old-generation heap limit in MiB. It does not cap the process's total RAM usage. Buffers, native libraries, and other allocations also consume memory.
Increasing a heap limit can help a legitimate workload fit, but an unlimited cache will continue growing. Choose limits from measured requirements and available capacity.
6. Rust Allocation Failure and try_reserve
Rust helps prevent many memory access errors in safe code. That does not give a program unlimited RAM.
With ordinary allocation failure, a typical standard-library Rust binary prints an error and aborts. The official handle_alloc_error documentation describes the default behavior and configuration differences.
Scenario A: Reserve memory with an API that can return an error
fn main() {
let mut data: Vec<u8> = Vec::new();
match data.try_reserve(1_000_000) {
Ok(()) => println!("Capacity reserved"),
Err(error) => println!("Cannot reserve capacity: {}", error),
}
}
Possible successful output:
Capacity reserved
A reported allocation failure or capacity overflow takes the error branch. Exact error text depends on the cause.
Meaning: “Ask for capacity using an operation that reports certain failures instead of automatically stopping the program.”
Successful reservation does not protect against an external OS kill or guarantee that later unrelated allocations succeed.
Scenario B: Reject excessive work before allocating
fn main() {
let requested_bytes: usize = 20 * 1024 * 1024;
let allowed_bytes: usize = 5 * 1024 * 1024;
if requested_bytes > allowed_bytes {
println!("Rejected: request exceeds the 5 MiB limit");
return;
}
println!("Request accepted");
}
Expected output:
Rejected: request exceeds the 5 MiB limit
This prevents this particular oversized allocation from being attempted. In a real upload handler, enforce the byte count while reading too; do not trust a client-supplied size alone.
7. Go Out-of-Memory Errors and Garbage Collection
Go's garbage collector reclaims memory occupied by objects that are no longer reachable. It cannot discard objects your program still retains.
Scenario A: A growing collection remains reachable
var history [][]byte
func remember(data []byte) {
history = append(history, data)
}
If this retains more data forever, the garbage collector cannot remove the referenced buffers. Go may eventually terminate with an error such as:
fatal error: runtime: out of memory
If the OS kills the process first, this runtime message might never appear.
Ordinary recover() handles certain panics; it does not make fatal runtime OOM recoverable.
Scenario B: Stream a file using a fixed transfer buffer
package main
import (
"fmt"
"io"
"os"
)
func copyFile() error {
source, err := os.Open("large-report.csv")
if err != nil {
return err
}
defer source.Close()
target, err := os.Create("report-copy.csv")
if err != nil {
return err
}
buffer := make([]byte, 32*1024)
_, copyErr := io.CopyBuffer(target, source, buffer)
closeErr := target.Close()
if copyErr != nil {
return copyErr
}
return closeErr
}
func main() {
if err := copyFile(); err != nil {
fmt.Println("Copy failed:", err)
os.Exit(1)
}
fmt.Println("Copy completed")
}
Expected successful output:
Copy completed
io.CopyBuffer streams the copy. It can use optimized reader or writer methods instead of the supplied buffer. Either way, this example avoids explicitly reading the whole file into a byte slice.
Scenario C: A Go memory setting is not a hard container boundary
Go's GOMEMLIMIT is a soft runtime memory limit that guides garbage collection. It is not an absolute cap on all process memory and cannot guarantee that a container will never be killed.
The same general rule applies across languages: understand whether a setting controls a heap, tracked allocations, runtime-managed memory, or the whole container.
8. More Real-World Memory Failure Scenarios
Scenario 1: Too many jobs run together
Suppose one PDF job peaks at about 300 MiB:
1 job ≈ 300 MiB
5 jobs ≈ 1,500 MiB
20 jobs ≈ 6,000 MiB
These estimates exclude databases, web servers, the OS, and other overhead.
What changes: A job that succeeds alone can fail during peak traffic.
Practical response: Limit simultaneous jobs and queue the rest. For example, two active workers and 18 waiting jobs may be safer than 20 active jobs, provided measured peaks fit the budget.
Scenario 2: A small image becomes huge in memory
Consider a 10,000 × 10,000 image decoded into four bytes per pixel:
10,000 × 10,000 × 4 = 400,000,000 bytes
Approximately 381 MiB for one pixel buffer
Resizing or converting may need additional buffers.
Practical response: Limit width, height, total pixels, output dimensions, and processing time. Checking compressed file size alone is insufficient.
Scenario 3: A container is killed despite free host RAM
Host RAM: 16 GiB
Worker container hard limit: 512 MiB
Workload attempts to exceed the container's permitted memory
The process can be killed because its own boundary is reached. It does not automatically gain access to all 16 GiB.
Practical response: Check container memory events and measured worker peaks before changing limits or concurrency.
Scenario 4: Restarts hide an unlimited cache
Service starts → memory grows → service fails → supervisor restarts it
Restarting restores availability temporarily. If the collection keeps growing, the failure returns.
Practical response: Find what remains retained, add a cache budget, and monitor whether memory reaches a stable level under sustained load.
9. Memory Exhaustion vs Memory Corruption vs Code Execution
These are different problems:
| Term | Simple explanation | Example |
|---|---|---|
| Memory exhaustion | The program cannot obtain more memory | An unlimited cache fills the heap |
| Memory corruption | A bug accesses or modifies memory incorrectly | Native code uses memory after it has been freed |
| Arbitrary code execution | An exploitable vulnerability allows attacker-controlled instructions to run | A specific memory bug is successfully exploited |
An OOM error does not automatically mean the server was hacked. Ordinary memory exhaustion can cause a failed request, a failed job, or service interruption.
An attacker can deliberately create expensive work to reduce availability. Turning a memory failure into code execution requires an additional exploitable vulnerability; it is not an automatic feature of PHP, Python, or another language.
Patching runtime vulnerabilities and controlling resource consumption address different problems. Both matter.
10. Seven Practical Ways to Prevent Memory Failures
1. Patch the runtime, extensions, and dependencies
Update the runtime that actually serves the website. CLI and web PHP may use different binaries and configuration files.
Example inspection commands:
php -v
php --ini
/opt/lampp/bin/php -v
These inspect CLI installations only. Confirm the web-serving runtime separately through your server configuration or a temporary authenticated diagnostic. Remove diagnostics afterward.
For Node.js and Python, also identify native addons or extensions used by image and document processing. Dependency updates and runtime updates are separate tasks.
2. Limit input before expensive processing
Set appropriate request-body, upload, record-count, and processing-time limits.
Example policy:
Maximum upload: 10 MiB
Maximum PDF pages: 100
Maximum image pixels: 20 million
Maximum active document jobs: 2
These numbers are examples. A page-count check may itself require parsing, so perform risky inspection inside the constrained worker too.
A controlled oversized-request response might be:
HTTP 413 Content Too Large
Please upload a smaller file.
3. Stream data and use bounded database batches
Use CSV row readers, file streams, and database batches. Do not accumulate the processed output into another unlimited collection.
Example: writing each extracted text section to a file can use less memory than building one enormous joined string. Whether a PDF library itself streams effectively must be checked separately.
4. Isolate PDF and image processing
Have the API accept the job and pass it to a dedicated worker process or container with memory and time limits.
Example application behavior:
Upload accepted → job queued → worker processes document
Success → result available
Failure → job marked failed with a controlled message
A worker failure should not require terminating the API process. Separate workers still share infrastructure unless their resources are properly constrained.
5. Run with limited permissions
Give workers access only to necessary input, output, and temporary directories. Use service-specific credentials.
Example: a PDF worker needs to read its assigned upload and write extracted text. It generally does not need permission to modify application source files.
This limits damage from a separate vulnerability; it does not create more RAM.
6. Monitor memory and restart workers carefully
Track worker memory peaks, queue length, failed jobs, exit reasons, and restart frequency.
Useful Linux inspection commands include:
free -h
ps -eo pid,comm,rss --sort=-rss
journalctl -k --since '1 hour ago'
On Linux, rss is reported in KiB here. Kernel logs may reveal OOM kills; access and logging availability vary. A high RSS value alone does not prove a leak.
Restart failed workers with backoff. Avoid repeatedly retrying the same document without a retry limit.
7. Test oversized and malformed inputs in staging
Check large files, very long CSV records, high-resolution images, malformed documents, and concurrent jobs under realistic resource limits.
A useful result looks like this:
Oversized upload → rejected before processing
Worker memory limit reached → failed job recorded
Malformed document → controlled failure
Other API requests → continue within expected performance targets
Measure those outcomes. Do not assume isolation guarantees zero impact on shared CPU, disk, or memory bandwidth.
11. Example Memory Budget for a Small Server
Suppose a server has 4 GiB RAM. An illustrative planning budget is:
| Component | Planning allowance |
|---|---|
| Operating system and background services | 700 MiB |
| Web/API processes | 600 MiB |
| Database | 1,000 MiB |
| Two document workers at 400 MiB each | 800 MiB |
| Remaining reserve | Approximately 996 MiB |
These are planning estimates, not automatic reservations or recommended settings for every server. Filesystem cache, native allocations, burst traffic, and worker overhead can change actual consumption.
Use measured peaks and leave room for bursts. Raising every service's limit independently can make the combined workload exceed the server's capacity.
12. Frequently Asked Questions
Can PHP run out of memory while the server has free RAM?
Yes. PHP can reach its configured script memory limit before the machine runs out of available memory.
Does Python always recover from MemoryError?
No. Recovery depends on the failure and remaining resources. The OS can also kill the process without raising a Python exception.
Can Node.js try/catch recover from fatal heap exhaustion?
Ordinary JavaScript exception handling does not recover a fatal V8 heap OOM. Prevent unbounded retention and isolate expensive work.
Does Rust prevent out-of-memory errors?
No. Rust's memory safety features address many invalid memory accesses. Capacity remains finite. Fallible allocation APIs can help handle specific allocation failures.
Does Go garbage collection guarantee enough memory?
No. Objects that are still reachable remain live, and the workload can exceed available memory even with garbage collection.
Will increasing RAM fix every memory problem?
No. More RAM can support a larger legitimate workload, but unlimited data retention continues growing. Diagnose why memory grows and whether it eventually stabilizes.
Does an out-of-memory error prove a security breach?
No. It can result from normal workload growth, a programming mistake, or deliberate resource exhaustion. Code execution requires a separate exploitable vulnerability.
Top comments (0)