PHP File Handling — Missing fclose() Kills Batch Job
Avoid silent failure: unclosed file handles in PHP hit ulimit after ~800 files, stopping batch jobs.
- fopen() returns a handle — check it before use
- Mode 'w' truncates instantly; 'a' appends without destroying
- fgets() streams line-by-line; file_get_contents() loads everything
- Always close handles or risk exhausting file descriptors (ulimit -n 1024)
- Use LOCK_EX with file_put_contents() for concurrent write safety
- file_exists() and is_readable() prevent silent failures on permission issues
Think of a PHP file like a physical notebook sitting on your desk. To use it, you pick it up (open), you read the pages or scribble on them (read/write), then you put it back down (close). If you forget to put it back, someone else can't use it — and eventually your desk gets cluttered. PHP file handling is exactly that simple: it's just telling your program how to grab a file, do something useful with it, and put it back neatly.
Every web application eventually needs to persist data somewhere. While databases handle structured records beautifully, there are plenty of real-world jobs that plain files do better — logging errors, storing configuration values, generating reports, processing CSV uploads, or caching API responses. Knowing how to handle files correctly in PHP is not optional knowledge; it's a fundamental skill that separates developers who ship reliable software from those who ship mysterious bugs.
The problem most tutorials ignore is the gap between 'it works on my machine' and 'it works safely in production.' PHP gives you raw, powerful file functions — but with that power comes responsibility. An unclosed file handle can silently corrupt data. Writing without checking permissions can crash a script with no helpful error message. Reading a multi-gigabyte log file into a variable can kill your server's memory in seconds. These aren't edge cases; they're Monday-morning incidents.
By the end of this article you'll be able to open files with the right mode for the job, read them line-by-line without memory issues, write and append data safely, check for errors before they bite you, and understand exactly why every step exists — not just how to type it.
Opening and Closing Files — The Contract You Must Always Honor
Every file operation in PHP starts with fopen(). It takes two arguments: the file path and a mode string. The mode tells PHP what you intend to do with the file — and the operating system uses that to decide what access to grant. This isn't just ceremony. If you open a file in read-only mode ('r'), PHP won't let you accidentally write to it, protecting data integrity. If you open with 'w', PHP truncates the file immediately — before you write a single byte. That behaviour surprises a lot of developers.
The return value of fopen() is a file handle — a reference your script holds while the file is open. Think of it as a library card: while you hold it, you're borrowing the file. fclose() returns the card. On most Unix systems, a single process has a limited number of file descriptors it can hold open simultaneously (often 1024). Scripts that open handles in loops without closing them quietly exhaust that limit, causing fopen() to return false with no obvious reason why.
Always pair fopen() with fclose(), and always check that fopen() didn't return false before you try to use the handle. The few lines of error-checking feel tedious until the day they save a production server.
fopen() is called — even if you never call fwrite(). If you want to add to an existing file without destroying it, always use 'a' (append) instead.fopen() calls returned false silently, and the job wrote empty output.fclose() pair is mandatory.fclose() in a loop will crash your script with no helpful error.Reading Files the Right Way — Line-by-Line vs. All-at-Once
PHP gives you two main approaches to reading files, and choosing the wrong one is the most common performance mistake in file handling. file_get_contents() loads the entire file into a single string in one call. It's perfect for small configuration files, templates, or JSON files you know are under a few megabytes. It's one of the most readable functions in PHP and should be your first choice when the file is small and you need all the content.
For larger files — log files, CSV exports, data feeds — you need fgets(). It reads one line at a time, keeping memory usage flat no matter how big the file is. Your script uses roughly the same amount of RAM reading a 1 KB file as it does reading a 500 MB file, because it only ever holds one line in memory at once. This is called streaming, and it's the industrial-strength pattern.
feof() tells you whether the file pointer has reached the end of the file. You use it as the condition in a while loop: keep reading lines until we hit the end. It sounds simple, but there's a subtle gotcha: feof() only becomes true after a read attempt fails, not before. So the last iteration of a naive loop can return an empty string — something the code below handles cleanly.
file_get_contents() because it loads everything at once.file_get_contents() on a 2 GB production log caused the PHP process to consume 2+ GB RAM, triggering OOM kills on a small server.fgets() loop. Memory dropped to < 1 MB.filesize() > 1 MB, stream with fgets() — never load whole file.filesize() before deciding which method to use.Writing and Appending — Choosing the Right Mode for the Job
The difference between writing and appending is one character in your fopen() call, but the consequences of mixing them up are dramatic. Write mode ('w') is a wrecking ball: it empties the file first, then lets you write from a clean slate. Append mode ('a') is a pen: it finds the end of whatever is already there and keeps adding. Using 'w' when you meant 'a' — on a production log file, for example — silently destroys weeks of diagnostic data.
For writing structured data — think generating a CSV report or dumping a JSON snapshot — 'w' is exactly right. You want a fresh file every time. For anything cumulative like logs, audit trails, or user-submitted data you're collecting before batch processing, always use 'a'.
There's a third scenario: what if you need to both read AND write? PHP has combined modes for this: 'r+' reads and writes without truncating (file must exist), and 'w+' reads and writes after truncating (creates file if missing). These are rarely needed in everyday work, but understanding they exist stops you from reaching for two separate fopen() calls when one would do.
File Utility Functions — The Toolkit Beyond Open and Close
Opening, reading, and writing are the core verbs, but real applications need more. You need to check if a file exists before reading it, know its size before deciding whether to stream it, delete old temp files, rename uploaded files to safe names, and copy backups before overwriting. PHP's file utility functions cover all of this.
file_exists() is your gatekeeper — call it before any operation where the file's absence would cause an error. is_readable() and is_writable() go a step further: a file can exist but be locked down by OS permissions, and these functions tell you so before you waste a call on fopen(). They're especially important when your PHP process runs as www-data and the file is owned by a different user.
filesize() returns the number of bytes in a file. Use it before loading a file into memory so you can decide whether to stream it line-by-line instead. A practical rule: if filesize() is above 1 MB, reach for fgets(). rename() and copy() are atomic and cross-platform — prefer them over shell_exec('mv ...') which is fragile and a security risk if user input is ever involved.
realpath() and verify the result starts with your intended base directory. Without this, an attacker can pass '../../../etc/passwd' as a filename and read sensitive system files.realpath() on the resolved path and a prefix check against the uploads directory.fopen().Error Handling and File Permissions — What Breaks in Production
PHP file functions are silent when they fail — they return false, not exceptions. That means you must check every return value yourself. The most common failure scenarios in production: file permissions, disk full, exhausted file descriptors, and missing directories (fopen() does not create parent directories).
For permissions: PHP's process user (usually www-data) must have the correct access on the file and the directory. A common trap: the file has 644 but the parent directory is 700 owned by root, so PHP can't traverse into it. Use is_readable() and is_writable() before fopen().
For missing directories: fopen() fails if the directory doesn't exist. Use is_dir() and mkdir() with recursive=true beforehand.
For disk full: fwrite() returns the number of bytes written. Compare it against the expected length. If it's less, the disk may be full — but PHP won't throw an error. Log the discrepancy.
For resource limits: long-running scripts must monitor file descriptor usage via /proc/self/fd or softlimit. Use register_shutdown_function() to close any leaked handles.
Production-grade code wraps every file operation in a try-catch? No — PHP's file functions don't throw by default. You need to manually check. Use a helper function that throws an exception on failure so you can centralise error handling.
fwrite() and file_put_contents() may return a partial byte count or false. They don't throw. Always check the return value against the expected length. If fewer bytes were written, log it immediately — the file is likely truncated.fwrite() return value. When the disk filled up, it wrote partial files. The monitoring system saw the file existed but was incomplete, triggering false alerts.strlen(). Throw an exception on mismatch.File Descriptor Leak Brought Down a Batch Processor
fopen() began returning false for every subsequent file, but the script didn't check the return value and passed false to fwrite(), which triggered a non-obvious warning about 'expects parameter 1 to be resource'fclose(), thinking PHP would clean up automatically when the script ended.fopen() calls failed, but the missing error check masked the real cause.fclose() after processing each file. Also implemented a counter to log warning when open handles exceeded 900, and added explicit error checking after every fopen() call with a descriptive die() message.- Always pair
fopen()withfclose()— one per iteration, not one per script. - Check
fopen()return value immediately — false means something broke. - Monitor file descriptor usage in long-running scripts with lsof -p or /proc/self/fd.
is_readable() and is_writable() to pinpoint the access level.fwrite() but returns a smaller byte count. Also verify LOCK_EX is applied for concurrent writes.filesize() guard before loading. If file > 1 MB, switch to fgets() streaming. Monitor memory_get_peak_usage() in logs.fopen() mode was 'w' — it truncates immediately. If you meant to append, use 'a'. Also verify the write pointer position with ftell().Key takeaways
fopen()'s return value before using the handlefgets() or fwrite() produces a misleading type error, not a helpful 'file not found' message.fgets() inside a while looprealpath() on user-supplied paths to prevent directory traversal attacks. Always canonicalise user input before using it in file operations.Common mistakes to avoid
3 patternsUsing 'w' mode when you meant 'a'
fopen() is called, before you've written a single byte. On a production log file this can destroy hours of diagnostics.fopen() call and ask 'do I want to keep what's already in this file?' If yes, use 'a'. If you want a clean slate, 'w' is correct — but be conscious of that choice.Never checking fopen()'s return value
fopen() fails it returns false, and the next line tries to call fgets(false) or fwrite(false, ...), producing a confusing 'expects parameter 1 to be resource' warning with no indication of what actually went wrong.fopen() in an if ($handle === false) { die(...) } check immediately after the call, and include the file path in the error message so you know exactly which file caused the problem.Forgetting LOCK_EX when multiple requests write to the same file
fwrite() when using the manual handle approach.Interview Questions on This Topic
What is the difference between fopen() modes 'r+', 'w', and 'a', and can you describe a real scenario where choosing the wrong mode would cause a data loss bug?
Frequently Asked Questions
That's PHP Basics. Mark it forged?
6 min read · try the examples if you haven't