Introduction
JSON is everywhere in modern .NET applications.
APIs frequently accept flexible request payloads, store metadata, integrate with third-party systems, and persist event data that does not fit neatly into relational columns. PostgreSQL's jsonb type makes this practical by allowing applications to store structured JSON while still providing indexing and query capabilities.
For an EF Core application, however, using jsonb effectively requires more than simply changing a column type.
The performance of JSONB workloads depends on:
Document size
Query patterns
JSON path complexity
Index design
Data distribution
Number of rows
Update frequency
Projection strategy
Serialization overhead
EF Core query translation
This becomes particularly important when evaluating a PostgreSQL version upgrade.
A query that works correctly against an older database may behave differently when the database engine, statistics, planner decisions, or workload characteristics change.
A useful benchmark therefore needs to measure the complete path:
.NET API
|
v
EF Core
|
v
SQL
|
v
PostgreSQL JSONB
|
v
Index / Query Plan
|
v
ResultThis article explains how to benchmark JSONB workloads from .NET APIs and identify performance regressions before they reach production.
What Is JSONB?
PostgreSQL provides jsonb as a binary representation of JSON data.
Consider a document like:
{
"customerId": 1842,
"region": "north",
"preferences": {
"language": "en",
"notifications": true
},
"tags": [
"premium",
"enterprise"
]
}A relational model could store these values in separate columns.
But some application data is naturally variable.
For example:
Customer
|
+---- Fixed columns
| Id
| Name
| CreatedAt
|
+---- JSONB
Preferences
Metadata
AttributesThis hybrid model can be useful when some fields are stable while other attributes change frequently.
Why JSONB Performance Needs Benchmarking
A JSONB column can make application development easier, but flexible storage does not automatically mean efficient querying.
For example:
SELECT *
FROM customers
WHERE metadata->>'region' = 'north';This may behave very differently from a query using an appropriate index.
The difference can become substantial as the table grows:
10,000 rows
|
v
Small performance difference
10 million rows
|
v
Index strategy becomes criticalThe benchmark should therefore test realistic data volumes rather than only a development database.
A Typical EF Core Model
A .NET application might represent JSON metadata as a property.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public CustomerMetadata Metadata { get; set; } = new();
}The metadata might contain:
public class CustomerMetadata
{
public string Region { get; set; } = string.Empty;
public string Segment { get; set; } = string.Empty;
public bool NotificationsEnabled { get; set; }
}The database representation can use PostgreSQL jsonb.
The important question is not whether this mapping works.
The important question is:
How does it perform under realistic API workloads?
Define the Benchmark Workload
Before measuring anything, define the operations your application actually performs.
A useful JSONB benchmark should include:
Read complete document
Read one JSON property
Filter by JSON property
Filter by nested property
Search JSON array
Update JSON document
Update one JSON property
Insert JSON document
Bulk insert
Bulk update
Paginated JSON queryThese operations have different performance characteristics.
Create a Representative Dataset
Suppose the application has:
5 million customersEach customer contains:
{
"region": "north",
"segment": "enterprise",
"preferences": {
"language": "en",
"notifications": true
},
"tags": [
"premium",
"priority"
]
}Do not generate identical documents.
Realistic benchmarks should include variation:
Region:
north
south
east
west
Segment:
consumer
business
enterprise
Tags:
1–10 values
Metadata size:
1 KB
5 KB
20 KB
50 KBData distribution strongly affects query performance.
Benchmark Different Document Sizes
JSONB performance can change as documents become larger.
Test multiple document sizes:
| Document Size | Example Workload |
|---|---|
| Small | 1 KB |
| Medium | 5 KB |
| Large | 20 KB |
| Very Large | 50+ KB |
A query that performs well with a 1 KB document may become expensive when every row contains a much larger document.
This is particularly relevant when APIs return complete JSON documents.
Measure Insert Performance
A simple EF Core operation might be:
var customer = new Customer
{
Name = "Customer 1842",
Metadata = new CustomerMetadata
{
Region = "north",
Segment = "enterprise",
NotificationsEnabled = true
}
};
dbContext.Customers.Add(customer);
await dbContext.SaveChangesAsync();Measure:
Insert latency
Rows per second
Transaction duration
Database CPU
Write amplificationThen repeat with different JSON document sizes.
Benchmark Read Performance
The simplest workload retrieves the complete entity.
var customer = await dbContext.Customers
.FirstOrDefaultAsync(x => x.Id == customerId);Measure:
Database execution time
Network transfer
EF Core materialization
Total API latencyIt is useful to separate these components.
PostgreSQL
|
v
Network
|
v
EF Core
|
v
ASP.NET APIOtherwise, a slow API response may incorrectly be blamed on PostgreSQL.
Benchmark Property Filtering
A common JSONB workload is filtering by one property.
Conceptually:
WHERE metadata->>'region' = 'north'The application might expose an API such as:
GET /customers?region=northThe benchmark should measure:
10,000 rows
100,000 rows
1 million rows
10 million rowsand compare execution behavior.
Compare Indexed and Non-Indexed Queries
This is one of the most important benchmark comparisons.
Without an appropriate index, PostgreSQL may scan a large portion of the table.
Query
|
v
Sequential Scan
|
v
Millions of JSONB valuesWith a suitable index:
Query
|
v
Index
|
v
Matching RowsThe benchmark should measure the difference rather than assuming an index is always beneficial.
Test JSONB Indexing Strategies
PostgreSQL supports indexing approaches suited to JSONB workloads.
A common example is a GIN index.
CREATE INDEX idx_customer_metadata
ON customers
USING GIN (metadata);But index selection should depend on the actual query workload.
For a frequently queried scalar property, a targeted expression index can sometimes be more appropriate.
Conceptually:
CREATE INDEX idx_customer_region
ON customers ((metadata->>'region'));The benchmark should compare the strategies using real queries.
Do Not Benchmark Only Query Speed
Indexes have costs.
An index can improve:
Read performancewhile increasing:
Insert cost
Update cost
Storage usage
Maintenance workTherefore the benchmark should measure both:
Read Performance
+
Write Performance
+
Storage CostA solution that makes reads 2x faster but makes every write significantly more expensive may not be the right design.
Benchmark Nested JSON Properties
Real documents are rarely flat.
For example:
{
"customer": {
"region": {
"country": "India",
"state": "Bihar"
}
}
}The API may need:
Find customers where country = Indiaor:
Find customers where state = BiharNested queries should be benchmarked separately because their access patterns can differ from simple scalar properties.
Benchmark JSON Arrays
Arrays introduce another common workload.
Consider:
{
"tags": [
"premium",
"enterprise",
"priority"
]
}A query may ask:
Find all customers tagged "enterprise".The benchmark should measure:
No index
Appropriate JSONB index
Alternative normalized representationThis helps determine whether the JSONB model remains appropriate at scale.
Compare JSONB With Relational Columns
Not every frequently queried field should necessarily remain inside JSONB.
Suppose:
{
"region": "north",
"segment": "enterprise"
}is queried by almost every API request.
You could store:
region
segmentas relational columns while keeping less frequently accessed metadata in JSONB.
Customer
|
+---- Region relational
+---- Segment relational
|
+---- Metadata JSONBThis hybrid approach can be useful for high-value query fields.
Benchmark both designs.
A Useful Comparison
| Design | Read | Write | Flexibility | Indexing |
|---|---|---|---|---|
| JSONB only | Variable | Good | High | Flexible |
| Relational only | Predictable | Predictable | Lower | Strong |
| Hybrid | Often strong | Moderate | High | Targeted |
The correct choice depends on workload rather than ideology.
Benchmark EF Core Projection
Returning the entire JSONB document can be expensive if the API only needs a few fields.
Instead of:
var customers = await dbContext.Customers
.Where(x => x.Id == customerId)
.ToListAsync();consider a projection that retrieves only what the endpoint needs.
Conceptually:
var result = await dbContext.Customers
.Where(x => x.Id == customerId)
.Select(x => new
{
x.Id,
x.Name,
x.Metadata.Region
})
.FirstOrDefaultAsync();The benchmark should compare:
Full document
vs.
Required fieldsThis can reveal unnecessary data transfer and materialization costs.
Measure API Payload Size
Suppose a JSONB document contains 30 KB of metadata but the API only returns:
{
"id": 1842,
"name": "Customer 1842",
"region": "north"
}Retrieving the entire document from PostgreSQL and discarding most of it wastes resources.
Measure:
Database bytes
Network bytes
Serialized response size
Client response timeA database benchmark that ignores network transfer can miss a significant part of API performance.
Test Update Workloads
JSONB updates deserve special attention.
For example:
Update customer preferencesmay result in a database operation that rewrites more data than expected.
Benchmark:
Update entire document
Update one property
Update multiple propertiesThen measure:
Execution time
Write throughput
WAL generation
Storage impact
Lock durationThe difference becomes increasingly important as documents grow.
Compare Whole-Document and Targeted Updates
Conceptually:
Whole Document Update
JSONB
|
v
Replace entire documentversus:
Targeted Update
JSONB
|
v
Modify selected propertyThe best strategy depends on the application and update frequency.
A benchmark should use the same workload patterns that occur in production.
Test Concurrent Writes
Single-user tests can hide contention.
For example:
1 request
|
v
Update JSONB
|
v
FastBut production may have:
500 concurrent requests
|
v
JSONB updates
|
v
Database contentionRun concurrency tests such as:
1
10
50
100
500and measure:
Throughput
P95 latency
P99 latency
Lock waits
Database CPUTest Concurrent Reads and Writes
A realistic API rarely performs only reads or only writes.
A workload might be:
80% Reads
15% Updates
5% InsertsAnother application might be:
50% Reads
40% Updates
10% InsertsRun mixed workloads.
Clients
|
+---- Read
+---- Read
+---- Update
+---- Read
+---- Insert
+---- UpdateThis provides a much better representation of production behavior.
Benchmark Connection Pooling
A slow JSONB API is not always caused by query execution.
Connection pool exhaustion can create latency even when individual SQL statements are fast.
Measure:
Connection acquisition time
Query execution time
Total database call timeFor example:
Connection wait: 80 ms
Query execution: 10 ms
Total: 90 msOptimizing the SQL alone will not solve the problem.
Measure Serialization Overhead
The JSONB data has to move through multiple representations:
PostgreSQL JSONB
|
v
.NET Object
|
v
JSON ResponseFor large documents, serialization and deserialization can consume meaningful CPU.
Measure:
Database time
EF Core materialization time
Serialization time
Total API timeThis helps identify the actual bottleneck.
Benchmark Cold and Warm Cache
Database caching can significantly affect results.
Run separate tests for:
Cold cache
Warm cacheA query might show:
Cold: 300 ms
Warm: 20 msBoth numbers can be useful.
The important requirement is consistency when comparing PostgreSQL versions or schema designs.
Test Query Plans
For important JSONB queries, capture execution plans.
A conceptual benchmark might record:
Query:
Filter customers by region
Scan:
Index Scan
Estimated Rows:
100,000
Actual Rows:
98,500
Execution:
42 msAfter changing the PostgreSQL version or index:
Scan:
Sequential Scan
Estimated Rows:
100,000
Actual Rows:
98,500
Execution:
740 msThis is a strong regression signal.
Watch Estimated Versus Actual Rows
JSONB queries can be sensitive to data distribution.
Suppose PostgreSQL estimates:
Estimated: 1,000
Actual: 2,000,000The optimizer may select an inappropriate execution strategy.
Record:
Estimated rows
Actual rows
Planning time
Execution timeand compare them across benchmark environments.
Benchmark Different Selectivity Levels
A query returning 1% of a table has a different performance profile from one returning 80%.
Test:
Highly selective
Moderately selective
Low selectivityFor example:
Region = rare value
Region = common valueThis helps identify when PostgreSQL should use an index and when a sequential scan may be more efficient.
Use a Benchmark Matrix
A serious benchmark can combine several dimensions.
Database Size
|
+---- 1M
+---- 10M
+---- 100M
Document Size
|
+---- 1 KB
+---- 10 KB
+---- 50 KB
Concurrency
|
+---- 1
+---- 50
+---- 500
Query Selectivity
|
+---- High
+---- Medium
+---- LowThis creates a much more realistic performance profile.
Example Benchmark Results
A benchmark report might look like:
| Workload | Baseline | New Version | Change |
|---|---|---|---|
| JSONB point lookup | 8 ms | 9 ms | +12.5% |
| Region filter | 42 ms | 45 ms | +7.1% |
| Nested property search | 85 ms | 92 ms | +8.2% |
| Array search | 110 ms | 118 ms | +7.3% |
| Large document read | 140 ms | 170 ms | +21.4% |
| Bulk JSONB update | 620 ms | 690 ms | +11.3% |
These numbers are illustrative. Your benchmark should use your own workload and environment.
Define Regression Thresholds
A practical policy might classify results as:
Green:
< 10% degradation
Yellow:
10–25% degradation
Red:
> 25% degradationBut thresholds should also consider absolute latency.
For example:
5 ms -> 8 msmay be acceptable.
While:
800 ms -> 1,100 msmay require immediate investigation even if the workload is infrequent.
Test JSONB Schema Evolution
Flexible JSON does not eliminate schema changes.
Suppose version one contains:
{
"region": "north"
}and version two introduces:
{
"region": "north",
"segment": "enterprise"
}Your application may need to support both.
Benchmark:
Old document
New document
Mixed document populationMixed schemas are particularly important during gradual deployments.
Test Missing Properties
Some documents may not contain a field.
For example:
{
"region": "north"
}while another contains:
{
"region": "north",
"preferences": {
"language": "en"
}
}The API should behave predictably when nested fields are absent.
Performance tests should include these cases because null or missing-value handling can affect query behavior.
Test Large JSONB Documents
Large JSON documents can create unexpected application costs.
Benchmark documents containing:
1 KB
10 KB
50 KB
100 KB
500 KBand measure:
Insert
Read
Update
Network transfer
Serialization
MemoryAt some point, the JSONB document may be carrying information that should be modeled separately.
Benchmarking helps identify that boundary.
Monitor Memory Usage
Large JSONB documents can increase memory usage at several levels:
PostgreSQL
|
v
Network Buffer
|
v
.NET
|
v
EF Core
|
v
JSON SerializerMeasure application memory during large-document workloads.
A query can be fast while still causing unacceptable memory pressure.
Test Pagination With JSONB Filters
An API might expose:
GET /customers?region=north&page=100Benchmark different pages.
Page 1
Page 10
Page 100
Page 1,000A JSONB filter combined with deep pagination can expose inefficient plans that are not visible in simple first-page tests.
Compare Offset and Keyset Pagination
For large datasets, compare:
OFFSET + LIMITagainst keyset-style pagination.
Conceptually:
Offset pagination
|
v
Skip many rows
|
v
Return pageversus:
Keyset pagination
|
v
Start after known key
|
v
Return next pageThe benchmark should use production-like page depths.
Create a .NET Benchmark Harness
A simple harness can execute the same EF Core query repeatedly.
public async Task<BenchmarkResult> RunAsync(
int customerId)
{
var stopwatch = Stopwatch.StartNew();
var result = await dbContext.Customers
.Where(x => x.Id == customerId)
.Select(x => new
{
x.Id,
x.Name,
x.Metadata.Region
})
.FirstOrDefaultAsync();
stopwatch.Stop();
return new BenchmarkResult
{
Duration = stopwatch.Elapsed,
Found = result != null
};
}Run multiple iterations and discard obvious warm-up effects.
Then calculate:
P50
P95
P99
Minimum
Maximum
ThroughputAvoid Benchmarking From a Developer Laptop
Application-level measurements from a local machine can contain unrelated noise.
For database upgrade testing, use controlled environments.
Benchmark Client
|
v
Controlled Network
|
v
PostgreSQL InstanceKeep CPU, memory, network, connection settings, and dataset consistent where practical.
Build a Regression Pipeline
The benchmark can become part of database upgrade testing.
Schema
|
v
Load Dataset
|
v
Prepare Statistics
|
v
Run EF Core Workload
|
v
Capture Plans
|
v
Compare Results
|
+---- Pass
|
+---- InvestigateThis prevents performance validation from becoming a manual activity performed only before major releases.
Common Mistakes
Storing Everything in JSONB
JSONB is flexible, but frequently queried fields may be better represented as relational columns.
Benchmarking Only Small Documents
Large documents can change the performance characteristics significantly.
Measuring Only SQL Execution Time
API latency also includes connection acquisition, network transfer, materialization, and serialization.
Ignoring Index Write Costs
Indexes improve some reads while adding storage and write overhead.
Testing Only One Parameter
Different values can produce different plans and execution times.
Using Unrealistic Data
Uniform synthetic data can hide real production selectivity problems.
Ignoring Concurrent Workloads
A query that performs well alone may behave differently under contention.
Treating JSONB as a Free Schema
Flexible structure still requires versioning, validation, and migration discipline.
Best Practices
Benchmark Real Query Patterns
Start with the operations your APIs actually perform.
Use Production-Like Data
Data size and distribution matter.
Measure Both Database and API Performance
Find out whether the bottleneck is PostgreSQL, EF Core, serialization, or the network.
Test Index Strategies
Compare indexing approaches with real workloads.
Measure Read and Write Costs
Do not optimize reads in isolation.
Test Multiple Document Sizes
JSONB performance changes as documents become larger.
Include Concurrency
Measure behavior under realistic traffic.
Capture Execution Plans
Latency tells you that something changed; the plan often helps explain why.
Track P95 and P99
Tail latency matters for APIs.
Automate Regression Detection
Make JSONB performance testing part of database upgrade and schema-change workflows.
A Practical JSONB Benchmark Checklist
Before approving a PostgreSQL upgrade or major JSONB schema change:
[ ] Identify critical JSONB queries
[ ] Capture EF Core generated SQL
[ ] Prepare production-like data
[ ] Test multiple document sizes
[ ] Test scalar property filters
[ ] Test nested properties
[ ] Test arrays
[ ] Compare index strategies
[ ] Test inserts
[ ] Test updates
[ ] Test reads
[ ] Test mixed workloads
[ ] Test concurrency
[ ] Test pagination
[ ] Capture execution plans
[ ] Compare estimated and actual rows
[ ] Measure P50/P95/P99
[ ] Measure API payload size
[ ] Measure serialization overhead
[ ] Test schema evolution
[ ] Define regression thresholds
[ ] Automate benchmark reportingConclusion
PostgreSQL JSONB is a powerful option for .NET applications that need flexible data structures, but its performance depends heavily on workload design. The same JSONB model can perform well for one application and become expensive for another depending on document size, query selectivity, indexing, update frequency, and concurrency.
For EF Core applications, the most useful benchmark does not stop at measuring SQL execution time. It follows the complete request path from the .NET API through EF Core and PostgreSQL and measures database execution, network transfer, materialization, serialization, and end-to-end latency.
When evaluating a PostgreSQL upgrade, capture a baseline first, use the same production-like dataset, compare execution plans, test multiple document sizes and parameter distributions, and measure both reads and writes. Most importantly, benchmark the queries your APIs actually execute rather than relying on isolated database tests.
The goal is not to prove that JSONB is faster than relational modeling. The goal is to understand where JSONB works well, where it needs careful indexing, and where frequently accessed data should move into a more predictable relational structure. That evidence makes PostgreSQL and EF Core architecture decisions much easier to defend before a workload reaches production.

Join the conversation! Your thoughts help the community grow.