Stateful applications are one of the areas where Kubernetes deployments become more complicated than they initially appear. Running a stateless API is relatively straightforward, but databases, file-processing services, and other workloads need storage that survives pod restarts and rescheduling.

.NET Aspire 13.5 adds support for modeling Kubernetes persistent volumes directly in the AppHost. You can define storage capacity, storage class, and access mode alongside the workload that consumes the storage. Aspire can then generate the corresponding persistent volume claim and, for workloads such as PostgreSQL, model the deployment as a StatefulSet.

This creates an interesting opportunity for .NET developers: instead of treating Kubernetes storage as a completely separate deployment concern, storage requirements can become part of the application's infrastructure model.

This article explains how persistent storage works with Aspire 13.5 and presents a practical benchmarking approach for measuring storage behavior.

Why Persistent Storage Matters in Kubernetes

A container filesystem should generally not be treated as durable application storage.

Consider a PostgreSQL container:

Pod
 |
 +-- PostgreSQL
      |
      +-- /var/lib/postgresql/data

If the database writes data only to the container's writable filesystem, replacing the container can also remove the data.

A persistent volume separates application data from the lifecycle of the individual pod.

                  Kubernetes
                      |
              Persistent Volume
                      |
                Persistent
               Volume Claim
                      |
                    Pod
                      |
                 PostgreSQL

The important distinction is that the pod can be replaced while the storage remains available to the replacement workload.

Kubernetes persistent volumes are storage resources managed through the Kubernetes API and can outlive an individual pod. They are particularly relevant to stateful workloads such as databases.

What .NET Aspire 13.5 Adds

Aspire 13.5 introduces APIs for modeling persistent volumes for Kubernetes environments.

A basic example is:

var builder = DistributedApplication.CreateBuilder(args);

var k8s = builder.AddKubernetesEnvironment("k8s");

var data = k8s.AddPersistentVolume("data")
    .WithStorageClass("managed-csi")
    .WithCapacity("20Gi")
    .WithAccessMode(PersistentVolumeAccessMode.ReadWriteOnce);

builder.AddContainer("postgres", "postgres:16")
    .WithVolume("data", "/var/lib/postgresql/data")
    .WithPersistentVolume(data);

builder.Build().Run();

The important part is that the storage definition is close to the workload definition.

The configuration specifies:

The release documentation also notes that the persistent-volume APIs are experimental in Aspire 13.5 and use the ASPIRECOMPUTE002 diagnostic. That should be considered when evaluating the feature for production use.

Understanding the Storage Configuration

Storage Class

The storage class determines the storage implementation Kubernetes should use.

.WithStorageClass("managed-csi")

The actual storage class must exist in the target Kubernetes cluster. Therefore, an AppHost configuration should not assume that every cluster provides the same storage classes.

A development cluster and a production cluster may use different storage implementations.

Capacity

Capacity expresses how much storage the workload requests:

.WithCapacity("20Gi")

The requested capacity should reflect the application's expected data requirements rather than simply selecting a large value.

For benchmarking, it is also useful to keep capacity consistent between test runs so that the storage configuration does not become another variable affecting the comparison.

Access Mode

The access mode describes how the volume can be mounted.

For example:

.WithAccessMode(
    PersistentVolumeAccessMode.ReadWriteOnce)

ReadWriteOnce is appropriate for workloads where the volume is intended to be mounted for read/write access by a single node.

The correct access mode depends on the storage provider and application architecture. It should not be selected simply because it appears in an example.

Persistent Volume vs Container Storage

The difference becomes clearer when comparing the two approaches.

CharacteristicContainer FilesystemPersistent Volume
Pod lifecycle dependencyHighLower
Data survives pod replacementNot reliablyDesigned for persistence
Suitable for database dataGenerally unsuitableAppropriate
Storage managed separatelyNoYes
Kubernetes storage configurationMinimalRequired
Stateful workload supportLimitedStronger

For stateful workloads, persistent storage provides a much more appropriate lifecycle boundary than the container filesystem.

Designing a Storage Benchmark

The title of this article focuses on benchmarking, but there is an important research gap: the available release material describes the capability and configuration but does not provide a standardized performance benchmark comparing different storage classes or configurations.

Therefore, performance numbers should not be presented as established results.

Instead, developers can create their own controlled benchmark.

A useful benchmark should measure several dimensions.

1. Sequential Write Performance

Measure how quickly the workload can write a large amount of sequential data.

For example:

Write 1 GB
Write 5 GB
Write 10 GB

2. Sequential Read Performance

Read the same data back and measure throughput.

3. Random I/O

Database workloads frequently perform smaller reads and writes rather than only large sequential operations.

Measure:

4 KB random reads
4 KB random writes

or another workload size appropriate to the application.

4. Latency

Throughput alone does not tell the entire story.

Measure:

A storage configuration with high throughput but poor latency characteristics may still perform badly for transactional workloads.

Example Benchmark Workflow

A repeatable experiment could follow these steps.

  1. Create a Kubernetes test environment.

  2. Configure Aspire 13.5 with a persistent volume.

  3. Deploy the same application configuration.

  4. Run a warm-up workload.

  5. Execute the benchmark multiple times.

  6. Record throughput and latency.

  7. Delete and recreate the pod.

  8. Verify that the data remains available.

  9. Repeat using another storage configuration.

  10. Compare the results.

The important point is consistency.

Do not change the application image, database configuration, workload size, CPU allocation, memory allocation, and storage configuration simultaneously. Otherwise, it becomes difficult to determine what caused a performance difference.

Testing Persistence After Pod Replacement

Performance is only one part of the experiment.

A persistent volume should also be tested for data durability across workload lifecycle events.

For example:

Create data
   |
   v
Write records
   |
   v
Verify records
   |
   v
Delete/restart workload
   |
   v
Workload recreated
   |
   v
Verify records again

For a database workload, the verification query might be as simple as:

SELECT COUNT(*)
FROM Orders;

The expected result should remain consistent after the workload is recreated, assuming the storage lifecycle and Kubernetes configuration are designed to preserve the data.

Benchmarking With PostgreSQL

PostgreSQL is a useful example because it is stateful and has clear read/write workloads.

The Aspire configuration can associate the persistent storage directly with the PostgreSQL workload:

var data = k8s.AddPersistentVolume("postgres-data")
    .WithStorageClass("managed-csi")
    .WithCapacity("20Gi")
    .WithAccessMode(PersistentVolumeAccessMode.ReadWriteOnce);

builder.AddContainer("postgres", "postgres:16")
    .WithVolume("postgres-data", "/var/lib/postgresql/data")
    .WithPersistentVolume(data);

The storage path is important:

/var/lib/postgresql/data

This is where PostgreSQL stores its database files in the container image used by this example.

The benchmark should use the same PostgreSQL configuration across all storage experiments. Otherwise, database configuration changes can obscure the actual storage behavior.

What to Measure

A useful benchmark report can contain a table such as:

MetricTest ATest BTest C
Sequential writeMeasureMeasureMeasure
Sequential readMeasureMeasureMeasure
Random write latencyMeasureMeasureMeasure
Random read latencyMeasureMeasureMeasure
Median latencyMeasureMeasureMeasure
High-percentile latencyMeasureMeasureMeasure
Persistence after restartPass/FailPass/FailPass/Fail

The values should come from actual measurements performed in the target environment.

This is preferable to publishing generic numbers because Kubernetes storage performance depends on the underlying storage implementation, cluster configuration, workload, and environment.

Common Mistakes

Assuming a Persistent Volume Is Automatically Fast

Persistence and performance are different properties.

A persistent volume can protect data without necessarily providing the throughput or latency required by a particular workload.

Using the Same Storage Class Everywhere

A storage class available in one cluster may not exist in another.

Validate the target cluster before deploying the AppHost configuration.

Benchmarking Only Throughput

A database workload can be heavily affected by latency and random I/O.

Measure the characteristics that actually represent your application's workload.

Changing Multiple Variables

If the storage class, CPU limits, memory limits, database configuration, and application version all change between tests, the benchmark becomes difficult to interpret.

Treating Experimental APIs as Stable

The persistent-volume APIs in Aspire 13.5 are identified as experimental. Teams should account for that status when deciding how aggressively to depend on the API in production infrastructure.

Troubleshooting

Persistent Volume Claim Does Not Bind

Check whether the requested storage class exists:

kubectl get storageclass

Then inspect the persistent volume claims:

kubectl get pvc

A claim that remains pending may indicate a storage-class, capacity, access-mode, or cluster-provisioning issue.

Data Is Missing After Restart

Check the actual volume configuration and mount path.

For PostgreSQL, verify that the persistent volume is mounted at the directory where PostgreSQL stores its data.

Also distinguish between restarting a pod and deleting the underlying storage resource. A persistent volume cannot protect data if the storage resource itself is intentionally deleted according to its lifecycle policy.

Benchmark Results Vary Significantly

First check the test environment.

Look for:

Repeat the test and record the environment alongside the results.

Advantages and Disadvantages

Advantages

Disadvantages

Conclusion

.NET Aspire 13.5 makes Kubernetes persistent storage easier to express as part of an application's infrastructure model. A developer can define the storage class, capacity, access mode, and workload relationship directly in the AppHost instead of treating persistent storage as an entirely separate concern.

The more interesting question, however, is not simply whether persistent volumes work. It is how different storage configurations behave under the application's real workload.

That is where benchmarking becomes valuable. Measure sequential and random I/O, latency, throughput, and data persistence after workload replacement. Keep the environment controlled and publish actual measurements rather than assumed performance numbers.

For teams evaluating Aspire 13.5, this provides a practical path from "the application can use persistent storage" to a much more useful engineering question: "Does this storage configuration provide the durability and performance characteristics our workload actually requires?"