# A laravel.log That Fills the Disk: Log Rotation on Shared Hosting

- آدرس: https://di9.ir/blog/en/laravel-log-rotation-shared-hosting-disk-full
- نویسنده: danidiba
- انتشار: 2026-09-08T10:38:53+03:30
- به‌روزرسانی: 2026-09-08T10:38:55+03:30
- دسته: Hosting & Servers
- برچسب‌ها: دیباگ و لاگ، کرون، لاراول، هاست اشتراکی

> The hosting disk quota filled up and the site went down with write errors. One file was to blame: a multi-gigabyte laravel.log. This note shows how to find the culprit, why the single driver never rotates, and how to prevent a repeat with daily and the right log level.

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 device` or `failed to open stream: Disk quota exceeded`.
- Sometimes you see `Permission denied` instead. This one is misleading, because you immediately go looking for `chmod` when 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:

```bash
du -sh ~
```

Then go one layer down and sort the biggest folders:

```bash
du -sh ~/* | sort -h
```

`sort -h` sorts numbers with suffixes (K, M, G) correctly. Now repeat the same thing inside the app folder until you reach `storage`:

```bash
du -sh ~/app/storage/* | sort -h
ls -lh ~/app/storage/logs
```

If 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:

```bash
find ~ -type f -size +100M -exec ls -lh {} \; 2>/dev/null
```

The `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:

1. **The `single` driver.** This driver means "everything in `laravel.log`, forever". Until you touch it yourself, the file only gets bigger.

1. **The `debug` log level in production.** The default value of `LOG_LEVEL` in many sample files is `debug`. That is the lowest possible level; meaning whatever any package decides to report gets written.

1. **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.

1. **Bots.** Crawlers and scanners hit odd paths, generate `404` and `MethodNotAllowed`, 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:

```bash
rm ~/app/storage/logs/laravel.log
```

It 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:

```bash
truncate -s 0 ~/app/storage/logs/laravel.log
```

If `truncate` is not on the host, do the same with the shell itself:

```bash
: > ~/app/storage/logs/laravel.log
```

Both 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:

```bash
tail -n 2000 ~/app/storage/logs/laravel.log > ~/last-errors.txt
truncate -s 0 ~/app/storage/logs/laravel.log
```

This 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`:

```ini
LOG_CHANNEL=daily
LOG_LEVEL=warning
LOG_DAILY_DAYS=14
```

A 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`:

```ini
LOG_CHANNEL=stack
LOG_STACK=daily
LOG_LEVEL=warning
LOG_DAILY_DAYS=14
```

First 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:

```php
'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:

```bash
php artisan config:clear
```

If 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.

![A comparison of one log file growing endlessly with seven daily files whose oldest is deleted](http://localhost:8890/wp-content/uploads/2026/09/fig-fig-svg1-yb32h-5inkec.svg)

## 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.log` file by itself.** The deletion pattern only matches dated files. That multi-gigabyte `laravel.log` from 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_DAYS` to 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](https://di9.ir/blog/app-debug-%D8%B1%D9%88%DB%8C-%D9%87%D8%A7%D8%B3%D8%AA-%D8%A7%D8%B4%D8%AA%D8%B1%D8%A7%DA%A9%DB%8C) 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:

```php
'critical' => [
    'driver' => 'daily',
    'path' => storage_path('logs/critical.log'),
    'level' => 'error',
    'days' => 60,
    'replace_placeholders' => true,
],
```

And in `.env`:

```ini
LOG_CHANNEL=stack
LOG_STACK=daily,critical
LOG_LEVEL=warning
LOG_DAILY_DAYS=14
```

Now 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:

```bash
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:

```bash
0 7 * * 1 /usr/bin/du -sh /home/USER /home/USER/app/storage /home/USER/public_html
```

If 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](https://di9.ir/blog/php84-%D8%AF%D8%B1-%DA%A9%D8%B1%D9%88%D9%86-%D8%AC%D8%A7%D8%A8-%D8%B3%DB%8C-%D9%BE%D9%86%D9%84). 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.

![The share of disk usage between logs, backups, the image cache, sessions and the framework cache](http://localhost:8890/wp-content/uploads/2026/09/fig-fig-svg2-agd06-qvmhtw.svg)

**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](https://di9.ir/blog/%D9%BE%D8%B1%D8%AF%D8%A7%D8%B2%D8%B4-%D8%AA%D8%B5%D9%88%DB%8C%D8%B1-%D8%A8%D8%A7-gd-%D8%A8%D8%AF%D9%88%D9%86-imagemagick) 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:

```bash
php artisan view:clear
php artisan cache:clear
```

To count session files and see whether their number has gone out of control:

```bash
ls -1 ~/app/storage/framework/sessions | wc -l
```

If you want one full review of the limits of this kind of host, the [shared hosting checklist for Laravel](https://di9.ir/blog/%DA%86%DA%A9-%D9%84%DB%8C%D8%B3%D8%AA-%D9%87%D8%A7%D8%B3%D8%AA-%D8%A7%D8%B4%D8%AA%D8%B1%D8%A7%DA%A9%DB%8C-%D8%A8%D8%B1%D8%A7%DB%8C-%D9%84%D8%A7%D8%B1%D8%A7%D9%88%D9%84) 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:

```apache
<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.

## پرسش‌های متداول

### Why does laravel.log get so big?

Because Laravel's default log driver is a single file that never rotates and has no ceiling. If the log level is debug too, almost every event is written. A repeating exception that writes a full stack trace on every request can become several gigabytes in a few weeks.

### What is the difference between the single and daily drivers in Laravel?

The single driver always appends to one fixed file and that file grows forever. The daily driver creates a separate file for each day with the date in its name and, when creating the new day's file, deletes files older than the configured number of days. The number of days is set with the LOG_DAILY_DAYS variable and if it is zero no file is deleted.

### Does deleting the laravel.log file cause problems?

Deleting the file entirely has two risks. First, whichever process writes next recreates the file, and its ownership or permissions may differ from before, leading to a Permission denied error. Second, if a process such as a worker keeps the file open, the disk space is not freed until it is closed. Instead of deleting, empty the file with the truncate command.

### What should LOG_LEVEL be in production?

For most production sites, warning is the right choice, since it catches exceptions and failures but does not write packages' informational messages. If the log volume is still large you can go to error, provided you accept losing the warnings that come before a failure. The debug value is only suitable for development.

### Why do I see no change after changing LOG_CHANNEL?

Usually because the config cache still holds the old value. Run php artisan config:clear and, if you cache the config, run config:cache again. Also, in newer Laravel versions, if LOG_CHANNEL is stack, the deciding variable is LOG_STACK, not LOG_CHANNEL.
