We got an alert from our ops team: the disk of one of our servers was filling up. The culprit was a log file that grew so fast it ate all the available space. The dev team didn't configured any cleanup, so the file just kept growing.
The application was already using Serilog. Let's have a look what options we have available to limit the amount of log data.
The default config
The simplest way to write a log file with Serilog looks like this:
Log.Logger = new LoggerConfiguration()
.WriteTo.File("logs/log.txt")
.CreateLogger();
This works, but it writes everything to one single file. By default, no rolling happens and nothing gets deleted.
Remark: This isn’t entirely true. The file sink has a default fileSizeLimitBytes of 1 GB. When the file reaches that limit, Serilog silently stops writing log events. So you don’t fill your disk, but you lose your logs. That’s not much better.
Rolling files
The first step is to start a new file at regular intervals. This is done through the rollingInterval parameter:
Log.Logger = new LoggerConfiguration()
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
Remark: In older Serilog versions, a separate RollingFile sink existed. But now this functionally is all embedded directly into the File sink.
Serilog inserts the date in the file name, so you get files like log-20261010.txt.
The available intervals are:
Infinite: never roll (the default)YearMonthDayHourMinute
Rolling on its own doesn’t clean up anything. You just get more files instead of one big file. For that we need the retention options.
Limit the number of files
retainedFileCountLimit controls how many log files are kept, including the current one:
.WriteTo.File(
"logs/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 14)
With a daily interval, this keeps roughly two weeks of logs. The default is 31. Pass null if you want to keep everything (you probably don’t).
Limit the age of files
Newer versions of Serilog.Sinks.File also have retainedFileTimeLimit. Instead of counting files, you define how old a file may get:
.WriteTo.File(
"logs/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileTimeLimit: TimeSpan.FromDays(7))
If you set both retainedFileCountLimit and retainedFileTimeLimit, both rules apply.
Which one should you use? The count is predictable in terms of disk space, the time limit is predictable in terms of history. Often you want both.
Limit the size of a single file
Here is the catch that explains our incident. Daily rolling doesn’t help when a single day produces 40 GB of logs. We need to limit the size of each file as well:
.WriteTo.File(
"logs/log-.txt",
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 100 * 1024 * 1024,
rollOnFileSizeLimit: true,
retainedFileCountLimit: 14)
fileSizeLimitBytes: the maximum size of one file (here 100 MB)rollOnFileSizeLimit: whentrue, Serilog starts a new file when the limit is reached instead of dropping events
When a file is rolled because of its size, Serilog adds a sequence number: log-20261010.txt, log-20261010_001.txt, log-20261010_002.txt, ...
Now you can calculate your worst-case disk usage: fileSizeLimitBytes × retainedFileCountLimit. In this example that is 100 MB × 14 = 1.4 GB.
Remark: retainedFileCountLimit counts files, not days. With size-based rolling, a busy day can produce many files and push the older days out sooner than you expect. Combine it with retainedFileTimeLimit if you want a guaranteed history.
Remark: Old files are removed when Serilog creates a new file. If your application never rolls over, nothing is cleaned up.
Putting it all together in appsettings.json
Hardcoding this in C# works, but you probably want to change these values without redeploying. Using Serilog.Settings.Configuration, the same setup looks like this:
{
"Serilog": {
"Using": [ "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "logs/log-.txt",
"rollingInterval": "Day",
"fileSizeLimitBytes": 104857600,
"rollOnFileSizeLimit": true,
"retainedFileCountLimit": 14,
"retainedFileTimeLimit": "7.00:00:00"
}
}
]
}
}
And in Program.cs:
builder.Host.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration));
Some other options
Some other parameters of the file sink are worth knowing about:
shared: allows multiple processes to write to the same file. Useful in some hosting scenarios, but it comes with a performance cost.buffered: buffers writes in memory instead of flushing every event. Faster, but you can lose events when the process crashes.flushToDiskInterval: how often buffered data is flushed to disk.restrictedToMinimumLevel: only write events at or above a given level to this file.outputTemplate: control the format of each line. A shorter template means smaller files.hooks: aFileLifecycleHooksimplementation to run custom logic when a file is opened or closed, for example to compress rolled files with GZip.
Tip: Rolling and retention only protect your disk. They don’t fix the cause. In our case a chatty Microsoft.* namespace was responsible for most of the volume. Raising the minimum level for those namespaces (see the Override section above) reduced the log size more than any retention setting.
That’s it! A few lines of configuration, and the disk-full alert is no longer waiting for you on a Monday morning.
