New in .NET 10 and C# 14: Optimizations in log aggregation jobs
.NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. In this series, we will explore which aspects of software can be upgraded with the latest release. Today, in the series of What is new in .NET 10 and C#14, we will see optimization in log aggregation jobs.

Logging is a pathway to find anomalies and debug issues in any application. The application keeps generating logs for operations such as receiving device data, calling an API, checking background services, and more. Processing of logs requires high throughput because some applications demand the generation of multiple logs for a single operation. That job adds up once you do aggregation, including normalizing, parsing to the desired format, and storing them. Notification services often analyze stored logs to generate notifications and alerts based on the criticality of the logs. So, log aggregation is a tedious job for the server and a silent soldier of the application. Mindful of these, .NET brings several refinements in its JSON and string operations. Besides memory allocation, changes will help you in log generation. I will shed light on how much the said task has been improved.
Log Aggregation job with .NET 8 and C#12
To observe a significant improvement in .NET 10, let's first create log aggregation with the prior version, .NET 8.
Step 1: Create a project
Run the command to create a log aggregation project
mkdir LogBenchmark.Net8
cd LogBenchmark.Net8Step 2: Install Benchmark and Newtonsoft NuGet packages
dotnet add package BenchmarkDotNet
dotnet add package Newtonsoft.JsonWe will use BenchmarkDotNet to measure the time taken for each method.
Step 3: Write Logging jobs with benchmark
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Newtonsoft.Json.Linq;
using System.Text.Json;
BenchmarkRunner.Run<LogBenchmark>();
[MemoryDiagnoser]
public class LogBenchmark
{
private readonly byte[] _utf8LogBytes;
private readonly string _logString;
public LogBenchmark()
{
_logString = """
{
"level": "info",
"deviceId": "A1B2C3D4E5F60788",
"timestamp": "2025-12-01T10:22:00Z",
"message": "Temperature reading received",
"value": 22.5
}
""";
_utf8LogBytes = System.Text.Encoding.UTF8.GetBytes(_logString);
}
// ------------------------------------------------------------
// 1. Baseline: Newtonsoft.Json
// ------------------------------------------------------------
[Benchmark(Baseline = true)]
public string Newtonsoft_Parse()
{
var obj = JObject.Parse(_logString);
return (string)obj["deviceId"]!;
}
// ------------------------------------------------------------
// 2. System.Text.Json in UTF-16
// ------------------------------------------------------------
[Benchmark]
public string SystemTextJson_Parse()
{
using var doc = JsonDocument.Parse(_logString);
return doc.RootElement.GetProperty("deviceId").GetString()!;
}
// ------------------------------------------------------------
// 3. UTF8JsonReader + Span<byte>
// ------------------------------------------------------------
[Benchmark]
public string? Utf8JsonReader_Parse()
{
var reader = new Utf8JsonReader(_utf8LogBytes);
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName &&
reader.ValueTextEquals("deviceId"))
{
reader.Read();
return reader.GetString();
}
}
return null;
}
}I defined 3 methods for logging using NewtonSoft and System.Text and Utf8Json returning deviceId. Each of them is decorated with the Benchmark attribute. To test, I defined a log message in the constructor.
Step 4: Run and test
As we know, we have to run a release in benchmark projects.
dotnet run -c Release| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|
| Newtonsoft_Parse | 3,735.4 ns | 187.27 ns | 531.25 ns | 3,562.1 ns | 1.02 | 0.20 | 3.0060 | 4728 B | 1.000 |
| SystemTextJson_Parse | 952.0 ns | 30.03 ns | 86.65 ns | 917.8 ns | 0.26 | 0.04 | 0.0706 | 112 B | 0.024 |
| Utf8JsonReader_Parse | 233.3 ns | 4.86 ns | 9.92 ns | 234.2 ns | 0.06 | 0.01 | 0.0248 | 40 B | 0.008 |
Log Aggregation job with .NET 10 and C#14
Now jump to our latest tool, the .NET 10. I will create the same project
Step 1: Create a project
mkdir LogBenchmark.Net10
cd LogBenchmark.Net10Step 2: Install Benchmark and Newtonsoft NuGet packages
dotnet add package BenchmarkDotNet
dotnet add package Newtonsoft.JsonStep 3: Write Logging jobs with benchmark
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Newtonsoft.Json.Linq;
using System.Text.Json;
BenchmarkRunner.Run<LogBenchmark>();
[MemoryDiagnoser]
public class LogBenchmark
{
private readonly byte[] _utf8LogBytes;
private readonly string _logString;
public LogBenchmark()
{
_logString = """
{
"level": "info",
"deviceId": "A1B2C3D4E5F60788",
"timestamp": "2025-12-01T10:22:00Z",
"message": "Temperature reading received",
"value": 22.5
}
""";
_utf8LogBytes = System.Text.Encoding.UTF8.GetBytes(_logString);
}
// ------------------------------------------------------------
// 1. Baseline: Newtonsoft.Json
// ------------------------------------------------------------
[Benchmark(Baseline = true)]
public string Newtonsoft_Parse()
{
var obj = JObject.Parse(_logString);
return (string)obj["deviceId"]!;
}
// ------------------------------------------------------------
// 2. System.Text.Json in UTF-16
// ------------------------------------------------------------
[Benchmark]
public string SystemTextJson_Parse()
{
using var doc = JsonDocument.Parse(_logString);
return doc.RootElement.GetProperty("deviceId").GetString()!;
}
// ------------------------------------------------------------
// 3. UTF8JsonReader + Span<byte>
// ------------------------------------------------------------
[Benchmark]
public string? Utf8JsonReader_Parse()
{
var reader = new Utf8JsonReader(_utf8LogBytes);
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName &&
reader.ValueTextEquals("deviceId"))
{
reader.Read();
return reader.GetString();
}
}
return null;
}
}So, to keep the scale equal, I have used the same code for all 3 methods with the same input message.
Step 4: Run and test
Running the project
dotnet run -c Release| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|
| Newtonsoft_Parse | 2,326.9 ns | 65.47 ns | 184.67 ns | 2,285.6 ns | 1.01 | 0.11 | 3.0098 | 4728 B | 1.000 |
| SystemTextJson_Parse | 793.3 ns | 19.81 ns | 54.57 ns | 777.6 ns | 0.34 | 0.03 | 0.0706 | 112 B | 0.024 |
| Utf8JsonReader_Parse | 202.5 ns | 7.74 ns | 22.32 ns | 195.8 ns | 0.09 | 0.01 | 0.0253 | 40 B | 0.008 |
We can observe the clear difference between the two versions. .NET 10 improved logging and string operations significantly. Our latest fellow reduced execution time up to 38%. Let's break down what made .NET 10 achieve the feat
- JIT optimizes Span<T> and Utf8JsonReader in the area of escape analysis. It reduced object allocation and reduced Gen0 Garbage Collector (GC) activity. JIT also inlines small methods,
Utf8JsonReaderresulting in fewer loops. - UTF-8 processing also saw improvements, including faster UTF-8 transcoding, reduced branching on common code paths, and faster byte scanning.
- The JIT compiler can place the promoted members of struct arguments into shared registers directly, rather than on the stack, eliminating unnecessary memory operations.
- .NET team is working to upgrade System. Text is Microsoft's own string manipulation library. System.Text that is native to .NET, as opposed to Newtonsoft, which is third-party. They improved property lookup, UTF-8 parsing, and JSONDocument memory usage in the updates.
- Although NewtonSoft is not managed by Microsoft, compiler enhancements affected the application's overall performance. We saw that in our results, too. Lower GC pauses, better dictionary lookups, faster JIT inlining, and improved string allocation helped Newtonsoft perform well.
Conclusion
.NET 10 is released in November 2025 and is supported for three years as a long-term support (LTS) release. It brings the latest version of C# with many refinements. In the blog post for our .NET 10 and C# 14 series, I highlighted improvements to the log aggregation jobs. Numerous changes to GC operations, JIT, and string manipulation have yielded significant results, as I demonstrated through benchmarking comparing predecessor .NET 8.
.NET 8 example: https://github.com/elmahio-blog/LogBenchmark.Net8
.NET 10 example: https://github.com/elmahio-blog/LogBenchmark.Net10
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website