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:
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.
| Characteristic | JSON Array | JSON Lines |
|---|
| Top-level structure | One array | Multiple JSON values |
| Incremental processing | Possible but more involved | Natural |
| Streaming records | Good with streaming APIs | Designed for it |
| Human readability | Good | Good |
| Append records | Requires maintaining valid JSON | Simple |
| Large data pipelines | Useful | Particularly convenient |
| Record boundaries | JSON structure | Newline |
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.
| Dataset | Purpose |
|---|
| Small | Serializer overhead |
| Medium | Typical application payload |
| Large | Throughput and allocation behavior |
| Very large | Sustained 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:
| Requirement | JSON Array | JSON Lines |
|---|
| Standard API response | Excellent | Usually unnecessary |
| Streaming records | Possible | Excellent fit |
| Append records | Awkward | Simple |
| Record-level processing | Less natural | Natural |
| Large exports | Good | Very useful |
| Existing JSON consumers | Excellent | Requires JSONL support |
| Human readability | Good | Good |
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:
| Test | JSON Array | JSON Lines |
|---|
| 100 records | Measure | Measure |
| 1,000 records | Measure | Measure |
| 10,000 records | Measure | Measure |
| Small payload | Measure | Measure |
| Medium payload | Measure | Measure |
| Large payload | Measure | Measure |
| Serialization | Measure | Measure |
| Deserialization | Measure | Measure |
| Allocation | Measure | Measure |
| Streaming | Baseline | Measure |
| File output | Measure | Measure |
| Compression | Optional | Optional |
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:
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
Use JSON Lines for record-oriented workloads.
Benchmark with realistic payloads.
Test multiple dataset sizes.
Measure allocations as well as latency.
Separate serialization from I/O.
Test synchronous and asynchronous paths independently.
Use Release builds.
Record the exact .NET SDK and runtime version.
Test malformed input handling.
Test schema evolution.
Do not assume streaming automatically means zero allocations.
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."