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.

New in .NET 10 and C# 14: Optimizations 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.Net8

Step 2: Install Benchmark and Newtonsoft NuGet packages

dotnet add package BenchmarkDotNet
dotnet add package Newtonsoft.Json

We 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.Net10

Step 2: Install Benchmark and Newtonsoft NuGet packages

dotnet add package BenchmarkDotNet
dotnet add package Newtonsoft.Json

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;
    }
}

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