One Thursday morning the site came up with a 500 error. We had no deploy, no code had changed, and the database was in place. Stranger still, when we went to read the log to see what had happened, the last line was from a few minutes earlier and after that nothing; as if the log itself had been cut off.
A few minutes later the host's email arrived: the disk quota is full. A simple du exposed the culprit: a file named storage/logs/laravel.log that had quietly grown for months and now took several gigabytes.
This kind of failure gives no gradual warning. As long as one byte is left everything works normally; the moment the quota runs out everything breaks at once: sessions, cache, uploads, and even writing the very log that was supposed to explain what happened. This note is that path: diagnosis, cause, and setting up log rotation on shared hosting so it does not happen again.
Symptoms: from slowness to write errors#
A full disk quota has no single symptom. Depending on which operation hits the wall first, you see different things:
- A white page or a 500 error on all routes, even routes that do nothing with files.
- In the web server log or PHP's error output, messages like:
No space left on deviceorfailed to open stream: Disk quota exceeded. - Sometimes you see
Permission deniedinstead. This one is misleading, because you immediately go looking forchmodwhen the problem is not permissions. - A user logs in but is immediately kicked out, because the session driver is file-based and can no longer write the session file.
- A file upload fails or an incomplete file is saved.
- Noticeable slowness: every request waits on a failed write.
- The host's warning email with a subject like "disk quota warning". It usually arrives when it is too late.
The important point is that none of these name the culprit file. The system only says there is no room; finding what ate the room is your job.
Finding the culprit with du and ls#
First see how much space is used at all. In cPanel the Disk Usage section shows this, but it is quicker over SSH:
du -sh ~Then go one layer down and sort the biggest folders:
du -sh ~/* | sort -hsort -h sorts numbers with suffixes (K, M, G) correctly. Now repeat the same thing inside the app folder until you reach storage:
du -sh ~/app/storage/* | sort -h
ls -lh ~/app/storage/logsIf the log is the culprit, ls -lh shows it at once: a laravel.log whose size is nowhere near the scale of the other files.
To be sure no other big thing is left, list all files over a hundred megabytes in the user's home:
find ~ -type f -size +100M -exec ls -lh {} \; 2>/dev/nullThe 2>/dev/null is so that "no access" errors on system folders do not clutter the output. If the output is empty, the problem is the number of small files, not one giant file; we get to that in the section on other consumers.
Why laravel.log grows without limit#
The short answer: because nobody told it not to. Laravel by default is set to a driver that has just one file and always appends to the end of that one file. There is no ceiling, no rotation, no automatic deletion.
Four things make this growth fast:
- The
singledriver. This driver means "everything inlaravel.log, forever". Until you touch it yourself, the file only gets bigger.
- The
debuglog level in production. The default value ofLOG_LEVELin many sample files isdebug. That is the lowest possible level; meaning whatever any package decides to report gets written.
- A repeating error. The worst case is this: an exception that happens on every request and writes a full stack trace each time. One stack trace can be several kilobytes. Multiply that by a month of requests and it becomes a scary number.
- Bots. Crawlers and scanners hit odd paths, generate
404andMethodNotAllowed, and if these get logged too, the log volume has nothing to do with the number of your real users.
On a dedicated server this situation is bad too but shows itself later. On shared hosting where the disk quota is often a few gigabytes, a repeating error swallows the whole quota in a few weeks.
Emergency cleanup: why rm is the wrong move#
The first temptation is to delete the file:
rm ~/app/storage/logs/laravel.logIt works, but it has two risks.
Risk one: ownership and permissions. With rm the file and all its attributes are gone. Whichever process writes a log first recreates the next file, with that process's ownership, permissions and umask. If on your host the PHP user differs from the SSH user, or cron jobs run with different settings, the next file may be unwritable for one of the two. Then instead of "the disk is full" you get "Permission denied" — the same misleading symptom mentioned above.
Risk two: the space is not really freed. If a process keeps the file open — such as a queue:work that is running permanently — deleting the file name does not give the disk space back. The operating system keeps the space until the last file descriptor is closed. You run rm, du says everything is fine, but the quota is still full.
The right way is to empty the file's content, not delete it:
truncate -s 0 ~/app/storage/logs/laravel.logIf truncate is not on the host, do the same with the shell itself:
: > ~/app/storage/logs/laravel.logBoth keep the file with the same inode, the same owner and the same permissions and only set its length to zero. Processes that opened the file in append mode — which Monolog does — keep writing without problems. If you want to keep the last few lines for inspection before emptying, take a small copy first:
tail -n 2000 ~/app/storage/logs/laravel.log > ~/last-errors.txt
truncate -s 0 ~/app/storage/logs/laravel.logThis cleanup is only an emergency. The site comes back but nothing is solved; tomorrow it starts growing again.
The real cure: log rotation with the daily driver#
The real fix is changing the driver from single to daily. Instead of one file, daily creates a separate file for each day and deletes files older than a set number of days by itself.
In .env:
LOG_CHANNEL=daily
LOG_LEVEL=warning
LOG_DAILY_DAYS=14A version note: in newer Laravel versions the default value of LOG_CHANNEL is stack, and the stack channel itself decides which channels to write to. If you are on stack, the deciding variable is LOG_STACK, not LOG_CHANNEL:
LOG_CHANNEL=stack
LOG_STACK=daily
LOG_LEVEL=warning
LOG_DAILY_DAYS=14First make sure which case you have: open the config/logging.php file and see what the stack channel reads from. The daily section usually looks like this:
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => (int) env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],And after every change to .env, clear the config cache, or nothing happens:
php artisan config:clearIf you cache config on the server, run php artisan config:cache again. And if you changed values and saw no change, suspect the line endings of the .env file itself too; an invisible character at the end of a line can corrupt a value without telling you anything.
When exactly log rotation happens (and when it does not)#
It is good to know what happens under the hood, because there are a few traps.
daily uses a handler that builds the file name with the date: instead of laravel.log you see files like laravel-2026-09-07.log. When a log is written and today's date differs from the current file name, the handler "rotates": it creates today's file and at the same moment counts the older files of the same pattern and deletes those beyond days.
Three practical consequences of this behaviour:
- Deletion is not scheduled; it is event-driven. If the site writes no log for several weeks, no file is deleted either. In that case, though, nothing has grown, so there is no problem.
- It does not delete the old
laravel.logfile by itself. The deletion pattern only matches dated files. That multi-gigabytelaravel.logfrom before stays there until you delete it once yourself. Be sure to check this, or the quota is still full and you think the problem is solved. - Zero means infinity. If you set
LOG_DAILY_DAYSto zero, no file is deleted. Write the number explicitly.
For the number of days, 7 to 14 is enough for an ordinary store. If you have a few megabytes of log a day and your quota is tight, 7 is more conservative. If your log is light and sometimes you need to go back two weeks, 14 is more reasonable.
Log level: why debug is expensive in production#
The log level means "the floor of importance that I write". Any message below this floor never goes to disk. The order of levels is standard:
| Level | What it means | In production |
|---|---|---|
| debug | Fine details for development | No |
| info | Normal, expected events | Usually no |
| notice | Normal but noteworthy events | Depends |
| warning | Something is not right but work continues | Yes |
| error | An operation failed | Yes |
| critical and above | Part of the system is down | Yes |
For most production sites warning is the right point: you catch exceptions and failures and not the chattiness of packages. If you want to cut the volume even more and only real failures matter to you, error is defensible too — as long as you know you lose the "not broken yet but about to break" warnings.
Let us also clarify a common mistake: APP_DEBUG and LOG_LEVEL are two separate things. APP_DEBUG=false only stops error details being shown to the user; writing to the log stays in place. If you are unsure about this separation, our note on APP_DEBUG on shared hosting opens up exactly this.
The stack channel and separating important errors#
daily with warning solves the volume problem, but it has a side effect: really important errors get lost among the heap of ordinary warnings. The clean way is the stack channel; that is, a message goes to several destinations at once, each with its own importance floor.
In config/logging.php, add a separate channel for important errors:
'critical' => [
'driver' => 'daily',
'path' => storage_path('logs/critical.log'),
'level' => 'error',
'days' => 60,
'replace_placeholders' => true,
],And in .env:
LOG_CHANNEL=stack
LOG_STACK=daily,critical
LOG_LEVEL=warning
LOG_DAILY_DAYS=14Now you have two streams. laravel-YYYY-MM-DD.log keeps everything from warning up and is deleted after two weeks. critical-YYYY-MM-DD.log takes only error and above, stays small, and can stay two months without bothering the disk. When a customer says "my payment got stuck last week", you know exactly which file to open.
A weekly cron job and an early quota warning#
daily does its own job, but a cheap safety net helps. Set up a weekly cron job that sweeps old dated files — the ones that may have been left by other channels or from before the settings:
0 4 * * 0 /usr/bin/find /home/USER/app/storage/logs -name '*.log' -mtime +30 -delete-mtime +30 means files that have not changed for more than 30 days. Because it uses the modification date, today's file is never deleted.
And a reporting cron job that tells you usage once a week. cPanel emails you the cron output, so this one line is enough to be warned before you reach the ceiling:
0 7 * * 1 /usr/bin/du -sh /home/USER /home/USER/app/storage /home/USER/public_htmlIf the output never reaches your email or the cron job fails silently, first suspect the difference between the cron shell and the interactive shell; we dissected this in the note on PHP 8.4 in cPanel cron jobs. In short, in cron there is no alias and no custom PATH; write the full path of commands, as we did in the examples above.
Other things that eat the disk quota#
The log is usually the biggest culprit but not the only one. When the quota gets tight, look at these four places too.
Forgotten backups. A backup script you wrote a few months ago that creates a sql.gz every night but never deletes the old ones. These are big single files and are found with the same find -size +100M above. A backup on the same disk that may fill up does not count as a backup; take a copy somewhere else.
Image and thumbnail cache. If you build several sizes for each image, the image cache folder can become bigger than the original images themselves. Especially if old versions are not deleted after a size change. If you work with GD, the note on image processing with GD is written about these same limits.
Session files. With the file driver, every visitor — including every bot — creates a file in storage/framework/sessions. Each file is small, but their number can reach tens of thousands, and on some hosts the inode count limit fills up before the size limit. Then du shows a small number and you are left confused.
Framework cache. storage/framework/cache/data and storage/framework/views are usually negligible, but if the cache driver is file-based and you cache big things, they grow. Clearing them is harmless:
php artisan view:clear
php artisan cache:clearTo count session files and see whether their number has gone out of control:
ls -1 ~/app/storage/framework/sessions | wc -lIf you want one full review of the limits of this kind of host, the shared hosting checklist for Laravel puts these together.
Logs must not be readable from the web#
One point that is usually forgotten when the disk fills up: the content of the log is sensitive. Stack traces may include file paths, query parameters, tokens and sometimes variable values. This is not something you want to be public.
In Laravel's standard structure, storage is outside the web root and there is no problem. But on some shared hosts the whole project is put inside public_html, and then storage/logs is reachable through the browser. Try once: open the log file's URL in the browser. If you see anything, close it that moment. In the .htaccess file in the root:
<FilesMatch "\.log$">
Require all denied
</FilesMatch>And better than that, fix the structure so that storage is not under the web root at all and only the public folder stays outside. This takes effort once and removes a whole class of security problems for good.
The main lesson#
A disk filled by a log is not a bug; it is a missing decision. Laravel does not ask you how long the log should stay, and if you do not answer, the default answer is "forever".
Three lines in .env make this decision: the daily driver, the warning level, and a set number of days. A weekly cron job is a safety net, not the solution. And if your disk is full right now, run truncate first so the site can breathe, then go to the settings — not the other way round.
And one habit that has served us many times: whenever a file grows for no reason, before deleting it read its last few hundred lines. They usually say what error repeated a thousand times a day — and that error, not the file size, is your real problem.