.NET Core  

Benchmarking JSON Lines Serialization in .NET 11

JSON is one of the most common formats for exchanging structured data in .NET applications.

Traditional JSON works well for APIs and configuration files, but processing large collections can become inconvenient when an application needs to stream records incrementally.

Consider a large dataset:

[
  { "Id": 1, "Name": "Alice" },
  { "Id": 2, "Name": "Bob" },
  { "Id": 3, "Name": "Charlie" }
]

The complete document is one JSON value.

JSON Lines, commonly called JSONL or NDJSON, takes a different approach. Each line represents an independent JSON value:

{"Id":1,"Name":"Alice"}
{"Id":2,"Name":"Bob"}
{"Id":3,"Name":"Charlie"}

This format is particularly useful for streaming pipelines, logs, data exports, batch processing, and large datasets.

.NET 11 introduces built-in JSON Lines support in System.Text.Json. Microsoft documents APIs for serializing and deserializing JSON Lines data through JsonSerializer.SerializeLines and JsonSerializer.DeserializeLines. (learn.microsoft.com)

That makes JSON Lines an interesting performance topic for .NET developers.

The important question is not simply whether JSON Lines is "faster."

The useful question is:

How does JSON Lines behave when applications serialize and process large numbers of records?

What Is JSON Lines?

JSON Lines stores one valid JSON value per line.

For example:

{"id":101,"name":"Alice","active":true}
{"id":102,"name":"Bob","active":true}
{"id":103,"name":"Charlie","active":false}

Each line can be processed independently.

This has several practical advantages.

A consumer can read:

Record 1
Record 2
Record 3

without waiting for an entire JSON array to become available.

This makes JSON Lines useful for:

  • Streaming data

  • Log processing

  • Large exports

  • ETL pipelines

  • Machine-learning datasets

  • Event processing

  • Batch workloads

JSON Array vs JSON Lines

The difference is easier to understand through an example.

Traditional JSON:

[
  {
    "id": 101,
    "name": "Alice"
  },
  {
    "id": 102,
    "name": "Bob"
  }
]

JSON Lines:

{"id":101,"name":"Alice"}
{"id":102,"name":"Bob"}

The two formats represent similar data but have different processing characteristics.

CharacteristicJSON ArrayJSON Lines
Top-level structureOne arrayMultiple JSON values
Incremental processingPossible but more involvedNatural
Streaming recordsGood with streaming APIsDesigned for it
Human readabilityGoodGood
Append recordsRequires maintaining valid JSONSimple
Large data pipelinesUsefulParticularly convenient
Record boundariesJSON structureNewline

JSON Lines is not a replacement for normal JSON.

It is another representation optimized for record-oriented workflows.

JSON Lines Support in .NET 11

.NET 11 adds built-in APIs for JSON Lines serialization and deserialization.

A simple example is:

using System.Text.Json;

var users = new[]
{
    new User(1, "Alice"),
    new User(2, "Bob"),
    new User(3, "Charlie")
};

using var stream = File.Create("users.jsonl");

JsonSerializer.SerializeLines(
    stream,
    users);

The resulting file contains one JSON object per line.

For example:

{"Id":1,"Name":"Alice"}
{"Id":2,"Name":"Bob"}
{"Id":3,"Name":"Charlie"}

Microsoft documents JSON Lines support as part of the .NET 11 System.Text.Json improvements. (learn.microsoft.com)

Defining the Model

For benchmarking, use a representative model rather than a single primitive value.

public sealed record User(
    int Id,
    string Name,
    string Email,
    bool Active);

This creates a realistic serialization workload containing:

  • Integer data

  • Strings

  • Boolean data

  • Multiple properties

For production benchmarking, the model should ideally resemble the application's actual payload.

Serializing JSON Lines

A basic serialization example:

using System.Text.Json;

var users = new[]
{
    new User(
        1,
        "Alice",
        "[email protected]",
        true),

    new User(
        2,
        "Bob",
        "[email protected]",
        true)
};

using var stream = File.Create("users.jsonl");

JsonSerializer.SerializeLines(
    stream,
    users);

The method writes each item as an individual JSON value.

This differs conceptually from:

JsonSerializer.Serialize(
    stream,
    users);

The latter produces one JSON array.

The JSON Lines API is explicitly designed for line-delimited values.

Deserializing JSON Lines

Reading JSON Lines is similarly straightforward:

using System.Text.Json;

using var stream =
    File.OpenRead("users.jsonl");

foreach (var user in
         JsonSerializer.DeserializeLines<User>(
             stream))
{
    Console.WriteLine(user.Name);
}

This is particularly useful when processing large files.

Instead of treating the entire dataset as one logical JSON array, the application can process the sequence of records.

Why Benchmark JSON Lines?

There are several performance questions worth investigating.

Serialization Throughput

How many records can the application serialize per second?

Memory Usage

How much memory is allocated while processing large datasets?

Streaming Behavior

Can records be processed without constructing an entire collection?

File I/O

How much of the overall time is serialization versus disk I/O?

Record Size

How does performance change when objects become larger?

These questions are more useful than a single benchmark result.

Setting Up BenchmarkDotNet

Install BenchmarkDotNet in a dedicated benchmark project:

dotnet add package BenchmarkDotNet

Then create a benchmark class:

using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class JsonLinesBenchmark
{
    private User[] _users = [];

    [Params(100, 1_000, 10_000)]
    public int Count { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        _users = Enumerable
            .Range(1, Count)
            .Select(id =>
                new User(
                    id,
                    $"User{id}",
                    $"user{id}@example.com",
                    true))
            .ToArray();
    }
}

The Params attribute allows the same benchmark to run against different dataset sizes.

That is important because serialization behavior can change significantly as the workload grows.

Benchmark JSON Lines Serialization

A benchmark can use a memory stream to separate serialization overhead from disk performance:

[Benchmark]
public void SerializeJsonLines()
{
    using var stream =
        new MemoryStream();

    JsonSerializer.SerializeLines(
        stream,
        _users);
}

This measures the serialization path without involving physical disk latency.

That distinction is important.

If the benchmark writes directly to an SSD, the result may represent:

Serialization
+
Buffering
+
Filesystem
+
Storage

rather than serialization alone.

Benchmark Traditional JSON

Create a comparable benchmark for ordinary JSON:

[Benchmark]
public void SerializeJsonArray()
{
    using var stream =
        new MemoryStream();

    JsonSerializer.Serialize(
        stream,
        _users);
}

Now the experiment compares:

JSON Array
vs
JSON Lines

under the same general conditions.

However, do not interpret this as a universal apples-to-apples performance comparison.

The formats have different semantics.

JSON Lines is record-oriented, while the array representation is a single JSON value.

The appropriate choice depends on the workload.

Measure Allocations

Add:

[MemoryDiagnoser]

to the benchmark class.

Then examine:

  • Allocated bytes

  • Gen 0 collections

  • Gen 1 collections

  • Gen 2 collections

Memory behavior is especially important for large data-processing workloads.

For example:

100 records
      |
      v
1,000 records
      |
      v
10,000 records
      |
      v
100,000 records

A design that behaves well at 100 records may behave differently at 100,000 records.

Avoid Measuring Only Small Payloads

A common benchmark mistake is using extremely small objects.

For example:

public record Small(int Id);

This can be useful for isolating serializer overhead, but it may not represent a real production payload.

Also test a more representative object:

public sealed record Order(
    long Id,
    string CustomerName,
    string Email,
    decimal Total,
    string Currency,
    DateTimeOffset CreatedAt,
    bool Paid);

The larger model provides a more realistic serialization workload.

Benchmark Different Record Sizes

A useful experiment includes multiple payload categories.

DatasetPurpose
SmallSerializer overhead
MediumTypical application payload
LargeThroughput and allocation behavior
Very largeSustained streaming behavior

Do not assume that one payload size represents all workloads.

Streaming vs Buffering

One of the most interesting characteristics of JSON Lines is its suitability for streaming.

Suppose an application processes a large export.

A traditional approach may create a complete collection:

var users =
    JsonSerializer.Deserialize<User[]>(
        json);

The application then processes the resulting array.

With JSON Lines, the application can process records individually:

foreach (var user in
         JsonSerializer.DeserializeLines<User>(
             stream))
{
    ProcessUser(user);
}

The exact memory behavior should be measured rather than assumed, but the record-oriented API makes incremental processing natural.

Benchmark End-to-End Processing

Serialization-only benchmarks are useful, but a production workload often looks like:

Read
 |
 v
Deserialize
 |
 v
Process
 |
 v
Serialize
 |
 v
Write

A more representative benchmark could process each record:

[Benchmark]
public async Task ProcessJsonLines()
{
    using var input =
        File.OpenRead("users.jsonl");

    using var output =
        new MemoryStream();

    foreach (var user in
             JsonSerializer.DeserializeLines<User>(
                 input))
    {
        var updated = user with
        {
            Active = true
        };

        await JsonSerializer.SerializeLinesAsync(
            output,
            new[] { updated });
    }
}

For a real benchmark, avoid creating a new array for every record if doing so is not representative of the production design.

The benchmark should reflect the actual pipeline you intend to deploy.

Separate Serialization From I/O

Disk and network operations can dominate performance.

Consider:

Application
    |
    +-- Serialization
    |
    +-- Memory buffering
    |
    +-- Filesystem
    |
    +-- Network

If you want to understand serializer performance, first use MemoryStream.

Then add real storage:

Phase 1
MemoryStream
    |
    v
Serializer behavior

Phase 2
FileStream
    |
    v
Serializer + filesystem

Phase 3
Network stream
    |
    v
Serializer + network

This makes bottlenecks easier to identify.

Benchmark Async JSON Lines Processing

For applications using asynchronous streams, test the asynchronous path separately.

For example:

await foreach (var user in
    JsonSerializer.DeserializeLinesAsync<User>(
        stream))
{
    await ProcessAsync(user);
}

The important point is to benchmark asynchronous processing as a complete workload.

Do not compare a synchronous JSON benchmark with an asynchronous JSON Lines benchmark and attribute every difference to the serialization format.

You would be changing multiple variables.

Benchmark Different Stream Types

The underlying stream can influence results.

Test:

MemoryStream
FileStream
Network stream
Compressed stream

For example:

await using var stream =
    new FileStream(
        "users.jsonl",
        FileMode.Create,
        FileAccess.Write,
        FileShare.None,
        bufferSize: 64 * 1024,
        useAsync: true);

The buffer size should be chosen based on the actual application rather than blindly copied from a benchmark.

JSON Lines and Compression

JSON Lines is frequently used for data exports, which can also be compressed.

A pipeline might look like:

Objects
   |
   v
JSON Lines
   |
   v
Compression
   |
   v
Storage

For example:

await using var file =
    File.Create("users.jsonl.gz");

await using var gzip =
    new GZipStream(
        file,
        CompressionLevel.Fastest);

JsonSerializer.SerializeLines(
    gzip,
    users);

Now the benchmark measures more than JSON serialization.

It measures:

Serialization
+
Compression
+
I/O

Therefore, compression should be benchmarked as a separate experiment before drawing conclusions about JSON Lines itself.

JSON Lines vs JSON Array

A practical comparison looks like this:

RequirementJSON ArrayJSON Lines
Standard API responseExcellentUsually unnecessary
Streaming recordsPossibleExcellent fit
Append recordsAwkwardSimple
Record-level processingLess naturalNatural
Large exportsGoodVery useful
Existing JSON consumersExcellentRequires JSONL support
Human readabilityGoodGood

Choose based on the data flow rather than performance assumptions.

Common Benchmarking Mistakes

Including Disk I/O in a Serializer Benchmark

Use MemoryStream when measuring serialization overhead.

Using Only One Dataset Size

Test multiple record counts.

Measuring Only Time

Include allocation and GC measurements.

Comparing Different Data Models

The compared formats should serialize equivalent data.

Using Debug Builds

Run performance benchmarks using Release configuration.

dotnet run -c Release

Publishing Fabricated Numbers

If the benchmark has not been executed on the target environment, do not publish made-up throughput or latency values.

Report the methodology and provide reproducible benchmark code instead.

Changing Multiple Variables

Keep the runtime, model, dataset, stream, and benchmark environment consistent.

A Reproducible Benchmark Matrix

A practical test matrix might look like:

TestJSON ArrayJSON Lines
100 recordsMeasureMeasure
1,000 recordsMeasureMeasure
10,000 recordsMeasureMeasure
Small payloadMeasureMeasure
Medium payloadMeasureMeasure
Large payloadMeasureMeasure
SerializationMeasureMeasure
DeserializationMeasureMeasure
AllocationMeasureMeasure
StreamingBaselineMeasure
File outputMeasureMeasure
CompressionOptionalOptional

The goal is to understand the workload rather than produce a single leaderboard.

Production Design Considerations

JSON Lines is particularly attractive when the application processes records continuously.

Examples include:

Application Logs
      |
      v
JSON Lines
      |
      v
Processing Pipeline

or:

Database Export
      |
      v
JSON Lines
      |
      v
Object Storage

or:

Event Producer
      |
      v
JSON Lines
      |
      v
Batch Consumer

In these scenarios, the record boundary itself can simplify processing.

Handling Malformed Lines

Production pipelines should assume that input can be malformed.

For example:

{"id":1,"name":"Alice"}
INVALID JSON
{"id":3,"name":"Charlie"}

The application needs a defined failure strategy.

Depending on the workload, it might:

  • Stop processing.

  • Skip the invalid record.

  • Move the invalid record to a dead-letter location.

  • Log the failure and continue.

  • Retry the input.

Do not silently discard malformed data.

The correct policy depends on the business requirements.

Schema Evolution

JSON Lines does not eliminate schema evolution concerns.

Suppose version one produces:

{"id":1,"name":"Alice"}

and version two adds:

{"id":1,"name":"Alice","active":true}

Consumers should be designed to tolerate expected changes.

Test:

Old producer -> New consumer
New producer -> Old consumer

when backward and forward compatibility are required.

Troubleshooting Performance Results

JSON Lines Is Slower in My Test

That may be completely reasonable.

JSON Lines and JSON arrays have different processing semantics.

First determine whether the benchmark is measuring serialization only or includes I/O, buffering, compression, and processing.

Memory Usage Is Higher Than Expected

Check whether the benchmark creates a large in-memory collection before serialization.

Also check intermediate allocations in your application pipeline.

File Processing Is Slow

Measure serialization and file I/O independently.

Storage throughput can dominate the result.

Streaming Does Not Reduce Memory

Inspect the complete pipeline.

A streaming serializer cannot eliminate memory usage introduced by application code that buffers every processed record.

Best Practices

  1. Use JSON Lines for record-oriented workloads.

  2. Benchmark with realistic payloads.

  3. Test multiple dataset sizes.

  4. Measure allocations as well as latency.

  5. Separate serialization from I/O.

  6. Test synchronous and asynchronous paths independently.

  7. Use Release builds.

  8. Record the exact .NET SDK and runtime version.

  9. Test malformed input handling.

  10. Test schema evolution.

  11. Do not assume streaming automatically means zero allocations.

  12. Use actual production-shaped workloads before making architecture decisions.

Frequently Asked Questions

What is JSON Lines?

JSON Lines is a record-oriented format where each line contains one valid JSON value.

For example:

{"id":1}
{"id":2}
{"id":3}

It is also commonly called JSONL or NDJSON.

Is JSON Lines faster than JSON arrays?

There is no universal answer.

The formats solve different problems. Performance depends on payload size, serializer configuration, streaming behavior, I/O, memory usage, and workload.

Does JSON Lines reduce memory usage?

It can support incremental processing, but the overall application's memory behavior depends on how records are read, processed, buffered, and stored.

Benchmark the complete pipeline.

Should I use JSON Lines for REST APIs?

Not automatically.

Traditional JSON remains an excellent choice for typical REST responses. JSON Lines becomes more attractive when the API or data pipeline needs record-oriented streaming.

Does .NET 11 support JSON Lines natively?

Yes. .NET 11 adds JSON Lines serialization and deserialization APIs to System.Text.Json, including SerializeLines and DeserializeLines. (learn.microsoft.com)

Conclusion

JSON Lines provides a useful record-oriented representation for applications that need to process structured data incrementally.

.NET 11 adds native support through System.Text.Json, reducing the need for application-specific JSONL serialization infrastructure. (learn.microsoft.com)

The performance question should still be approached carefully.

A meaningful benchmark should separate:

Serialization
     +
Deserialization
     +
Memory Allocation
     +
Streaming
     +
I/O
     +
Compression
     +
Application Processing

from one another.

The most useful experiment compares equivalent data under controlled conditions:

Define representative model
          |
          v
Generate multiple dataset sizes
          |
          v
Benchmark JSON Array
          |
          v
Benchmark JSON Lines
          |
          v
Measure allocations
          |
          v
Measure streaming behavior
          |
          v
Add real I/O
          |
          v
Evaluate production workload

.NET 11's JSON Lines support makes record-oriented processing considerably easier to implement. Whether it improves performance for a particular application, however, remains a workload-specific question.

The right decision is therefore not "JSON Lines is faster."

It is:

"JSON Lines matches this workload, and our measurements show that its processing model provides the characteristics our application requires."