PostgreSQL  

PostgreSQL 18 AIO: Benchmarking .NET Database Workloads

Database performance problems in .NET applications are often blamed on the application layer.

A slow API may appear to be caused by:

  • Entity Framework Core

  • LINQ queries

  • Connection pooling

  • JSON serialization

  • Thread-pool contention

But sometimes the real bottleneck is much lower in the stack: database I/O.

PostgreSQL 18 introduces a new asynchronous I/O (AIO) subsystem that allows PostgreSQL to issue multiple I/O operations concurrently instead of waiting for each operation sequentially. PostgreSQL documents AIO support for workloads including sequential scans, bitmap heap scans, and vacuum.

For .NET developers, the interesting question is not simply:

Is PostgreSQL 18 faster?

The more useful question is:

Which .NET database workloads actually benefit from PostgreSQL 18 AIO, and how should we measure the difference?

This article builds a practical benchmark methodology using PostgreSQL, C#, Npgsql, and BenchmarkDotNet.

What PostgreSQL 18 AIO Changes

Traditional synchronous I/O can be thought of as:

Request I/O
    ↓
Wait
    ↓
Receive data
    ↓
Process
    ↓
Request next I/O

AIO allows the database to initiate multiple I/O operations without waiting for every individual operation to finish before continuing.

Conceptually:

Request I/O 1 ────────┐
Request I/O 2 ────────┤
Request I/O 3 ────────┤
Request I/O 4 ────────┘
        ↓
Process available results

PostgreSQL describes AIO as separating the initiation of an I/O operation from waiting for its result, allowing multiple operations to be initiated concurrently.

This matters most when the workload is actually I/O-bound.

A query that spends most of its time on CPU processing may see little benefit from AIO.

PostgreSQL 18 AIO Execution Methods

PostgreSQL 18 exposes the io_method configuration parameter.

The documented options are:

worker
io_uring
sync

worker uses PostgreSQL worker processes, io_uring uses Linux io_uring when PostgreSQL was built with the required support, and sync executes AIO-eligible operations synchronously. The documented default is worker.

You can inspect the current configuration with:

SHOW io_method;

You can also inspect related I/O settings:

SHOW io_method;
SHOW io_workers;

The exact available settings depend on the PostgreSQL version and build.

Why .NET Developers Should Care

The .NET application does not directly call PostgreSQL AIO.

The architecture looks like this:

.NET Application
       ↓
Npgsql
       ↓
PostgreSQL Protocol
       ↓
PostgreSQL Executor
       ↓
PostgreSQL AIO
       ↓
Operating System
       ↓
Storage

This distinction is important.

Changing:

await connection.ExecuteAsync(...);

does not automatically enable PostgreSQL AIO.

AIO is a PostgreSQL server-side execution capability.

Npgsql remains responsible for communication between the .NET application and PostgreSQL.

AIO Is Not the Same as .NET Async

This distinction causes confusion.

In .NET:

await command.ExecuteReaderAsync();

means the application can asynchronously wait for the database operation.

PostgreSQL AIO means PostgreSQL can manage eligible storage I/O asynchronously.

These are different layers:

Application Layer
    ↓
.NET async/await
    ↓
Npgsql
    ↓
Database Layer
    ↓
PostgreSQL AIO
    ↓
Storage

You can use both at the same time.

They solve different problems.

Build a Reproducible Benchmark

A useful benchmark should compare the same workload under controlled PostgreSQL configurations.

For example:

Configuration A
io_method = sync

Configuration B
io_method = worker

Configuration C
io_method = io_uring

Do not change other major variables between runs.

Record:

PostgreSQL version
.NET version
Npgsql version
CPU
RAM
Storage type
Operating system
Dataset size
Row count
Indexes
PostgreSQL configuration
io_method
io_workers
Cache state
Concurrency

Without these details, benchmark numbers are difficult to reproduce.

Create a Representative Table

For example:

CREATE TABLE orders
(
    id          BIGINT GENERATED ALWAYS AS IDENTITY,
    customer_id BIGINT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL,
    status      TEXT NOT NULL,
    amount      NUMERIC(12, 2) NOT NULL,
    description TEXT
);

For an I/O-oriented benchmark, the table should be large enough that the workload can exercise storage rather than fitting entirely into memory.

The appropriate size depends on the machine being tested.

Do not choose a fixed row count and assume it represents production.

Generate Test Data

A simple PostgreSQL-side generator can create repeatable data:

INSERT INTO orders
(
    customer_id,
    created_at,
    status,
    amount,
    description
)
SELECT
    (random() * 100000)::bigint,
    NOW() - (random() * INTERVAL '365 days'),
    CASE
        WHEN random() < 0.25 THEN 'Pending'
        WHEN random() < 0.50 THEN 'Processing'
        WHEN random() < 0.75 THEN 'Shipped'
        ELSE 'Completed'
    END,
    round((random() * 1000)::numeric, 2),
    repeat('benchmark-data ', 10)
FROM generate_series(1, 10000000);

The exact dataset size should be selected based on the hardware and benchmark objective.

After loading data, update statistics:

ANALYZE orders;

PostgreSQL uses statistics during query planning, so benchmark runs should not accidentally compare a freshly loaded and poorly analyzed table against a properly analyzed table.

Start With a Sequential Scan

AIO is particularly relevant to sequential scans.

Start with:

SELECT COUNT(*)
FROM orders;

Then inspect the execution plan:

EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*)
FROM orders;

Look for:

Seq Scan
Buffers
Execution Time
Planning Time

The goal is not simply to obtain one execution-time number.

You want to understand what the database actually did.

Compare Query Plans

A benchmark should establish that the query plan is comparable between configurations.

For example:

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM orders
WHERE created_at >= NOW() - INTERVAL '30 days';

Record:

Plan type
Rows scanned
Rows returned
Buffers
Execution time

If one configuration uses a different plan, the comparison is no longer simply an AIO comparison.

The planner may have made a different decision.

Understand Cache Effects

This is one of the biggest problems in database benchmarking.

Run 1 may read data from storage:

Application
    ↓
PostgreSQL
    ↓
Storage

Run 2 may find much of the same data in memory:

Application
    ↓
PostgreSQL
    ↓
Memory

The second run may therefore be much faster without any AIO change.

PostgreSQL's pg_stat_io and related statistics can help investigate I/O behavior, although PostgreSQL notes that its I/O statistics do not distinguish every case of data coming from disk versus the operating system page cache.

Therefore, benchmark both:

Cold-ish cache scenario
Warm-cache scenario

and clearly document how each was produced.

Do not call an ordinary repeated execution a "cold-cache benchmark."

Inspect PostgreSQL I/O Statistics

PostgreSQL 18 provides pg_stat_io.

A starting query is:

SELECT
    backend_type,
    object,
    context,
    reads,
    read_time,
    writes,
    write_time
FROM pg_stat_io;

The available columns should be checked against the exact PostgreSQL version being tested.

The purpose is to correlate:

Query performance
+
Database I/O activity

rather than relying only on application latency.

Benchmark From .NET

For a .NET application, Npgsql is the PostgreSQL ADO.NET provider.

A basic query method can look like:

using Npgsql;

public static async Task<long> CountOrdersAsync(
    string connectionString)
{
    await using var connection =
        new NpgsqlConnection(connectionString);

    await connection.OpenAsync();

    await using var command =
        new NpgsqlCommand(
            """
            SELECT COUNT(*)
            FROM orders;
            """,
            connection);

    var result =
        await command.ExecuteScalarAsync();

    return Convert.ToInt64(result);
}

This benchmark measures the end-to-end database call from the .NET application's perspective.

That is useful because it includes:

Npgsql
+
Network
+
PostgreSQL execution
+
Result transfer

It does not isolate PostgreSQL storage time by itself.

That is why application-level measurements should be combined with PostgreSQL execution plans and I/O statistics.

Add BenchmarkDotNet

BenchmarkDotNet provides a structured way to execute repeated .NET benchmarks.

A basic benchmark can look like:

using BenchmarkDotNet.Attributes;
using Npgsql;

[MemoryDiagnoser]
public class PostgreSqlBenchmark
{
    private NpgsqlDataSource _dataSource = default!;

    [GlobalSetup]
    public void Setup()
    {
        var connectionString =
            Environment.GetEnvironmentVariable(
                "POSTGRES_CONNECTION")
            ?? throw new InvalidOperationException(
                "POSTGRES_CONNECTION is missing.");

        _dataSource =
            NpgsqlDataSource.Create(
                connectionString);
    }

    [Benchmark]
    public async Task<long> CountOrders()
    {
        await using var command =
            _dataSource.CreateCommand(
                """
                SELECT COUNT(*)
                FROM orders;
                """);

        var result =
            await command.ExecuteScalarAsync();

        return Convert.ToInt64(result);
    }

    [GlobalCleanup]
    public async Task Cleanup()
    {
        await _dataSource.DisposeAsync();
    }
}

Then run:

dotnet run -c Release

BenchmarkDotNet can provide statistical summaries rather than relying on a single execution.

Avoid Measuring Connection Creation

Do not accidentally benchmark:

Create connection
Open connection
Execute query
Close connection

when your production application normally uses pooled connections.

For example:

await using var command =
    dataSource.CreateCommand(sql);

await command.ExecuteScalarAsync();

using a shared NpgsqlDataSource better represents a typical pooled application architecture.

The benchmark should reflect the behavior you actually want to compare.

Benchmark Multiple Workload Types

Do not use one query to conclude that AIO improves every workload.

Test different categories.

Sequential Scan

SELECT COUNT(*)
FROM orders;

This is a natural starting point because PostgreSQL explicitly lists sequential scans among AIO-supported operations.

Large Result Scan

SELECT
    id,
    customer_id,
    created_at,
    status,
    amount
FROM orders;

This tests a different result-transfer profile.

Filtered Scan

SELECT COUNT(*)
FROM orders
WHERE status = 'Completed';

The planner may choose different strategies depending on indexes and statistics.

Bitmap Heap Scan

Create an index:

CREATE INDEX ix_orders_customer_id
ON orders(customer_id);

Then test:

SELECT *
FROM orders
WHERE customer_id BETWEEN 1000 AND 5000;

Inspect the plan:

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM orders
WHERE customer_id BETWEEN 1000 AND 5000;

PostgreSQL 18 specifically lists bitmap heap scans among AIO-supported workloads.

Do Not Disable Indexes Without a Reason

You may be tempted to force a sequential scan:

SET enable_indexscan = off;
SET enable_bitmapscan = off;

This can be useful for controlled experiments.

But do not use forced plans to represent normal production behavior unless that is actually the production plan.

A benchmark should distinguish between:

Natural production plan

and:

Artificially forced plan

Benchmark Different Concurrency Levels

Single-user performance is not enough for a web API.

Test:

1 concurrent request
8 concurrent requests
32 concurrent requests
64 concurrent requests
128 concurrent requests

The exact values should match the hardware and expected workload.

The objective is to observe how throughput and latency behave as concurrency increases.

For example:

Concurrency
    ↓
Database workload
    ↓
I/O pressure
    ↓
Latency
    ↓
Throughput

A configuration that performs well at concurrency 1 may behave differently under load.

Measure More Than Average Latency

Average latency can hide important behavior.

Record:

Mean
Median
P95
P99
Throughput
Errors
CPU utilization
I/O utilization
Memory

For an API workload, P95 and P99 are often more useful than the average alone.

However, do not claim that one metric is universally the correct production metric.

The appropriate SLO depends on the application.

Compare End-to-End and Database Timing

Suppose the .NET application records:

Total request:
250 ms

while PostgreSQL reports:

Execution:
220 ms

The remaining time may include:

Connection acquisition
Network
Serialization
Application processing

This is why both layers matter.

A useful diagnostic model is:

API latency
    |
    +-- Connection pool
    +-- Network
    +-- PostgreSQL execution
    +-- Result transfer
    +-- Application processing

Do not attribute the entire API latency difference to PostgreSQL AIO.

Measure Connection Pool Behavior

Npgsql connection pooling can influence application-level results.

If the benchmark creates a new physical database connection for every operation, you may measure connection overhead rather than database execution.

Use a long-lived NpgsqlDataSource:

var dataSource =
    NpgsqlDataSource.Create(
        connectionString);

Then create commands from the data source.

This allows the provider's connection pooling to operate normally.

Test EF Core Separately

If the application uses Entity Framework Core, create a separate benchmark layer.

For example:

public async Task<int> CountOrdersAsync()
{
    return await dbContext.Orders.CountAsync();
}

Then compare:

EF Core
    ↓
Npgsql
    ↓
PostgreSQL

against:

Npgsql
    ↓
PostgreSQL

This helps determine whether a result is caused by:

ORM behavior

or:

Database execution

Do not assume that an EF Core benchmark is automatically a PostgreSQL benchmark.

Keep LINQ Translation Constant

If comparing PostgreSQL configurations, do not simultaneously change:

LINQ expression
EF Core version
Npgsql version
Tracking behavior
Query projection

Otherwise, multiple variables change at once.

A clean experiment changes one major variable:

PostgreSQL I/O configuration

while keeping the application stack constant.

Example EF Core Query

A read-only query might look like:

var count =
    await dbContext.Orders
        .AsNoTracking()
        .Where(x => x.Status == "Completed")
        .CountAsync();

If this is part of the benchmark, capture the generated SQL and verify that it remains equivalent across runs.

Benchmark Vacuum Separately

PostgreSQL 18 AIO also applies to vacuum operations.

You can measure:

VACUUM (ANALYZE) orders;

But vacuum benchmarks should be treated differently from request/response query benchmarks.

Measure:

Duration
I/O activity
CPU
Impact on concurrent queries

Do not mix vacuum execution time into a normal API query latency benchmark.

Compare sync and worker

For a controlled experiment, configure:

ALTER SYSTEM SET io_method = 'sync';

Restart PostgreSQL as required.

Then verify:

SHOW io_method;

Repeat the benchmark.

Next, test:

io_method = worker

and, where supported by the PostgreSQL build and operating system:

io_method = io_uring

The official PostgreSQL documentation states that io_method is a server-start setting and that io_uring requires PostgreSQL to be built with the required liburing support.

Do not assume io_uring is available on every PostgreSQL installation.

Do Not Assume io_uring Is Always Faster

A common benchmark mistake is:

io_uring
   ↓
Modern
   ↓
Must be fastest

That conclusion is unsupported without measurement.

Performance depends on:

Storage
Kernel
CPU
Workload
Dataset
Cache state
Concurrency
PostgreSQL configuration

The correct approach is:

Measure
    ↓
Compare
    ↓
Explain

not:

Feature exists
    ↓
Assume performance improvement

Account for PostgreSQL Version

Use a clear comparison.

For example:

PostgreSQL 17
vs
PostgreSQL 18

If the goal is specifically to measure AIO, you can also compare PostgreSQL 18 configurations:

PostgreSQL 18
io_method = sync

PostgreSQL 18
io_method = worker

PostgreSQL 18
io_method = io_uring

This helps separate:

Version-level differences

from:

AIO-method differences

Use the Same Dataset

A benchmark is invalid if one configuration uses:

10 million rows

and another uses:

20 million rows

Keep:

Schema
Data
Indexes
Statistics

consistent.

For repeatability, generate the dataset from a documented script.

Watch for Storage-Level Bottlenecks

AIO cannot manufacture storage throughput.

If the underlying disk is saturated:

Application
    ↓
PostgreSQL
    ↓
AIO
    ↓
Storage limit

the storage device may remain the bottleneck.

Record storage metrics where possible:

Read throughput
Write throughput
IOPS
Latency
Queue depth

The exact metrics depend on the operating system and storage platform.

Monitor CPU

AIO can change how efficiently I/O and CPU work overlap.

Therefore, record CPU utilization.

For example:

Before:
CPU = low
Disk = saturated

After:
CPU = higher
Disk = better utilized

That could represent improved I/O utilization.

But if:

CPU = saturated
Disk = underutilized

the bottleneck may have moved to CPU.

This is why performance benchmarking should identify bottlenecks rather than chase one metric.

Monitor PostgreSQL Wait Behavior

When diagnosing a slow query, execution time alone does not explain why it is slow.

Combine:

EXPLAIN (ANALYZE, BUFFERS)
+
pg_stat_io
+
OS metrics
+
Application latency

This provides a more complete picture.

Use EXPLAIN for Every Benchmark Query

A benchmark result without its query plan is difficult to interpret.

Use:

EXPLAIN (ANALYZE, BUFFERS, WAL)
SELECT COUNT(*)
FROM orders;

The exact options should match the workload.

For read-only queries, BUFFERS is particularly useful.

For write-heavy workloads, WAL information can also be valuable.

Example Benchmark Workflow

A repeatable workflow can be:

1. Provision identical environment
        ↓
2. Install PostgreSQL
        ↓
3. Configure io_method
        ↓
4. Create schema
        ↓
5. Load identical dataset
        ↓
6. ANALYZE tables
        ↓
7. Record query plan
        ↓
8. Warm up application
        ↓
9. Run benchmark
        ↓
10. Capture PostgreSQL I/O statistics
        ↓
11. Capture OS metrics
        ↓
12. Repeat for next configuration
        ↓
13. Compare results

Do not skip the warm-up and measurement separation.

Recommended Benchmark Matrix

Workloadsyncworkerio_uring
Sequential scanMeasureMeasureMeasure
Large result scanMeasureMeasureMeasure
Bitmap heap scanMeasureMeasureMeasure
Filtered queryMeasureMeasureMeasure
Concurrent readsMeasureMeasureMeasure
VacuumMeasureMeasureMeasure

Not every workload will benefit from AIO.

That is exactly why the matrix is useful.

Example Results Table

Do not fill this table with invented numbers.

Instead, populate it from your own benchmark environment:

WorkloadI/O MethodMeanP95P99ThroughputRead I/O
Sequential scansyncMeasureMeasureMeasureMeasureMeasure
Sequential scanworkerMeasureMeasureMeasureMeasureMeasure
Sequential scanio_uringMeasureMeasureMeasureMeasureMeasure
Bitmap heap scansyncMeasureMeasureMeasureMeasureMeasure
Bitmap heap scanworkerMeasureMeasureMeasureMeasureMeasure
Bitmap heap scanio_uringMeasureMeasureMeasureMeasureMeasure

This is preferable to presenting benchmark figures that cannot be reproduced.

What Would Count as a Meaningful Result?

A meaningful conclusion might be:

Under the tested storage and dataset configuration,
the worker-based AIO configuration reduced P95 latency
for the sequential-scan workload.

That is a defensible statement.

An unsupported conclusion would be:

PostgreSQL 18 AIO makes all .NET applications 3x faster.

The PostgreSQL project itself notes performance improvements in certain scenarios, but those results are workload-dependent rather than a universal application-level guarantee.

Common Benchmarking Mistakes

Benchmarking Only One Query

One query cannot represent an application's workload.

Ignoring Cache State

Warm and cold behavior can be dramatically different.

Changing Multiple Variables

Do not change PostgreSQL version, query, indexes, and application code simultaneously if the goal is to isolate AIO.

Measuring Only Average Latency

Tail latency may expose behavior hidden by the mean.

Ignoring Query Plans

A changed query plan can invalidate an AIO comparison.

Measuring Connection Creation

This can hide database execution differences.

Running Tiny Datasets

If everything fits comfortably in memory, you may not meaningfully exercise storage I/O.

Assuming io_uring Is Available

It depends on the PostgreSQL build and environment.

Treating Vendor Benchmarks as Your Results

Published benchmark results are useful context.

They are not measurements of your infrastructure.

Publishing Results Without Environment Details

A benchmark number without hardware, software, dataset, and configuration information is difficult to reproduce.

Troubleshooting

io_uring Is Not Available

Check:

SHOW io_method;

Also verify how PostgreSQL was built and whether the required liburing support is available. PostgreSQL documents io_uring as requiring a build with liburing support.

AIO Shows No Improvement

That is a valid result.

Check whether the workload is:

CPU-bound
Cache-bound
Network-bound
Storage-bound

If the workload is not limited by eligible I/O operations, AIO may not produce a measurable improvement.

Results Change Between Runs

Investigate:

Cache state
Concurrent activity
Background vacuum
CPU frequency
Storage load
Dataset changes

Run multiple iterations and document the environment.

PostgreSQL Uses a Different Query Plan

Compare:

EXPLAIN (ANALYZE, BUFFERS)

for each configuration.

Planner differences can make an AIO comparison misleading.

.NET Latency Changes but PostgreSQL Time Does Not

Look outside PostgreSQL:

Connection pooling
Network
Application scheduling
Serialization
Result processing

The database may not be responsible for the observed difference.

pg_stat_io Does Not Match Disk Metrics

This is not necessarily an error.

PostgreSQL's I/O statistics and operating-system storage metrics observe different layers. PostgreSQL notes that its I/O statistics do not distinguish all disk versus kernel-page-cache cases.

Best Practices

  1. Benchmark the actual workload rather than a synthetic query alone.

  2. Keep schema and dataset identical across configurations.

  3. Record PostgreSQL, .NET, and Npgsql versions.

  4. Record CPU, memory, storage, and operating system details.

  5. Compare sync, worker, and io_uring where supported.

  6. Verify the query plan with EXPLAIN.

  7. Measure warm and cold-cache scenarios separately.

  8. Measure concurrency, not only single-request performance.

  9. Record P95 and P99 latency.

  10. Capture PostgreSQL I/O statistics.

  11. Monitor CPU and storage utilization.

  12. Use connection pooling consistently.

  13. Keep the .NET application code unchanged when isolating database behavior.

  14. Run multiple benchmark iterations.

  15. Do not publish fabricated or environment-independent performance claims.

  16. Document benchmark methodology so others can reproduce it.

  17. Test sequential scans and bitmap heap scans separately.

  18. Treat vacuum as a separate workload.

  19. Distinguish database execution time from end-to-end API latency.

  20. Re-test after PostgreSQL upgrades and infrastructure changes.

Frequently Asked Questions

What is PostgreSQL 18 AIO?

PostgreSQL 18 introduces an asynchronous I/O subsystem that allows PostgreSQL to initiate multiple eligible I/O operations concurrently rather than waiting for each one sequentially. PostgreSQL lists sequential scans, bitmap heap scans, and vacuum among the workloads supported by AIO.

Is PostgreSQL AIO the same as C# async/await?

No.

C# async/await controls asynchronous application operations.

PostgreSQL AIO controls how PostgreSQL handles eligible database I/O internally.

They operate at different layers.

Does Npgsql need special AIO code?

PostgreSQL AIO is server-side.

An ordinary Npgsql query can therefore benefit when the PostgreSQL server executes an eligible workload using its AIO subsystem.

The .NET application does not explicitly call PostgreSQL AIO APIs.

Is io_uring always the fastest option?

No.

It should be benchmarked.

Performance depends on the operating system, PostgreSQL build, storage, workload, concurrency, and other environmental factors.

Which queries benefit most?

PostgreSQL specifically identifies sequential scans, bitmap heap scans, and vacuum as AIO-supported workloads.

The actual performance benefit must be measured for the workload.

How do I check the active AIO method?

Run:

SHOW io_method;

You can also inspect:

SHOW io_workers;

How should I benchmark PostgreSQL AIO from .NET?

Use a stable Npgsql data source, execute representative queries repeatedly, and measure application latency with a tool such as BenchmarkDotNet.

At the same time, collect PostgreSQL execution plans and I/O statistics.

Should I benchmark EF Core or Npgsql?

Ideally both separately.

An Npgsql benchmark helps isolate database communication and execution.

An EF Core benchmark shows the behavior of the complete ORM-to-database path.

Why might AIO produce no measurable improvement?

Possible reasons include:

Workload is CPU-bound
Data is already cached
Query uses a different execution path
Storage is not the bottleneck
Dataset is too small
Concurrency is too low
The workload does not exercise AIO-supported operations

A zero or negligible improvement is still useful information when the experiment is properly controlled.

Conclusion

PostgreSQL 18's asynchronous I/O subsystem is an important database-engine change, but the most useful way for .NET developers to evaluate it is through measurement rather than assumptions.

The relevant architecture is:

.NET
  ↓
Npgsql
  ↓
PostgreSQL
  ↓
AIO
  ↓
Operating System
  ↓
Storage

A proper benchmark should therefore measure more than:

Query completed in X milliseconds

Instead, correlate:

Application latency
+
Query execution plan
+
Buffer activity
+
PostgreSQL I/O statistics
+
CPU utilization
+
Storage utilization
+
Concurrency

PostgreSQL 18 provides multiple AIO execution methods, including worker, io_uring, and sync, allowing controlled comparisons when the environment supports them.

The most important benchmark principle is:

Do not benchmark the feature. Benchmark the workload.

A sequential scan on a large dataset, a bitmap heap scan, a cached lookup, and a CPU-heavy query can behave very differently.

For .NET teams, the strongest experiment is therefore:

Representative workload
        ↓
Controlled PostgreSQL configuration
        ↓
Repeatable Npgsql benchmark
        ↓
EXPLAIN + BUFFERS
        ↓
pg_stat_io
        ↓
OS-level metrics
        ↓
Latency + throughput analysis
        ↓
Evidence-based conclusion

PostgreSQL 18 provides the AIO infrastructure. Whether your application benefits—and by how much—is something your workload and your benchmark environment must determine.

That is the difference between a feature demonstration and a production-performance investigation.