<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[ASP Today]]></title><description><![CDATA[Expert insights into Microsoft ASP.NET, classic ASP, and the latest web development trends.]]></description><link>https://www.asptoday.com</link><image><url>https://substackcdn.com/image/fetch/$s_!LonC!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58c47c8-e39c-48e4-826a-d05e9ca9d537_509x509.jpeg</url><title>ASP Today</title><link>https://www.asptoday.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 16 Oct 2026 11:37:12 GMT</lastBuildDate><atom:link href="https://www.asptoday.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Matthew Pomar]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[asptoday@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[asptoday@substack.com]]></itunes:email><itunes:name><![CDATA[Matthew Pomar]]></itunes:name></itunes:owner><itunes:author><![CDATA[Matthew Pomar]]></itunes:author><googleplay:owner><![CDATA[asptoday@substack.com]]></googleplay:owner><googleplay:email><![CDATA[asptoday@substack.com]]></googleplay:email><googleplay:author><![CDATA[Matthew Pomar]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Contract-First API Development in ASP.NET Core: OpenAPI, Client Generation, and Schema Governance]]></title><description><![CDATA[Build reliable APIs with OpenAPI, generated clients, and schema governance. #ASPToday #aspnetcore #dotnet #openapi]]></description><link>https://www.asptoday.com/p/contract-first-api-development-in</link><guid isPermaLink="false">https://www.asptoday.com/p/contract-first-api-development-in</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 13 Oct 2026 15:03:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!uzqR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>An API is more than a collection of endpoints. It is an agreement between the applications that produce data and the applications that consume it. When that agreement changes unexpectedly, mobile apps fail, frontend integrations break, and independent development teams lose confidence in the services they depend on. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uzqR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uzqR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!uzqR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!uzqR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!uzqR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uzqR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2147008,&quot;alt&quot;:&quot;Developers review an OpenAPI contract connecting generated clients, API endpoints, and schema governance. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/219523950?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Developers review an OpenAPI contract connecting generated clients, API endpoints, and schema governance. " title="Developers review an OpenAPI contract connecting generated clients, API endpoints, and schema governance. " srcset="https://substackcdn.com/image/fetch/$s_!uzqR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!uzqR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!uzqR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!uzqR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcec5282f-f603-4c7d-97ff-601d75b6309a_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Contract-first API development solves this problem by defining the API&#8217;s structure and behavior before implementing its endpoints. With <em><a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a></em>, OpenAPI, generated clients, automated contract checks, and schema governance, teams can build APIs that evolve predictably without forcing every consumer to change at the same time.</p><h2>Why API Contracts Become Important as Systems Grow</h2><p>Imagine a development team building an online shopping platform.</p><p>The backend team maintains an ASP.NET Core API. A separate team develops the website. Another team builds the mobile application. Several external partners use the same API to retrieve product information and submit orders.</p><p>Initially, coordination is simple.</p><p>A backend developer creates an endpoint:</p><pre><code><code>GET /api/products/42</code></code></pre><p>It returns:</p><pre><code><code>{
  "id": 42,
  "name": "Wireless Headphones",
  "price": 89.99
}</code></code></pre><p>The frontend team integrates the response, and everything works.</p><p>Three months later, a backend developer decides that <code>price</code> is too vague. The application now supports multiple currencies, so the response changes:</p><pre><code><code>{
  "id": 42,
  "name": "Wireless Headphones",
  "pricing": {
    "amount": 89.99,
    "currency": "USD"
  }
}</code></code></pre><p>The new structure is arguably better.</p><p>Unfortunately, the mobile application still expects:</p><pre><code><code>product.price</code></code></pre><p>That property no longer exists.</p><p>The backend developer improved the design but accidentally broke an existing consumer.</p><p>The problem was not JSON, HTTP, or ASP.NET Core.</p><p>The problem was that the API contract changed without a controlled compatibility process.</p><p>Contract-first development establishes that process before implementation begins.</p><h2>What Is Contract-First API Development?</h2><p>In a traditional code-first workflow, developers implement endpoints and generate documentation from the resulting application.</p><p>The sequence looks like this:</p><pre><code><code>Write endpoint
      &#8595;
Define request and response models
      &#8595;
Generate API description
      &#8595;
Consumers discover the contract</code></code></pre><p>Contract-first development reverses the initial relationship.</p><pre><code><code>Define API contract
      &#8595;
Review and approve contract
      &#8595;
Generate clients and supporting code
      &#8595;
Implement ASP.NET Core endpoints
      &#8595;
Verify implementation against contract
      &#8595;
Publish the approved contract</code></code></pre><p>The API description becomes a shared design artifact.</p><p>Backend developers know what they must implement.</p><p>Frontend developers know what responses to expect.</p><p>Mobile developers can generate typed clients before the server is complete.</p><p>Test engineers can design contract tests against an agreed specification.</p><p>Architecture teams can review naming, security, pagination, and compatibility before implementation decisions become expensive.</p><p>This does not mean developers stop writing code.</p><p>It means the contract becomes the starting point instead of an accidental byproduct of implementation.</p><h2>OpenAPI Provides a Common Language</h2><p>OpenAPI is a widely adopted specification for describing HTTP APIs.</p><p>An OpenAPI document describes elements such as request paths, HTTP methods, parameters, request bodies, response structures, status codes, authentication requirements, and reusable schemas.</p><p>It is commonly written in YAML or JSON.</p><p>For example, our product endpoint might be described as:</p><pre><code><code>openapi: 3.0.3

info:
  title: Product API
  version: 1.0.0

paths:
  /api/products/{id}:
    get:
      operationId: getProductById
      summary: Retrieve a product

      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int32

      responses:
        '200':
          description: Product found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'

        '404':
          description: Product not found

components:
  schemas:
    Product:
      type: object
      required:
        - id
        - name
        - price

      properties:
        id:
          type: integer
          format: int32

        name:
          type: string

        price:
          type: number
          format: decimal</code></code></pre><p>This example uses OpenAPI 3.0.3. The same contract-first principles also apply to OpenAPI 3.1 and newer versions, although their schema rules and tooling compatibility differ.</p><p>Notice what this document accomplishes.</p><p>Before writing the endpoint, we have established its URL, HTTP method, input parameter, successful response, missing-resource response, and product representation.</p><p>A frontend developer can read this contract without examining ASP.NET Core source code.</p><p>A client generator can use it to produce typed methods.</p><p>A contract test can use it to validate the implemented endpoint.</p><p>Most importantly, the team has something concrete to review before development begins.</p><h2>Designing the Contract Before Writing Code</h2><p>Contract-first development works best when the team treats API design as a deliberate engineering activity. </p><p>In our previous guide on <em><a href="https://www.asptoday.com/p/advanced-api-design-in-aspnet-core">Advanced API Design in ASP.NET Core</a></em>, we explored how consistent API structures, predictable responses, and clear design conventions make integrations easier to maintain. Contract-first development takes those principles further by establishing the agreement before implementation begins.</p><p>Consider an order management API.</p><p>Before implementation, the team should answer several questions.</p><p>What resource does the endpoint represent?</p><p>Should creating an order use <code>POST</code>?</p><p>What information must the caller provide?</p><p>What happens when the order is invalid?</p><p>Which fields are required?</p><p>What response should successful creation return?</p><p>Can clients safely retry the operation?</p><p>How will authentication work?</p><p>How will the API evolve?</p><p>For example:</p><pre><code><code>POST /api/orders</code></code></pre><p>Request:</p><pre><code><code>{
  "customerId": 125,
  "items": [
    {
      "productId": 42,
      "quantity": 2
    }
  ]
}</code></code></pre><p>Successful response:</p><pre><code><code>HTTP/1.1 201 Created
Location: /api/orders/8102</code></code></pre><pre><code><code>{
  "orderId": 8102,
  "status": "pending",
  "total": 179.98
}</code></code></pre><p>The team can agree on these details before the backend and frontend implementations begin.</p><p>This is particularly valuable when different teams work on independent release schedules.</p><h2>Designing Consistent Request and Response Schemas</h2><p>A good contract defines more than property names.</p><p>It establishes expectations about data types, required fields, nullability, validation, and structure.</p><p>Consider:</p><pre><code><code>OrderRequest:
  type: object
  required:
    - customerId
    - items

  properties:
    customerId:
      type: integer
      format: int32

    items:
      type: array
      minItems: 1
      items:
        $ref: '#/components/schemas/OrderItem'

OrderItem:
  type: object
  required:
    - productId
    - quantity

  properties:
    productId:
      type: integer
      format: int32

    quantity:
      type: integer
      minimum: 1</code></code></pre><p>This makes several decisions explicit.</p><p>An order must include a customer identifier.</p><p>The items collection must contain at least one item.</p><p>Each item must identify a product.</p><p>Quantity cannot be less than one.</p><p>Those constraints can inform validation in ASP.NET Core, generated clients, and automated testing.</p><p>However, there is an important distinction.</p><p>Describing a constraint in OpenAPI does not automatically guarantee that every ASP.NET Core endpoint enforces it at runtime.</p><p>The implementation must still validate incoming requests. Contract tests should verify that actual behavior matches the published specification.</p><h2>Implementing an Approved Contract in ASP.NET Core</h2><p>Once the contract is reviewed, the backend team can implement it.</p><p>ASP.NET Core supports generating OpenAPI documents for HTTP endpoints through its OpenAPI integration.</p><p>For a modern Minimal API application, a simplified setup looks like this:</p><pre><code><code>var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet(
    "/api/products/{id:int}",
    (int id) =&gt;
    {
        if (id != 42)
        {
            return Results.NotFound();
        }

        return Results.Ok(new ProductResponse(
            42,
            "Wireless Headphones",
            89.99m));
    })
    .WithName("getProductById")
    .Produces&lt;ProductResponse&gt;(StatusCodes.Status200OK)
    .Produces(StatusCodes.Status404NotFound);

app.Run();

public record ProductResponse(
    int Id,
    string Name,
    decimal Price);</code></code></pre><p>The <code>AddOpenApi()</code> method registers OpenAPI document generation services, while <code>MapOpenApi()</code> exposes the generated document endpoint.</p><p>For this example, the project must reference the appropriate <code>Microsoft.AspNetCore.OpenApi</code> package.</p><p>The generated document can then be compared with the approved contract.</p><p>This is a critical distinction.</p><p>ASP.NET Core&#8217;s generated OpenAPI document describes the implemented application. It does not automatically prove that the implementation conforms to an independently authored contract.</p><p>A contract-first pipeline must explicitly compare the two or test runtime behavior against the approved specification.</p><p>For implementation details, see the official ASP.NET Core OpenAPI documentation.</p><h2>OpenAPI Generation Is Not Contract Governance</h2><p>Generating an OpenAPI document is useful, but it is not the same as governing an API contract.</p><p>Imagine that a developer changes:</p><pre><code><code>public record ProductResponse(
    int Id,
    string Name,
    decimal Price);</code></code></pre><p>to:</p><pre><code><code>public record ProductResponse(
    int Id,
    string Name,
    decimal Amount);</code></code></pre><p>ASP.NET Core may generate a perfectly valid updated OpenAPI document.</p><p>But the generated document now describes a different response.</p><p>Existing consumers expecting <code>price</code> may break.</p><p>Nothing about successful document generation guarantees backward compatibility.</p><p>A governed contract-first workflow asks a different question:</p><blockquote><p>Does the new implementation still satisfy the API agreement that consumers rely upon?</p></blockquote><p>That question requires comparison, review, and testing.</p><h2>Client Generation Removes Repetitive Integration Work</h2><p>One of the biggest practical benefits of OpenAPI is client generation.</p><p>Without generated clients, frontend developers often write HTTP integration code manually.</p><p>For example:</p><pre><code><code>async function getProduct(id) {
  const response = await fetch(`/api/products/${id}`);

  if (!response.ok) {
    throw new Error("Request failed");
  }

  return await response.json();
}</code></code></pre><p>This is manageable for one endpoint. </p><p>But imagine an application with 150 endpoints. </p><p>Every consumer must manage URLs, HTTP methods, serialization, authentication, response types, and errors. </p><p>Duplicated integration code becomes a maintenance problem. </p><p>OpenAPI-based generators can produce clients in languages such as C#, TypeScript, Java, and others. </p><p>Tools commonly used for this include NSwag, OpenAPI Generator, and Kiota, depending on the target ecosystem and API design. </p><h2>Generating a C# Client With NSwag</h2><p>Suppose our approved contract is stored at: </p><pre><code><code>contracts/product-api.yaml</code></code></pre><p>A development team can use NSwag to generate a C# client. </p><p>For example, after installing the appropriate NSwag CLI: </p><pre><code><code>nswag openapi2csclient \
  /input:contracts/product-api.yaml \
  /classname:ProductApiClient \
  /namespace:Company.ProductApi.Client \
  /output:Generated/ProductApiClient.cs</code></code></pre><p>The generated client can expose typed methods and response models based on the contract. </p><p>Instead of constructing raw HTTP requests throughout the application, consumers work through a consistent client abstraction. </p><p>This can reduce manual serialization code, improve discoverability, and make contract changes easier to identify during compilation. </p><p>Generated code is not a replacement for testing, however. </p><p>A generated client can accurately implement a contract that the server fails to honor. </p><p>Both sides still need validation. </p><h2>Why Operation IDs Matter</h2><p>Consider this OpenAPI operation: </p><pre><code><code>operationId: getProductById</code></code></pre><p>The operation ID gives the endpoint a stable logical identity. </p><p>Client generators frequently use operation IDs to derive method names. </p><p>For example: </p><pre><code><code>await client.GetProductByIdAsync(42);</code></code></pre><p>Without deliberate operation IDs, generated method names may depend on generator-specific naming conventions. </p><p>That can produce awkward or unstable client APIs. </p><p>Imagine a developer changing an endpoint summary or route and unexpectedly triggering a generated method rename. </p><p>A stable operation ID helps reduce that risk. </p><p>For contract-first development, operation IDs should be treated as part of the client-facing design. </p><h2>Generated Clients Should Be Reproducible</h2><p>A reliable development process does not depend on one developer manually generating client code on their laptop. </p><p>Client generation should be repeatable. </p><p>For example: </p><pre><code><code>Approved OpenAPI contract
          &#8595;
Pinned generator version
          &#8595;
Client generation
          &#8595;
Compile generated clients
          &#8595;
Run integration tests
          &#8595;
Publish client package</code></code></pre><p>The generator version matters. </p><p>Different versions may produce different naming conventions, serialization behavior, or generated structures. </p><p>Pinning versions makes builds more predictable. </p><p>Teams should also avoid manually editing generated files. </p><p>Those changes will usually disappear the next time the client is regenerated. </p><p>Custom behavior belongs in supported extension points, wrappers, partial classes where available, or separately maintained application code. </p><h2>Contract-First Development Enables Parallel Work</h2><p>Imagine a team developing a new subscription feature. </p><p>The backend team needs to implement billing rules. </p><p>The frontend team needs to build the subscription management interface. </p><p>The mobile team needs to display subscription status. </p><p>Without an agreed contract, everyone waits for the backend implementation. </p><p>With contract-first development, the teams can agree on: </p><pre><code><code>GET /api/subscriptions/{id}</code></code></pre><p>Response:</p><pre><code><code>{
  "subscriptionId": 205,
  "plan": "premium",
  "status": "active",
  "renewalDate": "2026-12-01"
}</code></code></pre><p>The frontend team can generate its client and develop against mock responses. </p><p>The mobile team can do the same. </p><p>The backend team can implement the actual endpoint independently. </p><p>Once implementation is complete, integration testing confirms whether the service satisfies the shared contract. </p><p>This reduces unnecessary coordination and makes parallel delivery more practical. </p><h2>Mock Servers Help Consumers Start Early</h2><p>An OpenAPI contract can also support mock server generation. </p><p>A mock server returns responses based on the contract without requiring the real business logic or database. </p><p>For example:</p><pre><code><code>Frontend application
         &#8595;
Generated API client
         &#8595;
Mock server
         &#8595;
Example responses</code></code></pre><p>This is especially useful when backend implementation is still underway.</p><p>However, mock servers have limitations.</p><p>A schema-valid response does not necessarily reproduce real business behavior.</p><p>For example, a mock server may return an active subscription without modeling expiration, payment failures, or cancellation.</p><p>Mocks accelerate development.</p><p>They do not replace end-to-end integration testing.</p><h2>Schema Governance Prevents Accidental API Drift</h2><p>As organizations grow, API contracts need ownership.</p><p>Without governance, different teams make inconsistent decisions.</p><p>One service uses:</p><pre><code><code>{
  "customerId": 100
}</code></code></pre><p>Another uses:</p><pre><code><code>{
  "customer_id": 100
}</code></code></pre><p>A third uses:</p><pre><code><code>{
  "customerID": 100
}</code></code></pre><p>All three can work.</p><p>But inconsistency makes integration harder.</p><p>Schema governance establishes shared expectations.</p><p>These might include naming conventions, error responses, authentication descriptions, pagination patterns, versioning policies, operation IDs, and deprecation procedures.</p><p>The goal is not bureaucracy.</p><p>The goal is to make API behavior predictable across teams.</p><h2>Establish an API Style Guide</h2><p>A practical API style guide should answer common design questions.</p><p>For example, the organization may decide that JSON properties use camelCase.</p><p>Collection endpoints use a consistent pagination structure.</p><p>Errors use a common response format.</p><p>Resource identifiers use consistent naming.</p><p>Successful resource creation returns HTTP 201 where appropriate.</p><p>Authentication requirements are documented in the contract.</p><p>Every operation has a stable operation ID.</p><p>These conventions reduce repeated debates and help generated clients behave consistently.</p><p>A good style guide should remain small enough that developers can actually follow it.</p><h2>Use Standard Error Responses</h2><p>Error handling is a common source of API inconsistency.</p><p>One endpoint returns:</p><pre><code><code>{
  "error": "Invalid order"
}</code></code></pre><p>Another returns:</p><pre><code><code>{
  "message": "Order validation failed",
  "code": 400
}</code></code></pre><p>A third returns a plain string.</p><p>Consumers now need several error-handling strategies.</p><p>ASP.NET Core supports standardized problem details responses based on RFC 7807 and related specifications.</p><p>A response might look like:</p><pre><code><code>{
  "type": "https://example.com/problems/validation",
  "title": "Validation failed",
  "status": 400,
  "detail": "Quantity must be greater than zero."
}</code></code></pre><p>Contract-first teams can define consistent error schemas and document which status codes each operation returns.</p><p>That improves both client generation and error-handling predictability.</p><h2>Breaking Changes Are the Biggest Governance Challenge</h2><p>Consider the original response:</p><pre><code><code>{
  "id": 42,
  "name": "Wireless Headphones",
  "price": 89.99
}</code></code></pre><p>Now imagine the backend changes it to:</p><pre><code><code>{
  "id": 42,
  "name": "Wireless Headphones",
  "amount": 89.99
}</code></code></pre><p>The response is still valid JSON.</p><p>The endpoint still returns HTTP 200.</p><p>Unit tests may still pass.</p><p>But existing consumers may fail because the expected <code>price</code> property disappeared.</p><p>This is a breaking change.</p><p>Other common breaking changes include removing endpoints, changing HTTP methods, introducing new required request fields, changing property types, tightening accepted input values, or changing authentication requirements.</p><p>A contract-first workflow must detect these changes before deployment.</p><h2>Not Every Schema Change Is Backward-Compatible</h2><p>Adding an optional response property is often compatible with clients that ignore unknown fields.</p><p>But not every consumer behaves that way.</p><p>Some clients use strict deserialization or exhaustive schema validation.</p><p>Similarly, adding a new enum value can break consumers that assume the list of possible values never changes.</p><p>Consider:</p><pre><code><code>{
  "status": "pending"
}</code></code></pre><p>A client may expect only:</p><pre><code><code>pending
completed
cancelled</code></code></pre><p>Later, the server introduces:</p><pre><code><code>partially_fulfilled</code></code></pre><p>The schema change may appear additive, but some generated clients or application logic may not handle the new value correctly.</p><p>Compatibility depends on both the contract and the behavior of existing consumers.</p><p>This is why governance needs explicit compatibility rules rather than simplistic assumptions.</p><h2>Automate Breaking-Change Detection</h2><p>Manual review is important, but automated checks make the process repeatable.</p><p>A useful pipeline looks like this:</p><pre><code><code>Developer proposes contract change
              &#8595;
Validate OpenAPI syntax
              &#8595;
Run API style checks
              &#8595;
Compare with released contract
              &#8595;
Detect potential breaking changes
              &#8595;
Review and approve
              &#8595;
Generate clients
              &#8595;
Implement and test</code></code></pre><p>Tools such as <code>oasdiff</code> can compare OpenAPI documents and identify many classes of breaking changes.</p><p>For example:</p><pre><code><code>oasdiff breaking \
  contracts/released.yaml \
  contracts/proposed.yaml</code></code></pre><p>A CI pipeline can fail when a prohibited change is detected.</p><p>However, schema comparison tools cannot prove every aspect of behavioral compatibility.</p><p>They may not detect changes to business rules, sorting behavior, side effects, or undocumented assumptions.</p><p>Automated comparison is a safeguard, not a complete replacement for contract testing and review.</p><h2>Contract Testing Verifies Real Behavior</h2><p>A published OpenAPI document may describe:</p><pre><code><code>GET /api/products/42</code></code></pre><p>as returning:</p><pre><code><code>200 OK</code></code></pre><p>with a particular JSON schema.</p><p>But what if the implementation actually returns a different structure?</p><p>The document is valid.</p><p>The implementation compiles.</p><p>The generated client compiles.</p><p>Yet the contract is violated.</p><p>Contract testing closes this gap.</p><p>A test sends requests to the running API and verifies that responses satisfy the approved specification.</p><p>For example, tests can verify that successful responses include required fields, that property types match the schema, that documented status codes are used, and that invalid requests are rejected appropriately.</p><p>Some teams also use consumer-driven contract testing, where consumers publish the expectations they rely on and providers verify those expectations.</p><p>The central principle remains the same.</p><p>The contract must describe what the API actually does, not merely what developers intended it to do.</p><h2>Versioning Is a Compatibility Strategy</h2><p>Eventually, some breaking changes are unavoidable.</p><p>Perhaps a product API must adopt a new pricing model.</p><p>The old response is:</p><pre><code><code>{
  "price": 89.99
}</code></code></pre><p>The new response is:</p><pre><code><code>{
  "pricing": {
    "amount": 89.99,
    "currency": "USD"
  }
}</code></code></pre><p>One strategy is to introduce a new API version.</p><p>For example:</p><pre><code><code>GET /api/v1/products/42</code></code></pre><p>and:</p><pre><code><code>GET /api/v2/products/42</code></code></pre><p>Existing clients continue using version 1 while newer clients adopt version 2.</p><p>But versioning should not become an excuse to introduce unnecessary breaking changes.</p><p>Maintaining multiple versions increases testing, documentation, security, and operational costs.</p><p>The preferred approach is to evolve contracts compatibly whenever practical and introduce new versions when a genuinely incompatible change is required.</p><h2>Deprecation Must Be Managed Deliberately</h2><p>Publishing version 2 does not automatically mean version 1 can disappear.</p><p>Mobile applications may remain installed for years.</p><p>External partners may upgrade slowly.</p><p>Enterprise customers may follow fixed release schedules.</p><p>A responsible deprecation process communicates which API versions are affected, what consumers need to change, when support will end, and how migrations should proceed.</p><p>The organization should also measure actual usage of older versions.</p><p>Removing an API because a deadline passed is risky if critical consumers are still using it.</p><p>Good governance combines policy with operational evidence.</p><h2>Security Requirements Belong in the Contract</h2><p>API contracts should document authentication requirements.</p><p>For example, an OpenAPI document can describe bearer-token authentication:</p><pre><code><code>components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

security:
  - bearerAuth: []</code></code></pre><p>This helps client generators and documentation tools understand how callers authenticate.</p><p>However, describing security in OpenAPI does not enforce it.</p><p>ASP.NET Core must still implement authentication and authorization correctly.</p><p>A contract may say that an endpoint requires a bearer token, but only the application&#8217;s middleware, endpoint policies, and supporting infrastructure can enforce that requirement.</p><p>Security documentation and runtime security must remain aligned.</p><h2>Governance Should Not Slow Every Small Change</h2><p>One concern with contract-first development is that it can become overly bureaucratic.</p><p>Imagine requiring an architecture committee meeting whenever a developer adds an optional response field.</p><p>That would quickly make developers avoid the process.</p><p>A better approach uses automation for routine checks.</p><p>Syntax validation runs automatically.</p><p>Style rules run automatically.</p><p>Breaking-change detection runs automatically.</p><p>Client generation runs automatically.</p><p>Human review focuses on meaningful design decisions and exceptions.</p><p>The process should become stricter as the risk increases, not simply as the number of endpoints grows.</p><h2>Organizing Contracts Across Multiple Teams</h2><p>In a larger organization, API contracts need a predictable home.</p><p>A repository might contain:</p><pre><code><code>api-contracts/
&#9474;
&#9500;&#9472;&#9472; catalog/
&#9474;   &#9500;&#9472;&#9472; v1/
&#9474;   &#9474;   &#9492;&#9472;&#9472; openapi.yaml
&#9474;   &#9492;&#9472;&#9472; v2/
&#9474;       &#9492;&#9472;&#9472; openapi.yaml
&#9474;
&#9500;&#9472;&#9472; orders/
&#9474;   &#9492;&#9472;&#9472; v1/
&#9474;       &#9492;&#9472;&#9472; openapi.yaml
&#9474;
&#9492;&#9472;&#9472; payments/
    &#9492;&#9472;&#9472; v1/
        &#9492;&#9472;&#9472; openapi.yaml</code></code></pre><p>This structure makes contract ownership and version history visible.</p><p>However, a single central repository is not mandatory.</p><p>Some organizations keep contracts alongside their services and publish approved versions to an API registry or artifact repository.</p><p>Either model can work.</p><p>What matters is that consumers can identify the authoritative contract version and retrieve it reliably.</p><h2>Contract Ownership Is as Important as Contract Storage</h2><p>A contract without an owner becomes difficult to evolve.</p><p>Who approves breaking changes?</p><p>Who reviews security requirements?</p><p>Who decides when an old version can be retired?</p><p>Who maintains generated client packages?</p><p>Who investigates mismatches between the specification and implementation?</p><p>These responsibilities should be explicit.</p><p>For example, a service team may own its API contract while a platform team maintains shared linting rules and CI templates.</p><p>That arrangement balances team autonomy with organization-wide consistency.</p><h2>Treat Contracts as Versioned Artifacts</h2><p>An API contract should not exist only as an editable document on a shared drive.</p><p>Approved contracts should have version history.</p><p>A consumer should be able to identify which contract version its generated client came from.</p><p>A deployment pipeline should know which contract version the service claims to implement.</p><p>A breaking-change check should compare proposed changes with an appropriate released baseline.</p><p>This creates traceability between API design, implementation, generated clients, and deployed behavior.</p><h2>Integrating Contract Checks Into CI/CD</h2><p>A practical contract-first CI pipeline might follow this sequence:</p><pre><code><code>OpenAPI change submitted
           &#8595;
Validate document
           &#8595;
Apply style rules
           &#8595;
Compare against released contract
           &#8595;
Review compatibility
           &#8595;
Generate clients
           &#8595;
Compile generated clients
           &#8595;
Run implementation tests
           &#8595;
Run contract tests
           &#8595;
Publish approved artifacts</code></code></pre><p>Not every project needs all these stages immediately.</p><p>In our earlier article on <em><a href="https://www.asptoday.com/p/utilizing-azure-devops-for-cicd-in">CI/CD Pipelines with Azure DevOps for ASP.NET Core</a></em>, we examined how automated build, testing, and deployment workflows improve release reliability. Adding OpenAPI validation, breaking-change detection, and contract testing extends those safeguards to the APIs that other applications depend on.</p><p>A small team can begin with OpenAPI validation, breaking-change detection, and one integration test suite.</p><p>As the number of consumers increases, client generation, artifact publishing, and compatibility policies become more valuable.</p><p>The important thing is to make the process repeatable.</p><h2>A Real-World Example: An Order Management Platform</h2><p>Consider an ASP.NET Core platform with three services.</p><p>The Catalog API provides product information.</p><p>The Orders API creates and tracks purchases.</p><p>The Payments API processes payments.</p><p>The website, mobile application, and partner integrations consume these services.</p><p>Without contract-first development, each team may independently interpret request and response structures.</p><p>An Orders developer changes a field.</p><p>The website adapts immediately.</p><p>The mobile team discovers the change after its next release.</p><p>A partner integration breaks in production.</p><p>Now consider the contract-first approach.</p><p>The Orders team proposes a schema change in a pull request.</p><p>Automated checks identify that an existing response property is being removed.</p><p>The team decides to preserve the original property temporarily while introducing the new structure.</p><p>Generated clients are updated against the approved contract.</p><p>Integration tests verify that the implementation supports the documented responses.</p><p>Consumers migrate at their own pace.</p><p>The old property is removed only through an agreed deprecation or versioning process.</p><p>The technical change is still necessary.</p><p>But its consequences are managed rather than discovered accidentally.</p><h2>Common Contract-First API Mistakes</h2><p>The first mistake is assuming that generating OpenAPI documentation automatically makes an API contract-first.</p><p>It does not. A generated specification is useful, but contract-first development requires the contract to guide implementation and undergo compatibility checks.</p><p>Another mistake is treating every schema-valid change as backward-compatible. Client behavior, strict validation, and business assumptions can still create breaking changes.</p><p>Some teams generate clients but never test them against the actual service. That creates confidence in code generation without verifying runtime compatibility.</p><p>Others allow developers to manually edit generated clients, creating changes that disappear during regeneration.</p><p>Another common problem is creating a governance process that requires excessive approval for routine changes.</p><p>Finally, teams sometimes publish contracts without assigning ownership or establishing deprecation policies.</p><p>The solution is not more documentation.</p><p>It is a small, enforceable process connecting design, implementation, testing, and release.</p><h2>Contract-First Development Is About Predictability</h2><p>The greatest benefit of contract-first development is not YAML.</p><p>It is predictability.</p><p>Backend developers know what they are implementing.</p><p>Frontend developers know what they can rely on.</p><p>Generated clients reduce repetitive integration work.</p><p>Contract tests expose mismatches.</p><p>Schema comparisons identify risky changes.</p><p>Governance makes API evolution deliberate.</p><p>For a single application with one backend and one frontend, some of these practices may seem unnecessary.</p><p>But as teams, services, devices, and external integrations multiply, the cost of unmanaged API changes rises quickly.</p><p>Contract-first development helps prevent that cost from becoming a permanent source of production failures and coordination overhead.</p><h2>Coming Next</h2><p>In the next article, we&#8217;ll explore Building Asynchronous APIs in ASP.NET Core: Long-Running Operations and the 202 Accepted Pattern.</p><p>Not every operation can finish within a normal HTTP request.</p><p>Some jobs take seconds, minutes, or even hours.</p><p>We&#8217;ll examine how to accept work without keeping clients waiting, return HTTP 202 Accepted, expose operation-status endpoints, handle background processing safely, and design reliable APIs for long-running operations.</p><h2>Closing Thoughts</h2><p>An API contract is a promise.</p><p>It tells consumers which operations exist, what information they must provide, what responses they can expect, and how failures are represented.</p><p>When that promise changes unexpectedly, even a technically correct backend deployment can break the applications that depend on it.</p><p>Contract-first API development makes the promise explicit before implementation begins.</p><p>OpenAPI provides a common description format.</p><p>Client generation turns that description into usable integration code.</p><p>Contract testing verifies that implementations honor the agreement.</p><p>Schema governance helps teams detect breaking changes, enforce consistency, and manage API evolution responsibly.</p><p>The objective is not to make API development slower or more formal than necessary.</p><p>It is to make change safer.</p><p>A well-designed API is not simply one that works today. It is one that consumers can continue trusting as the system evolves.</p><h2>Subscribe Now</h2><p>Enjoying the series? <em><a href="https://www.asptoday.com/">Subscribe to ASP Today</a></em> for practical ASP.NET Core tutorials, production architecture guidance, and advanced API engineering strategies. Join our Substack Chat to discuss OpenAPI, contract-first development, client generation, schema governance, API compatibility, and building dependable integrations across modern .NET applications. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Benchmarking ASP.NET Core Applications: BenchmarkDotNet, Load Testing, and Finding Real Bottlenecks]]></title><description><![CDATA[Measure real ASP.NET Core performance with benchmarks, load tests, and bottleneck analysis. #ASPToday #aspnetcore #dotnet #performance]]></description><link>https://www.asptoday.com/p/benchmarking-aspnet-core-applications</link><guid isPermaLink="false">https://www.asptoday.com/p/benchmarking-aspnet-core-applications</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 06 Oct 2026 15:01:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!x94P!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Performance optimization without measurement is mostly guesswork. A method may look expensive but account for almost none of your application&#8217;s total response time. A clever optimization may make a benchmark 40 percent faster while having no measurable effect on production. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!x94P!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!x94P!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!x94P!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!x94P!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!x94P!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!x94P!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2772620,&quot;alt&quot;:&quot;Aircraft undergoes wind tunnel testing as engineers measure performance under extreme conditions. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/218735157?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Aircraft undergoes wind tunnel testing as engineers measure performance under extreme conditions. " title="Aircraft undergoes wind tunnel testing as engineers measure performance under extreme conditions. " srcset="https://substackcdn.com/image/fetch/$s_!x94P!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!x94P!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!x94P!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!x94P!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6649cb8-1e49-4433-adae-99105c45b73f_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>At the same time, a slow database query, blocked Thread Pool, excessive allocation, or overloaded downstream service may be quietly determining the capacity of the entire system. Benchmarking gives us a disciplined way to separate these situations. In <em><a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a></em>, that means knowing when to use BenchmarkDotNet, when to load-test the complete application, which numbers actually matter, and how to move from &#8220;the application feels slow&#8221; to a specific bottleneck we can prove and fix.</p><h2>We Have Spent Several Articles Making Things Faster</h2><p>Over the previous parts of this series, we have explored several performance techniques.</p><p>We reduced unnecessary allocations.</p><p>We examined garbage collection and object pooling.</p><p>We streamed large data instead of buffering everything.</p><p>We used pipelines to move data efficiently.</p><p>We designed backpressure so overloaded applications fail gracefully instead of collapsing.</p><p>Most recently, we explored Native AOT for faster startup and leaner deployment.</p><p>Every one of those techniques can improve a system.</p><p>But there is a problem.</p><p>How do we know that <strong>your</strong> application needs them?</p><p>Consider an endpoint that takes 180 milliseconds:</p><pre><code><code>Request
   &#8595;
Authentication        3 ms
   &#8595;
Application logic     5 ms
   &#8595;
Database query      160 ms
   &#8595;
Serialization         4 ms
   &#8595;
Response              8 ms</code></code></pre><p>Suppose we spend two days optimizing serialization.</p><p>We make it twice as fast.</p><p>Excellent.</p><p>Serialization now takes:</p><pre><code><code>2 ms</code></code></pre><p>Total response time becomes roughly:</p><pre><code><code>178 ms</code></code></pre><p>We performed a successful optimization that barely improved the user experience.</p><p>That is why performance engineering begins with measurement.</p><h2>Benchmarking and Load Testing Are Not the Same Thing</h2><p>These terms are often mixed together, but they answer different questions.</p><p>A <strong>microbenchmark</strong> asks:</p><blockquote><p>How efficiently does this small piece of code execute?</p></blockquote><p>A <strong>load test</strong> asks:</p><blockquote><p>How does the complete application behave when many users or requests hit it?</p></blockquote><p>A <strong>stress test</strong> asks:</p><blockquote><p>What happens when we push the system beyond normal operating conditions?</p></blockquote><p>Microsoft makes the same distinction in its official <em><a href="https://learn.microsoft.com/en-us/aspnet/core/test/load-tests?view=aspnetcore-5.0&amp;utm">ASP.NET Core load and stress testing guidance</a></em>. Load testing checks whether an application can satisfy its response goals under a specified expected load. Stress testing deliberately subjects it to abnormal conditions to examine stability and recovery. </p><p>These tests complement each other.</p><p>They are not interchangeable.</p><h2>Start With the Question</h2><p>Before opening a benchmarking tool, define what you are trying to learn.</p><p>Bad question:</p><blockquote><p>Is this code fast?</p></blockquote><p>Better question:</p><blockquote><p>Which of these two implementations allocates less memory and completes faster?</p></blockquote><p>Better load-testing question:</p><blockquote><p>Can this API sustain 1,000 requests per second while keeping p95 latency below 250 milliseconds and errors below 0.1 percent?</p></blockquote><p>Good performance tests have a hypothesis.</p><p>Without one, you can generate thousands of numbers without learning anything useful.</p><h2>BenchmarkDotNet Measures Small Pieces Extremely Well</h2><p>BenchmarkDotNet is widely used in the .NET ecosystem for controlled measurements of small units of .NET code.</p><p>Imagine that we have two ways of constructing a response string:</p><pre><code><code>public string BuildWithInterpolation()
{
    return $"{_firstName}:{_lastName}:{_customerId}";
}</code></code></pre><p>and:</p><pre><code><code>public string BuildWithConcat()
{
    return string.Concat(
        _firstName,
        ":",
        _lastName,
        ":",
        _customerId);
}</code></code></pre><p>Which is faster?</p><p>You could write:</p><pre><code><code>var sw = Stopwatch.StartNew();

for (var i = 0; i &lt; 1_000_000; i++)
{
    BuildWithInterpolation();
}

sw.Stop();

Console.WriteLine(sw.Elapsed);</code></code></pre><p>But this innocent-looking test has many problems.</p><p>What happened during JIT compilation?</p><p>Was the machine warming up?</p><p>Did garbage collection happen?</p><p>Did the runtime optimize something?</p><p>Did another process interrupt execution?</p><p>Did the compiler eliminate work?</p><p>How many allocations occurred?</p><p>Was the first execution representative of later executions?</p><p>BenchmarkDotNet exists because reliable microbenchmarking is harder than starting and stopping a stopwatch.</p><h2>A Simple BenchmarkDotNet Benchmark</h2><p>A basic benchmark might look like this:</p><pre><code><code>using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class CustomerFormatterBenchmarks
{
    private readonly string _firstName = "Maria";
    private readonly string _lastName = "Silva";
    private readonly int _customerId = 74281;

    [Benchmark(Baseline = true)]
    public string Interpolation()
    {
        return $"{_firstName}:{_lastName}:{_customerId}";
    }

    [Benchmark]
    public string Concat()
    {
        return string.Concat(
            _firstName,
            ":",
            _lastName,
            ":",
            _customerId);
    }
}

BenchmarkRunner.Run&lt;CustomerFormatterBenchmarks&gt;();</code></code></pre><p>BenchmarkDotNet runs repeated measurements and produces statistical results rather than one stopwatch reading.</p><p>The important point is not whether <code>Concat</code> or interpolation wins this particular example.</p><p>The important point is that we now have a repeatable experiment.</p><h2>Mean Is Only the Beginning</h2><p>A typical benchmark report may contain values such as:</p><pre><code><code>Method          Mean      Error     StdDev     Allocated
Interpolation   82 ns     ...       ...        96 B
Concat          74 ns     ...       ...        88 B</code></code></pre><p>Those numbers are illustrative.</p><p>Do not copy them into a performance claim.</p><p>Run the benchmark on your code and your environment.</p><p>The <strong>Mean</strong> tells us the average measured execution time.</p><p><strong>Error</strong> describes uncertainty around that estimate.</p><p><strong>StdDev</strong> shows variability.</p><p><strong>Allocated</strong> tells us how much managed memory was allocated per operation.</p><p>That last column can be particularly valuable in ASP.NET Core.</p><p>A method that saves:</p><pre><code><code>20 bytes</code></code></pre><p>does not sound important.</p><p>But if it executes:</p><pre><code><code>50 million times per hour</code></code></pre><p>the economics change.</p><p>Performance depends on frequency.</p><h2>Hot Paths Matter More Than Clever Code</h2><p>A hot path is code that executes frequently enough, or consumes enough resources, to have a meaningful effect on application performance.</p><p>Imagine two methods.</p><p>Method A takes:</p><pre><code><code>5 ms</code></code></pre><p>and runs:</p><pre><code><code>once per hour</code></code></pre><p>Method B takes:</p><pre><code><code>100 &#956;s</code></code></pre><p>and runs:</p><pre><code><code>20,000 times per second</code></code></pre><p>Which deserves attention?</p><p>Probably Method B.</p><p>Performance optimization should consider:</p><pre><code><code>Cost per operation
        &#215;
Execution frequency</code></code></pre><p>not merely which method looks slow in isolation.</p><h2>Benchmark the Work That Matters</h2><p>Good BenchmarkDotNet candidates include:</p><pre><code><code>Serialization routines
Parsing
Mapping
Formatting
Compression
Hashing
Buffer manipulation
Algorithms
Collection operations
Custom middleware logic
Frequently executed transformations</code></code></pre><p>Poor candidates include complete production workflows involving:</p><pre><code><code>Remote databases
Internet APIs
Message brokers
DNS
Cloud storage
External authentication</code></code></pre><p>Network and infrastructure variability can overwhelm the tiny code differences a microbenchmark is designed to measure.</p><p>If you want to understand the complete application, move up a level.</p><h2>BenchmarkDotNet Does Not Tell You How Your API Scales</h2><p>Suppose a method benchmark shows:</p><pre><code><code>25 &#956;s per operation</code></code></pre><p>That does <strong>not</strong> mean your endpoint will process:</p><pre><code><code>40,000 requests per second</code></code></pre><p>An HTTP request may also involve:</p><pre><code><code>Kestrel
Middleware
Authentication
Authorization
Model binding
Serialization
Database access
Network calls
Logging
Caching
Thread scheduling
Response transmission</code></code></pre><p>A microbenchmark isolates one piece.</p><p>A load test measures the assembled system.</p><p>This distinction prevents a huge amount of misleading performance work.</p><h2>What Load Testing Actually Measures</h2><p>Suppose our application exposes:</p><pre><code><code>GET /api/products/42</code></code></pre><p>A load test can send requests from many simulated clients.</p><p>Perhaps:</p><pre><code><code>10 users
     &#8595;
100 users
     &#8595;
500 users
     &#8595;
1,000 users</code></code></pre><p>At each stage we observe:</p><pre><code><code>Throughput
Latency
Error rate
CPU
Memory
GC
Thread Pool behavior
Database activity
Dependency latency
Queue depth</code></code></pre><p>Now we are studying a system rather than a method.</p><p>Tools such as k6, JMeter, Gatling, Locust, Vegeta, and NBomber can all be used for this type of testing.</p><p>There is no requirement that every ASP.NET Core team use the same tool.</p><p>The testing model matters more than the logo on the tool.</p><h2>Never Load-Test a Debug Build</h2><p>This sounds obvious, but it causes surprisingly misleading results.</p><p>A Debug build is intended to support development and debugging, not to represent optimized production performance.</p><p>Likewise, Development configuration may enable additional diagnostics and logging that affect the results.</p><p>A setup like this:</p><pre><code><code>Development laptop
Debug build
Verbose logging
Browser open
IDE debugger attached</code></code></pre><p>is not a trustworthy production performance environment.</p><p>Prefer something closer to:</p><pre><code><code>Release build
Production configuration
Production-like infrastructure
Representative database
Representative network
Representative resource limits</code></code></pre><p>Perfect reproduction of production is not always possible.</p><p>But the closer the environment is, the more meaningful the conclusions become.</p><h2>Throughput Is Only One Number</h2><p>Suppose our load test reports:</p><pre><code><code>2,500 requests/second</code></code></pre><p>That sounds impressive.</p><p>But what if users wait eight seconds for responses?</p><p>Throughput measures how much work the system completes over time.</p><p>It does not tell us how long individual users wait.</p><p>We therefore need latency too.</p><h2>Average Latency Can Hide Pain</h2><p>Imagine ten requests:</p><pre><code><code>50 ms
52 ms
48 ms
55 ms
51 ms
49 ms
53 ms
50 ms
51 ms
2,000 ms</code></code></pre><p>Most requests are fast.</p><p>One is terrible.</p><p>The average rises, but it still hides the shape of the experience.</p><p>At scale, we usually care about percentiles.</p><p>For example:</p><pre><code><code>p50 = 52 ms
p95 = 140 ms
p99 = 700 ms</code></code></pre><p><strong>p50</strong> represents the median experience.</p><p><strong>p95</strong> means 95 percent of measured requests completed at or below that value.</p><p><strong>p99</strong> exposes behavior near the slow end of the distribution.</p><p>This matters because production problems often live in the tail.</p><h2>Tail Latency Is Where Systems Reveal Themselves</h2><p>Why might a small percentage of requests suddenly become slow?</p><p>Possible causes include:</p><pre><code><code>Garbage collection
Database lock contention
Connection pool exhaustion
Thread Pool starvation
Slow downstream services
Cache misses
Disk activity
Queue buildup
Network delays
Retries</code></code></pre><p>An average can smooth these events away.</p><p>Users experiencing the p99 do not care that the average looked healthy.</p><p>This is why serious load tests should report latency distributions rather than a single average.</p><h2>Error Rate Belongs Beside Latency</h2><p>Imagine two systems.</p><p>System A:</p><pre><code><code>4,000 requests/sec
8% errors</code></code></pre><p>System B:</p><pre><code><code>3,500 requests/sec
0.02% errors</code></code></pre><p>Which is faster?</p><p>That is the wrong question.</p><p>Performance and correctness cannot be separated.</p><p>A system can produce extraordinary throughput by failing quickly.</p><p>Your load-test dashboard should therefore put at least these together:</p><pre><code><code>Throughput
Latency percentiles
Error rate</code></code></pre><p>A performance result without errors is incomplete.</p><h2>Capacity Is Not the Same as Maximum Throughput</h2><p>Suppose we gradually increase load:</p><pre><code><code>500 req/s
1,000 req/s
1,500 req/s
2,000 req/s
2,500 req/s
3,000 req/s</code></code></pre><p>At first, latency remains stable.</p><p>Then something changes.</p><p>Perhaps:</p><pre><code><code>Throughput plateaus
p95 latency rises
p99 explodes
Errors appear
Queues grow</code></code></pre><p>We have reached a saturation region.</p><p>The application&#8217;s useful capacity is usually <strong>below</strong> the absolute point where it collapses.</p><p>Production systems need headroom.</p><p>If a service barely survives its expected peak traffic in a laboratory test, it does not have healthy production capacity.</p><h2>Saturation Often Looks Like a Curve</h2><p>Imagine:</p><pre><code><code>Load      p95

500       60 ms
1000      65 ms
1500      72 ms
2000      90 ms
2500     180 ms
3000     850 ms
3500    3100 ms</code></code></pre><p>Notice what happened.</p><p>Latency did not rise smoothly.</p><p>The system reached a point where additional work began waiting behind existing work.</p><p>That waiting creates more waiting.</p><p>This is exactly where the principles from</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;07dbfdf3-0af0-45ef-8ef3-e6f578fc66c5&quot;,&quot;caption&quot;:&quot;A healthy ASP.NET Core application can become unstable without a single bug in its business logic. All it takes is more work arriving than the system can process. Requests accumulate, queues grow, memory usage rises, database connections disappear, latency explodes, and eventually the entire application may fail. Backpressure prevents that chain reactio&#8230;&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;lg&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Designing Backpressure in ASP.NET Core: Handling Overload Without Crashing Your System&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:262362149,&quot;name&quot;:&quot;Jenny Muralidharan&quot;,&quot;bio&quot;:&quot;A life long learner, mountain lover and a tech enthusiast. &quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F761d6747-6757-4175-be30-aa23b8c54bbd_96x96.png&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2026-09-01T15:02:38.898Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!T8pJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.asptoday.com/p/designing-backpressure-in-aspnet&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:213656095,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:2950429,&quot;publication_name&quot;:&quot;ASP Today&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!LonC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58c47c8-e39c-48e4-826a-d05e9ca9d537_509x509.jpeg&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>become important. Bounded queues, concurrency limits, and load shedding help prevent an overloaded application from continuing to accept more work than it can safely process.</p><p>Without those controls, a system can keep accepting requests after useful capacity has already been exhausted.</p><h2>Load Testing Should Model Real Traffic</h2><p>Suppose production traffic is:</p><pre><code><code>60% GET /products
20% GET /search
10% POST /orders
5% GET /account
5% other</code></code></pre><p>But our load test sends:</p><pre><code><code>100% GET /health</code></code></pre><p>The result tells us almost nothing about production capacity.</p><p>A realistic test should approximate:</p><pre><code><code>Endpoint mix
Payload sizes
Authentication
Database behavior
Cache hit rates
User pacing
Concurrency
Data distribution
Downstream calls</code></code></pre><p>This is particularly important with caching.</p><p>If every virtual user repeatedly requests:</p><pre><code><code>/api/products/1</code></code></pre><p>you may accidentally benchmark your cache rather than your application.</p><h2>Warm Tests and Cold Tests Answer Different Questions</h2><p>Consider a service that uses:</p><pre><code><code>JIT compilation
Connection pools
Database caches
Application caches
DNS caches
TLS sessions</code></code></pre><p>Its first few requests may behave differently from later requests.</p><p>That creates two legitimate questions.</p><p><strong>Cold-start test:</strong></p><blockquote><p>How quickly can a new instance become useful?</p></blockquote><p><strong>Steady-state test:</strong></p><blockquote><p>How does the service perform after it has warmed up?</p></blockquote><p>Do not mix them.</p><p>This becomes particularly important when evaluating <em><a href="https://www.asptoday.com/p/native-aot-in-aspnet-core-faster">Native AOT in ASP.NET Core: Faster Startup, Smaller Containers, and the Trade-Offs</a></em>. Native AOT can substantially change startup characteristics, but a steady-state load test answers a different performance question.</p><p>The best way to determine whether Native AOT actually benefits your service is to benchmark both deployment models under conditions that match the problem you are trying to solve.</p><h2>A Fast Endpoint Can Hide a Slow Dependency</h2><p>Suppose:</p><pre><code><code>API processing          8 ms
Database              140 ms
Payment service       250 ms
Serialization           5 ms</code></code></pre><p>Developers may inspect the controller and spend hours optimizing those eight milliseconds.</p><p>But the controller is not the bottleneck.</p><p>The system is waiting on dependencies.</p><p>This is where distributed tracing becomes extremely useful.</p><p>A trace can show:</p><pre><code><code>HTTP request
&#9474;
&#9500;&#9472;&#9472; authentication       3 ms
&#9500;&#9472;&#9472; application logic    5 ms
&#9500;&#9472;&#9472; SQL query          142 ms
&#9500;&#9472;&#9472; external API       247 ms
&#9492;&#9472;&#9472; serialization        6 ms</code></code></pre><p>Now optimization has direction.</p><h2>Find the Bottleneck, Not the Most Interesting Code</h2><p>Developers naturally gravitate toward code they control.</p><p>But the real bottleneck may be:</p><pre><code><code>Database index
Network
DNS
Connection pool
Thread Pool
Lock
Disk
External API
Queue
Memory pressure
GC</code></code></pre><p>The correct question is not:</p><blockquote><p>What code can I make faster?</p></blockquote><p>It is:</p><blockquote><p>What resource currently limits the system?</p></blockquote><p>That difference separates performance tuning from performance engineering.</p><h2>CPU Saturation Tells One Story</h2><p>Suppose load increases and CPU reaches:</p><pre><code><code>95&#8211;100%</code></code></pre><p>At the same time:</p><pre><code><code>Throughput stops increasing
Latency rises</code></code></pre><p>You probably have a CPU-bound bottleneck.</p><p>Now investigate:</p><pre><code><code>Serialization
Compression
Encryption
Mapping
Algorithms
Excessive logging
Exception handling
Busy loops</code></code></pre><p>A CPU profiler can identify hot code paths.</p><p>This may lead naturally back to BenchmarkDotNet.</p><p>The workflow becomes:</p><pre><code><code>Load test
    &#8595;
CPU bottleneck found
    &#8595;
Profiler identifies hot method
    &#8595;
BenchmarkDotNet isolates method
    &#8595;
Optimization
    &#8595;
Benchmark again
    &#8595;
Load test again</code></code></pre><p>That is much stronger than starting with a random microbenchmark.</p><h2>Low CPU Does Not Mean the Application Has Capacity</h2><p>Imagine latency is terrible but CPU sits at:</p><pre><code><code>25%</code></code></pre><p>That does not mean the application is healthy.</p><p>It may be waiting.</p><p>Perhaps threads are blocked on:</p><pre><code><code>Database
Locks
Network calls
Synchronous I/O
Connection pools</code></code></pre><p>This is a classic case where CPU alone gives the wrong impression.</p><p>The server is not busy computing.</p><p>It is busy waiting badly.</p><h2>Thread Pool Starvation Can Look Mysterious</h2><p>Imagine every request does this:</p><pre><code><code>var result = SomeAsyncOperation().Result;</code></code></pre><p>Under light traffic, everything may appear fine.</p><p>Under heavy concurrency, request threads become blocked waiting for asynchronous operations.</p><p>More requests arrive.</p><p>More threads become blocked.</p><p>The Thread Pool tries to compensate.</p><p>Latency rises dramatically.</p><p>This is exactly why a functional test cannot replace a load test.</p><p>The problem may become visible only when concurrency changes the system&#8217;s behavior.</p><h2>Memory Metrics Tell Another Story</h2><p>Suppose throughput remains healthy initially.</p><p>Over a longer test:</p><pre><code><code>Memory rises
GC becomes more frequent
Pause time increases
Latency becomes unstable</code></code></pre><p>Now the bottleneck may involve allocations or retention.</p><p>Watch metrics such as:</p><pre><code><code>Allocation rate
Heap size
Gen 0 collections
Gen 1 collections
Gen 2 collections
LOH activity
GC pause time</code></code></pre><p>This connects directly to <em><a href="https://www.asptoday.com/p/memory-management-in-aspnet-core">Memory Management in ASP.NET Core: GC, Allocations, LOH, and Object Pooling</a></em>, where we explored why allocation rate, retained memory, the Large Object Heap, and garbage collection can affect application latency and throughput.</p><p>A load test can reveal whether those memory-management decisions still behave well when hundreds or thousands of requests are competing for resources.</p><h2>Database Bottlenecks Are Extremely Common</h2><p>Consider:</p><pre><code><code>var orders = await db.Orders
    .Where(x =&gt; x.CustomerId == customerId)
    .ToListAsync();</code></code></pre><p>The C# looks harmless.</p><p>But perhaps:</p><pre><code><code>CustomerId has no useful index</code></code></pre><p>or the query returns:</p><pre><code><code>80,000 rows</code></code></pre><p>or an ORM pattern creates:</p><pre><code><code>N + 1 queries</code></code></pre><p>or the connection pool is exhausted.</p><p>The application can be slow while ASP.NET Core itself is doing almost nothing wrong.</p><p>Database telemetry must therefore be part of serious performance testing.</p><h2>Test the Database With Representative Data</h2><p>A query against:</p><pre><code><code>1,000 rows</code></code></pre><p>may behave beautifully.</p><p>Production contains:</p><pre><code><code>300 million rows</code></code></pre><p>That is a different system.</p><p>Performance tests need representative:</p><pre><code><code>Table sizes
Indexes
Data distribution
Relationship density
Query patterns
Cache behavior</code></code></pre><p>Synthetic data is fine if it reproduces the characteristics that matter.</p><p>Tiny development databases are not performance models.</p><h2>Connection Pools Can Become Invisible Queues</h2><p>Suppose the database connection pool can support a certain number of active connections.</p><p>Traffic increases.</p><p>Every request needs a database connection.</p><p>Eventually:</p><pre><code><code>Request
   &#8595;
Wait for connection
   &#8595;
Execute query
   &#8595;
Return connection</code></code></pre><p>Now the query itself may still take only:</p><pre><code><code>20 ms</code></code></pre><p>but the request spends:</p><pre><code><code>400 ms</code></code></pre><p>waiting to obtain a connection.</p><p>If you measure only SQL execution duration, you may conclude that the database is fast.</p><p>End-to-end tracing reveals the wait.</p><p>This is why bottleneck analysis requires multiple layers of telemetry.</p><h2>Queues Need Measurement Too</h2><p>A background system may process each job in:</p><pre><code><code>50 ms</code></code></pre><p>That sounds healthy.</p><p>But if jobs arrive faster than workers complete them:</p><pre><code><code>Arrival rate     &gt;     Processing rate</code></code></pre><p>queue depth grows.</p><p>Users experience increasing delay even though individual jobs remain fast.</p><p>Measure:</p><pre><code><code>Queue depth
Queue wait time
Processing time
Arrival rate
Completion rate</code></code></pre><p>Queue latency is often more important than worker execution time.</p><h2>Benchmark Allocations, Not Just Nanoseconds</h2><p>Suppose two implementations produce:</p><pre><code><code>A: 120 ns, 0 B allocated
B: 100 ns, 256 B allocated</code></code></pre><p>Which is better?</p><p>There is not enough information.</p><p>If the operation runs rarely, B may be perfectly reasonable.</p><p>If it executes millions of times per second, the allocation difference could create additional GC pressure.</p><p>BenchmarkDotNet&#8217;s memory diagnostics are therefore extremely useful.</p><p>Performance decisions should consider:</p><pre><code><code>Execution time
Allocations
Frequency
Concurrency
Complexity</code></code></pre><p>not one number.</p><h2>Small Benchmark Wins Can Be Noise</h2><p>Suppose version B is:</p><pre><code><code>1.7% faster</code></code></pre><p>Should we rewrite production code?</p><p>Maybe not.</p><p>Ask:</p><pre><code><code>Is the result statistically stable?

Is the method actually hot?

Does it affect end-to-end performance?

Does the new code become harder to maintain?

Will the difference survive real workloads?</code></code></pre><p>A 2 percent microbenchmark improvement inside code responsible for 0.1 percent of request time is almost certainly irrelevant.</p><p>Optimization has an opportunity cost.</p><h2>Performance Regressions Are Where Benchmarks Become Powerful</h2><p>Benchmarking is not only for making code faster.</p><p>It is excellent for preventing code from becoming slower.</p><p>Suppose a critical parser currently processes data in:</p><pre><code><code>40 &#956;s</code></code></pre><p>A future change accidentally increases it to:</p><pre><code><code>90 &#956;s</code></code></pre><p>A benchmark suite can expose the regression before deployment.</p><p>Likewise, load tests can enforce service-level performance requirements.</p><p>For example:</p><pre><code><code>p95 &lt; 250 ms
error rate &lt; 0.5%
throughput &gt; 1,500 req/s</code></code></pre><p>Now performance becomes something the team can test, rather than something users report after release.</p><h2>Be Careful With Performance Gates in CI</h2><p>There is a catch.</p><p>Shared CI runners can be noisy.</p><p>Other workloads may affect:</p><pre><code><code>CPU
Scheduling
Memory
Disk
Network</code></code></pre><p>A strict rule such as:</p><pre><code><code>Fail build if benchmark is 2% slower</code></code></pre><p>may produce false failures.</p><p>Use stable benchmarking environments for sensitive comparisons.</p><p>For ordinary CI, larger regression thresholds or historical trend analysis may be more practical.</p><p>Performance automation is valuable only when teams trust the signal.</p><h2>Stress Testing Goes Beyond Expected Traffic</h2><p>A load test asks whether the system handles expected conditions.</p><p>A stress test deliberately goes further.</p><p>Perhaps expected peak traffic is:</p><pre><code><code>2,000 requests/sec</code></code></pre><p>A stress test might push:</p><pre><code><code>2,500
3,000
4,000
5,000</code></code></pre><p>until the system degrades.</p><p>We are deliberately discovering the system&#8217;s limits.</p><p>But there is another important question.</p><p>What happens after the overload disappears?</p><h2>What Happens After Overload?</h2><p>Suppose traffic spikes to:</p><pre><code><code>5&#215; normal</code></code></pre><p>Latency explodes.</p><p>Some requests fail.</p><p>Then traffic returns to normal.</p><p>What happens next?</p><p>Healthy system:</p><pre><code><code>Load falls
   &#8595;
Queues drain
   &#8595;
Resources recover
   &#8595;
Latency normalizes</code></code></pre><p>Unhealthy system:</p><pre><code><code>Load falls
   &#8595;
Retry storm continues
   &#8595;
Queues remain enormous
   &#8595;
Connections remain exhausted
   &#8595;
Latency stays terrible</code></code></pre><p>The second system survived the spike technically but failed operationally.</p><p>This connects directly to backpressure, retry design, and graceful recovery.</p><h2>Soak Tests Find Problems Short Tests Miss</h2><p>A five-minute test can show excellent results.</p><p>Run the same workload for:</p><pre><code><code>8 hours</code></code></pre><p>and perhaps:</p><pre><code><code>Memory slowly climbs
Connections leak
Caches grow without bounds
Queue depth drifts upward
Temporary files accumulate
Latency gradually worsens</code></code></pre><p>This is where soak testing becomes useful.</p><p>A soak test applies sustained workload for an extended period to expose degradation that takes time to develop.</p><p>It is particularly valuable for applications that are expected to run continuously.</p><h2>Spike Tests Examine Sudden Change</h2><p>Autoscaling systems often face sudden traffic.</p><p>Imagine:</p><pre><code><code>500 req/s
     &#8595;
5,000 req/s
within 10 seconds</code></code></pre><p>Can the system absorb the transition?</p><p>Does autoscaling react quickly enough?</p><p>Do existing instances collapse before new ones become ready?</p><p>Do connection pools become exhausted?</p><p>Does the database survive the surge?</p><p>Faster startup may improve scaling response, but only a realistic spike test can tell you whether it materially changes the system&#8217;s behavior.</p><h2>The Load Generator Can Become the Bottleneck</h2><p>This is an easy mistake.</p><p>Suppose your load-testing machine reaches:</p><pre><code><code>100% CPU</code></code></pre><p>while the server is at:</p><pre><code><code>35% CPU</code></code></pre><p>Requests stop increasing.</p><p>You might conclude:</p><blockquote><p>The server maxes out at 10,000 requests per second.</p></blockquote><p>But perhaps the <strong>client generating the traffic</strong> maxed out.</p><p>The test infrastructure must have enough capacity to overload the system under test.</p><p>Otherwise you benchmark your load generator.</p><p>For very large tests, traffic may need to originate from multiple machines or a managed load-testing service.</p><h2>Network Placement Changes Results</h2><p>A load generator on the same machine as the application measures something different from a user thousands of kilometers away.</p><p>Network latency can dominate short server operations.</p><p>So decide what you want to measure.</p><p>For application processing capacity:</p><pre><code><code>Load generator close to service</code></code></pre><p>may be appropriate.</p><p>For user experience:</p><pre><code><code>Realistic geographic/network paths</code></code></pre><p>matter.</p><p>Again, the test design follows the question.</p><h2>Establish a Baseline Before Optimizing</h2><p>Suppose we are about to change JSON serialization.</p><p>First record:</p><pre><code><code>Throughput: 2,200 req/s
p50:         48 ms
p95:        130 ms
p99:        310 ms
Errors:     0.03%
CPU:        68%
Memory:     620 MB</code></code></pre><p>These numbers are illustrative.</p><p>Then make one meaningful change.</p><p>Run the same test again.</p><p>Without a baseline, statements such as:</p><blockquote><p>It feels faster.</p></blockquote><p>are not performance engineering.</p><h2>Change One Thing at a Time</h2><p>Suppose we simultaneously:</p><pre><code><code>Add caching
Change serializer
Upgrade .NET
Rewrite database query
Enable pooling
Increase connection limits</code></code></pre><p>Performance improves by 35 percent.</p><p>What caused it?</p><p>We do not know.</p><p>Perhaps one change improved performance by 50 percent while another made it 15 percent worse.</p><p>Controlled experiments are powerful because they isolate causes.</p><p>A useful loop is:</p><pre><code><code>Measure
   &#8595;
Form hypothesis
   &#8595;
Change one important variable
   &#8595;
Measure again
   &#8595;
Compare</code></code></pre><h2>Profile Before You Micro-Optimize</h2><p>Suppose CPU profiling shows:</p><pre><code><code>Database client          38%
JSON serialization       22%
Custom mapper            18%
Logging                  12%
Everything else          10%</code></code></pre><p>Now you know where investigation is likely to pay off.</p><p>Benchmark the mapper.</p><p>Investigate serialization.</p><p>Check logging.</p><p>Do not spend three days optimizing something hidden inside the final 10 percent unless evidence points there.</p><p>Profiling gives microbenchmarks a purpose.</p><p>Instead of asking:</p><blockquote><p>What can we benchmark?</p></blockquote><p>we ask:</p><blockquote><p>Which expensive path is worth isolating and improving?</p></blockquote><h2>Measure Production Too</h2><p>Laboratory tests are essential.</p><p>Production telemetry is reality.</p><p>Production contains things your test environment may not reproduce:</p><pre><code><code>Real customers
Unexpected payloads
Uneven traffic
Slow devices
Real geographic latency
Third-party failures
Organic database growth
Cache churn
Deployment events
Background jobs</code></code></pre><p>Use production metrics to discover which scenarios deserve controlled testing.</p><p>The two systems reinforce each other:</p><pre><code><code>Production reveals problem
        &#8595;
Controlled test reproduces it
        &#8595;
Profiler finds cause
        &#8595;
Benchmark validates optimization
        &#8595;
Load test validates system
        &#8595;
Production confirms improvement</code></code></pre><p>That is a mature performance workflow.</p><h2>A Practical Performance Investigation</h2><p>Imagine an order API.</p><p>Users report occasional slow checkout.</p><p>Normal traffic looks healthy.</p><p>Under a realistic load test, we observe:</p><pre><code><code>p50     120 ms
p95     850 ms
p99    2400 ms</code></code></pre><p>CPU is only:</p><pre><code><code>45%</code></code></pre><p>So raw computation probably is not the main bottleneck.</p><p>Tracing shows many slow requests waiting for database connections.</p><p>Database metrics show the pool frequently reaching its limit.</p><p>Further investigation reveals that one endpoint performs several sequential queries per request.</p><p>We rewrite the data-access pattern to reduce unnecessary round trips.</p><p>Run the same load test again.</p><p>Suppose p95 falls substantially and connection waits disappear.</p><p>Now we have evidence.</p><p>Not:</p><blockquote><p>The new code looks cleaner.</p></blockquote><p>But:</p><blockquote><p>We identified a resource bottleneck, changed the behavior causing it, and measured the improvement under the same workload.</p></blockquote><p>That is performance engineering.</p><h2>Do Not Optimize Away Readability for Tiny Wins</h2><p>BenchmarkDotNet can tempt developers into competitions over nanoseconds.</p><p>Suppose:</p><pre><code><code>Readable version      95 ns
Complex version       89 ns</code></code></pre><p>The second implementation is 6 nanoseconds faster.</p><p>But the method runs:</p><pre><code><code>20 times per request</code></code></pre><p>and requests take:</p><pre><code><code>200 ms</code></code></pre><p>That optimization is almost certainly meaningless.</p><p>Performance work must still respect:</p><pre><code><code>Maintainability
Correctness
Security
Reliability
Developer productivity</code></code></pre><p>The fastest code is useless if nobody can safely change it.</p><h2>Benchmarking Native AOT Properly</h2><p>Suppose we want to determine whether Native AOT helps our service.</p><p>Do not benchmark only one number.</p><p>Compare:</p><pre><code><code>JIT deployment
vs
Native AOT</code></code></pre><p>across:</p><pre><code><code>Publish size
Container size
Process startup
Time to readiness
Idle memory
Memory under load
Throughput
p50 latency
p95 latency
p99 latency
CPU
Build time</code></code></pre><p>Native AOT may win dramatically in some categories and show little difference in others.</p><p>That is not a contradiction.</p><p>It is precisely why we benchmark.</p><h2>Benchmarking High-Performance I/O</h2><p>The same principle applies to our earlier I/O work.</p><p>Suppose we replace:</p><pre><code><code>Read entire body
     &#8595;
Create string
     &#8595;
Parse</code></code></pre><p>with:</p><pre><code><code>Stream
   &#8595;
Parse incrementally</code></code></pre><p>The most interesting result may not be raw execution speed.</p><p>Perhaps:</p><pre><code><code>Latency changes slightly</code></code></pre><p>but:</p><pre><code><code>Peak memory drops dramatically
GC frequency decreases
Throughput under concurrency improves</code></code></pre><p>A narrow benchmark could miss the actual benefit.</p><p>Choose measurements that match the optimization.</p><h2>Benchmarking Memory Improvements</h2><p>Suppose we introduce <code>ArrayPool&lt;T&gt;</code>.</p><p>A microbenchmark can measure:</p><pre><code><code>Execution time
Allocated bytes
GC collections</code></code></pre><p>But then run a realistic concurrent workload.</p><p>Why?</p><p>Because pooling is often valuable when repeated allocation pressure compounds across many requests.</p><p>An isolated benchmark tells us whether the operation allocates less.</p><p>A load test tells us whether that changes application behavior.</p><p>Both are useful.</p><h2>Performance Is Multidimensional</h2><p>A common mistake is searching for one universal score.</p><p>There isn&#8217;t one.</p><p>Performance includes:</p><pre><code><code>Latency
Throughput
Startup
Memory
CPU
Allocation rate
Container size
Scalability
Recovery
Cost</code></code></pre><p>Improving one can hurt another.</p><p>Compression may:</p><pre><code><code>Reduce network bytes
Increase CPU</code></code></pre><p>Caching may:</p><pre><code><code>Reduce database latency
Increase memory</code></code></pre><p>Pooling may:</p><pre><code><code>Reduce allocations
Increase retained memory</code></code></pre><p>Higher concurrency may:</p><pre><code><code>Increase throughput
Increase contention</code></code></pre><p>Performance decisions are trade-offs.</p><h2>Define a Performance Budget</h2><p>Instead of saying:</p><blockquote><p>Make checkout fast.</p></blockquote><p>Define:</p><pre><code><code>p50 latency       &lt; 150 ms
p95 latency       &lt; 300 ms
p99 latency       &lt; 750 ms
error rate        &lt; 0.1%
memory            &lt; 1 GB per instance
CPU               &lt; 75% at target load
throughput        &#8805; 2,000 req/s</code></code></pre><p>Again, those are illustrative values.</p><p>Your business requirements should determine the real ones.</p><p>Now the team has a target.</p><p>Performance becomes testable.</p><h2>A Useful Testing Pyramid for Performance</h2><p>Think of performance testing at several levels:</p><pre><code><code>              Production
                 &#9650;
             Stress/Soak
                 &#9650;
              Load Test
                 &#9650;
              Profiling
                 &#9650;
           Microbenchmark</code></code></pre><p>At the bottom, BenchmarkDotNet gives precise measurements of small operations.</p><p>Moving upward introduces more of the real system.</p><p>By the time we reach production, realism is highest but experimental control is lowest.</p><p>No single layer replaces the others.</p><h2>The Best Performance Workflow</h2><p>A practical workflow looks like this.</p><p><strong>First, define the requirement.</strong></p><p>What must the system achieve?</p><p><strong>Second, establish a baseline.</strong></p><p>Record current performance.</p><p><strong>Third, reproduce the workload.</strong></p><p>Use representative traffic and data.</p><p><strong>Fourth, observe the complete system.</strong></p><p>Measure latency, throughput, errors, CPU, memory, GC, queues, databases, and dependencies.</p><p><strong>Fifth, identify the limiting resource.</strong></p><p>Do not guess.</p><p><strong>Sixth, profile the relevant component.</strong></p><p>Find the expensive path.</p><p><strong>Seventh, isolate code where useful.</strong></p><p>Use BenchmarkDotNet for focused comparisons.</p><p><strong>Eighth, change one meaningful thing.</strong></p><p>Keep the experiment understandable.</p><p><strong>Ninth, rerun the same test.</strong></p><p>Compare against the baseline.</p><p><strong>Tenth, verify production behavior.</strong></p><p>Make sure the laboratory improvement survives reality.</p><p>That workflow is far more valuable than memorizing a collection of &#8220;fast&#8221; coding tricks.</p><h2>Common Benchmarking Mistakes</h2><p>The most common mistakes are surprisingly consistent.</p><p><strong>Benchmarking Debug builds.</strong> Measure optimized production-style builds.</p><p><strong>Using a stopwatch for tiny operations.</strong> A proper benchmarking harness handles many sources of measurement error.</p><p><strong>Looking only at averages.</strong> Tail latency matters.</p><p><strong>Ignoring errors.</strong> Failed requests can make throughput look artificially impressive.</p><p><strong>Testing unrealistic endpoints.</strong> Your health endpoint is not your production workload.</p><p><strong>Using tiny databases.</strong> Data scale changes query behavior.</p><p><strong>Ignoring warm-up.</strong> Cold and steady-state tests answer different questions.</p><p><strong>Optimizing before profiling.</strong> Interesting code is not necessarily hot code.</p><p><strong>Ignoring the load generator.</strong> Your client can become the bottleneck.</p><p><strong>Changing several variables simultaneously.</strong> You lose causality.</p><p><strong>Treating microbenchmarks as application benchmarks.</strong> A fast method does not guarantee a scalable service.</p><p><strong>Ignoring production telemetry.</strong> Synthetic tests cannot reproduce every real-world condition.</p><h2>Finding the Real Bottleneck Changes Everything</h2><p>There is a powerful moment in performance work when the problem becomes specific.</p><p>Instead of:</p><blockquote><p>Our API is slow.</p></blockquote><p>we discover:</p><blockquote><p>p99 latency rises after 1,800 requests per second because database connection waits increase sharply.</p></blockquote><p>Or:</p><blockquote><p>Throughput stops scaling because CPU is dominated by serialization.</p></blockquote><p>Or:</p><blockquote><p>Latency spikes correlate with Gen 2 garbage collections caused by repeated large-buffer allocation.</p></blockquote><p>Or:</p><blockquote><p>Requests pile up because synchronous blocking is starving the Thread Pool.</p></blockquote><p>Those statements can be acted upon.</p><p>&#8220;Slow&#8221; cannot.</p><p>This is the real purpose of benchmarking.</p><p>Not generating impressive charts.</p><p>Not winning nanosecond competitions.</p><p><strong>Turning vague performance complaints into measurable engineering problems.</strong></p><h2>Coming Next</h2><p>In the next article, we&#8217;ll explore <strong>Contract-First API Development in ASP.NET Core: OpenAPI, Client Generation, and Schema Governance</strong>.</p><p>That may seem like a shift away from performance, but it continues the same broader theme: making production systems predictable.</p><p>Instead of allowing an API contract to emerge accidentally from implementation code, contract-first development treats the API schema as an explicit engineering artifact.</p><p>We&#8217;ll examine OpenAPI contracts, generated clients, breaking-change detection, schema governance, versioning decisions, and how teams keep API producers and consumers synchronized as systems grow.</p><h2>Closing Thoughts</h2><p>Performance optimization is seductive because changing code feels productive.</p><p>Measurement sometimes feels slower.</p><p>But measurement is what prevents us from spending days solving the wrong problem.</p><p>BenchmarkDotNet can tell us whether one small implementation is faster, more stable, or less allocation-heavy than another.</p><p>Profilers can tell us where the application spends CPU time.</p><p>Load tests can show how the complete system behaves under realistic concurrency.</p><p>Stress tests can reveal the point where the system stops coping.</p><p>Soak tests can expose problems that emerge slowly.</p><p>Production telemetry tells us whether any of those laboratory conclusions survive contact with real users.</p><p>Together, these tools give us a much more reliable picture of application performance than any single benchmark number.</p><p>The mature performance question is therefore not:</p><blockquote><p>How can we make this code faster?</p></blockquote><p>It is:</p><blockquote><p><strong>What is limiting the system right now, how do we prove it, and what measurement will prove that our change actually fixed it?</strong></p></blockquote><p>Once you can answer those questions, performance optimization stops being guesswork.</p><p>It becomes engineering.</p><h2>Subscribe Now</h2><p>Enjoying the series? Subscribe to <em><a href="https://www.asptoday.com/">ASP Today</a></em> for practical ASP.NET Core tutorials, production architecture deep dives, and evidence-based .NET performance strategies. Join our Substack Chat to discuss BenchmarkDotNet, load testing, profiling, bottleneck analysis, Native AOT, memory optimization, and the techniques that help ASP.NET Core applications perform reliably under real-world traffic. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Native AOT in ASP.NET Core: Faster Startup, Smaller Containers, and the Trade-Offs]]></title><description><![CDATA[Explore faster startup, smaller containers, and Native AOT trade-offs in ASP.NET Core. #ASPToday #aspnetcore #dotnet #nativeaot]]></description><link>https://www.asptoday.com/p/native-aot-in-aspnet-core-faster</link><guid isPermaLink="false">https://www.asptoday.com/p/native-aot-in-aspnet-core-faster</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 29 Sep 2026 15:03:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!3HyO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core applications</a> normally rely on the .NET runtime to compile parts of their code as the application runs. Native AOT changes that model. Instead of depending on Just-in-Time compilation at runtime, Native Ahead-of-Time compilation turns application code into native machine code during publishing.  </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3HyO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3HyO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!3HyO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!3HyO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!3HyO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3HyO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2475427,&quot;alt&quot;:&quot;Rocket launches from a prepared platform, symbolizing Native AOT's fast startup and lean deployment. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/217947947?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Rocket launches from a prepared platform, symbolizing Native AOT's fast startup and lean deployment. " title="Rocket launches from a prepared platform, symbolizing Native AOT's fast startup and lean deployment. " srcset="https://substackcdn.com/image/fetch/$s_!3HyO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!3HyO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!3HyO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!3HyO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5d65b50-aa9a-444b-a866-db6f056420d6_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The result can start faster, require less memory, and produce a smaller deployment footprint. Those advantages are especially attractive for containers, microservices, command-line tools, and workloads that start and stop frequently. But Native AOT is not a free performance switch. Trimming, reflection, runtime code generation, serialization, NuGet dependencies, diagnostics, and ASP.NET Core feature compatibility all become part of the decision. The real question is therefore not whether Native AOT is faster. It is whether your particular application fits the Native AOT execution model. </p><h2>What Actually Happens When an ASP.NET Core Application Starts?</h2><p>Before understanding Native AOT, it helps to understand the normal .NET execution model.</p><p>C# source code is usually compiled into Intermediate Language, or IL.</p><p>Conceptually:</p><pre><code><code>C# source
    &#8595;
Compiler
    &#8595;
Intermediate Language
    &#8595;
Application starts
    &#8595;
JIT compilation
    &#8595;
Native machine code
    &#8595;
CPU executes it</code></code></pre><p>The Just-in-Time compiler converts code into machine instructions appropriate for the current environment as the application executes.</p><p>This model has served .NET extremely well.</p><p>It allows the runtime to make decisions using information available on the actual machine executing the application.</p><p>But there is a cost.</p><p>Some compilation happens while the application is running.</p><p>For a long-lived application that runs for weeks, that startup work may be relatively unimportant.</p><p>For a small container that starts constantly, it may matter much more.</p><h2>Native AOT Moves Compilation Earlier</h2><p>Native AOT changes where that work happens.</p><p>Instead of waiting until runtime:</p><pre><code><code>Publish
   &#8595;
IL + AOT compiler
   &#8595;
Native executable
   &#8595;
Deploy
   &#8595;
Start immediately as native code</code></code></pre><p>Microsoft describes Native AOT applications as self-contained applications compiled ahead of time into native code. They do not use a JIT compiler when the published application runs. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?utm_source=chatgpt.com">Microsoft Learn</a></p><p>That difference sounds small.</p><p>Architecturally, it is significant.</p><p>We are moving work from:</p><pre><code><code>Application runtime</code></code></pre><p>to:</p><pre><code><code>Build and publishing time</code></code></pre><p>This creates the central trade-off of Native AOT.</p><p><strong>We do more work and make more decisions before deployment so that less work has to happen after the process starts.</strong></p><h2>Why ASP.NET Core Developers Care About Startup Time</h2><p>Imagine a traditional application running on one server.</p><p>It starts Monday morning and stays alive for three months.</p><p>Suppose startup takes:</p><pre><code><code>1.5 seconds</code></code></pre><p>Reducing that to:</p><pre><code><code>200 milliseconds</code></code></pre><p>might be technically impressive but operationally irrelevant.</p><p>Now imagine a containerized service where instances are repeatedly created because of:</p><pre><code><code>Autoscaling
Rolling deployments
Scale-to-zero architectures
Short-lived workers
Rapid failover
Development environments
Test infrastructure</code></code></pre><p>Startup suddenly matters much more.</p><p>Suppose traffic increases quickly.</p><p>The orchestrator launches 20 additional instances.</p><p>If those instances become ready sooner, the system can begin absorbing traffic sooner.</p><p>This complements the deployment practices we covered in <em><a href="https://www.asptoday.com/p/graceful-shutdown-and-application">Graceful Shutdown and Application Lifecycle in ASP.NET Core: Deploy Without Dropping Work</a></em>. Graceful shutdown helps an existing instance leave safely, while faster startup helps its replacement become ready sooner. </p><p>Microsoft specifically lists reduced startup time as one of Native AOT&#8217;s ASP.NET Core benefits and notes that quicker startup can improve transitions managed by container orchestrators. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><p>This connects directly to our earlier discussion of <strong>Graceful Shutdown and Application Lifecycle in ASP.NET Core</strong>.</p><p>Containers need to leave safely.</p><p>Native AOT can help their replacements arrive quickly.</p><h2>Smaller Containers Matter Too</h2><p>Native AOT also changes deployment size.</p><p>A Native AOT application is self-contained and includes what it needs to execute. During Native AOT publishing, unused code can be trimmed away, and the resulting application contains the required native code rather than relying on a JIT-based runtime deployment model. Microsoft highlights minimized disk footprint and smaller container images among the benefits for ASP.NET Core workloads. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?utm_source=chatgpt.com">Microsoft Learn</a></p><p>Why does a smaller container matter?</p><p>Consider a deployment across hundreds of nodes.</p><p>Each node may need to:</p><pre><code><code>Pull image
     &#8595;
Extract layers
     &#8595;
Create container
     &#8595;
Start process
     &#8595;
Pass readiness checks</code></code></pre><p>Reducing image size can help with:</p><pre><code><code>Registry transfers
Image pulls
Deployment speed
Storage usage
Cold starts</code></code></pre><p>Again, the value multiplies with deployment scale.</p><p>A 50 MB improvement may not matter on one development laptop.</p><p>Across thousands of image pulls, it becomes a different calculation.</p><h2>Lower Memory Demand Can Increase Density</h2><p>In our previous guide to <em>Memory Management in ASP.NET Core</em><strong>, </strong></p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;6a616d29-82bf-48f6-b7f8-0856c59a97e1&quot;,&quot;caption&quot;:&quot;An ASP.NET Core application can have plenty of available CPU, a healthy database, and fast network connections yet still become slow under load because of memory. Every request creates objects, strings, arrays, JSON buffers, collections, and temporary data. Most disappear quickly, which is exactly what .NET&#8217;s garbage collector is designed to handle. Pro&#8230;&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Memory Management in ASP.NET Core: GC, Allocations, LOH, and Object Pooling &quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:262362149,&quot;name&quot;:&quot;Jenny Muralidharan&quot;,&quot;bio&quot;:&quot;A life long learner, mountain lover and a tech enthusiast. &quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F761d6747-6757-4175-be30-aa23b8c54bbd_96x96.png&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2026-09-15T15:02:07.639Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!56aB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.asptoday.com/p/memory-management-in-aspnet-core&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:215666406,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:2950429,&quot;publication_name&quot;:&quot;ASP Today&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!LonC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa58c47c8-e39c-48e4-826a-d05e9ca9d537_509x509.jpeg&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>we explored how allocation pressure, garbage collection, and retained memory affect the performance and efficiency of a running application.</p><p>Microsoft also identifies reduced memory demand as a potential Native AOT advantage, depending on the workload. Lower memory consumption can allow more application instances to run on the same infrastructure. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><p>Suppose a node has:</p><pre><code><code>16 GB available</code></code></pre><p>If each application instance requires:</p><pre><code><code>400 MB</code></code></pre><p>you have one deployment density.</p><p>If architectural and runtime improvements reduce that substantially, you may be able to run more instances on the same infrastructure.</p><p>That does not automatically mean:</p><pre><code><code>Native AOT = cheap cloud bill</code></code></pre><p>Real applications have:</p><pre><code><code>Caches
Database clients
Buffers
Queues
Request data
Business objects
Telemetry</code></code></pre><p>Native AOT does not make those disappear.</p><p>But runtime overhead matters, particularly for many small services.</p><p>This is another reason our previous articles on <em>Memory Management and <a href="https://www.asptoday.com/p/high-performance-io-in-aspnet-core">High-Performance I/O</a></em> naturally lead into Native AOT.</p><h2>Your First Native AOT ASP.NET Core Application</h2><p>ASP.NET Core provides a Native AOT Web API project template.</p><p>You can create one with:</p><pre><code><code>dotnet new webapiaot -n FastApi</code></code></pre><p>The template is deliberately lean.</p><p>Microsoft&#8217;s Native AOT template uses Minimal APIs and <code>CreateSlimBuilder()</code> to reduce the features included by default and therefore reduce deployment size. <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot?view=aspnetcore-10.0&amp;utm_source">Microsoft Learn</a></p><p>A minimal application might look like:</p><pre><code><code>var builder =
    WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

app.MapGet(
    "/",
    () =&gt; new
    {
        Message = "Hello from Native AOT"
    });

app.Run();</code></code></pre><p>The important line is:</p><pre><code><code>WebApplication.CreateSlimBuilder(args);</code></code></pre><p>rather than the familiar:</p><pre><code><code>WebApplication.CreateBuilder(args);</code></code></pre><p>That choice is intentional.</p><h2>Why CreateSlimBuilder Exists</h2><p>A normal ASP.NET Core application includes a broad set of conveniences.</p><p>That is useful because most applications prefer productive defaults.</p><p>Native AOT encourages us to think differently:</p><p><strong>What does this application actually need?</strong></p><p><code>CreateSlimBuilder()</code> starts with a smaller feature set while retaining important functionality such as JSON configuration, user secrets, console logging, and logging configuration. Some features available through the standard builder aren&#8217;t included by the slim builder. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><p>This reflects a broader Native AOT philosophy.</p><p>Instead of:</p><pre><code><code>Include broad runtime capability
and discover what we need later</code></code></pre><p>we increasingly move toward:</p><pre><code><code>Know what we need
during build and publish</code></code></pre><p>That difference becomes particularly important when trimming enters the picture.</p><h2>Enabling Native AOT</h2><p>Native AOT publishing is enabled using the <code>PublishAot</code> property.</p><p>For example:</p><pre><code><code>&lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;PublishAot&gt;true&lt;/PublishAot&gt;
&lt;/PropertyGroup&gt;</code></code></pre><p>Microsoft documents <code>PublishAot</code> as the property that enables Native AOT during publishing and activates analysis for dynamic-code usage. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?utm_source=chatgpt.com">Microsoft Learn</a></p><p>You can then publish for a specific runtime.</p><p>For example:</p><pre><code><code>dotnet publish -c Release -r linux-x64</code></code></pre><p>The runtime identifier matters because we are creating native code for a particular target.</p><p>That is another conceptual change.</p><p>Traditional IL is highly portable.</p><p>Native machine code is inherently more closely tied to its target platform and architecture.</p><h2>Development Still Feels Familiar</h2><p>One subtle point is worth emphasizing.</p><p>Setting:</p><pre><code><code>&lt;PublishAot&gt;true&lt;/PublishAot&gt;</code></code></pre><p>does not mean every local development run becomes a full Native AOT compilation.</p><p>Microsoft notes that projects configured for Native AOT still use JIT compilation when run normally during development. Native AOT compilation happens during publishing. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><p>That keeps the normal development loop practical.</p><p>You can still:</p><pre><code><code>Edit
Run
Debug
Edit
Run</code></code></pre><p>without waiting for a Native AOT publish every time.</p><p>But that creates an important responsibility.</p><h2>Test the Published AOT Application</h2><p>Your development build is not the final execution environment.</p><p>An application may work perfectly under the normal JIT model and expose problems after trimming and AOT compilation.</p><p>That means this is insufficient:</p><pre><code><code>dotnet run
     &#8595;
Everything works
     &#8595;
Ship it</code></code></pre><p>Instead:</p><pre><code><code>Develop normally
     &#8595;
Publish Native AOT
     &#8595;
Run published executable
     &#8595;
Execute integration tests
     &#8595;
Exercise real features
     &#8595;
Inspect AOT warnings</code></code></pre><p>Microsoft recommends testing the AOT-published application and correcting AOT warnings. The publish process analyzes both application code and dependencies for compatibility issues. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><p>Native AOT compatibility should therefore be part of the development lifecycle, not something discovered the night before production deployment.</p><h2>Trimming Is Central to the Story</h2><p>Suppose an application references a library containing:</p><pre><code><code>10,000 methods</code></code></pre><p>but actually uses:</p><pre><code><code>300 methods</code></code></pre><p>Why ship everything?</p><p>Trimming analyzes the application and attempts to remove code that is not needed.</p><p>That helps reduce the final footprint.</p><p>But now imagine code that does this:</p><pre><code><code>var type =
    Type.GetType(typeName);

var instance =
    Activator.CreateInstance(type!);</code></code></pre><p>From static analysis alone, which type will be required at runtime?</p><p>The answer might depend on:</p><pre><code><code>Configuration
User input
Plugin discovery
Database contents
External metadata</code></code></pre><p>Now trimming becomes harder.</p><p>Code that appears unused during publishing might actually be expected dynamically at runtime.</p><p>This is one of the fundamental tensions between Native AOT and highly dynamic application architectures.</p><h2>Reflection Is Not Simply &#8220;Forbidden&#8221;</h2><p>Native AOT discussions sometimes get reduced to:</p><pre><code><code>Reflection does not work.</code></code></pre><p>That is too simplistic.</p><p>The real problem is <strong>unbounded dynamic behavior that cannot be determined during compilation</strong>.</p><p>Native AOT publishing trims unused code, so runtime patterns that dynamically discover types or members can become problematic unless the required metadata and code are known and preserved. Microsoft recommends source generators and AOT-compatible patterns rather than relying on unbounded runtime reflection. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><p>That distinction matters.</p><p>You do not need to panic every time you see:</p><pre><code><code>typeof(Customer)</code></code></pre><p>The question is whether the compiler and trimming analysis can understand what runtime code must remain available.</p><h2>Runtime Code Generation Is a Bigger Problem</h2><p>Some libraries dynamically generate code while the application runs.</p><p>For example, they may rely on:</p><pre><code><code>Reflection.Emit
Dynamic assemblies
Runtime proxy generation
Dynamic loading</code></code></pre><p>Native AOT does not support runtime code generation through mechanisms such as <code>System.Reflection.Emit</code>, and dynamic assembly-loading scenarios are also restricted. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?utm_source=chatgpt.com">Microsoft Learn</a></p><p>That has consequences for framework and library design.</p><p>A library built around:</p><pre><code><code>Discover types
     &#8595;
Generate implementation at runtime
     &#8595;
Compile dynamically
     &#8595;
Execute</code></code></pre><p>does not naturally fit an ahead-of-time world.</p><p>The AOT-friendly alternative is often:</p><pre><code><code>Discover requirements during build
     &#8595;
Generate source code
     &#8595;
Compile it with the application
     &#8595;
Execute already-generated native code</code></code></pre><p>That brings us to source generators.</p><h2>Source Generators Become Much More Important</h2><p>A source generator creates code during compilation.</p><p>Instead of discovering everything at runtime, the compiler can produce code ahead of time.</p><p>Conceptually:</p><pre><code><code>Application source
        +
Source generator
        &#8595;
Generated C# code
        &#8595;
Compilation
        &#8595;
Known executable code</code></code></pre><p>This aligns beautifully with Native AOT.</p><p>The compiler can see what exists.</p><p>The trimmer can reason about it.</p><p>The Native AOT compiler can generate the required native code.</p><p>Microsoft specifically recommends source generators as a way to avoid runtime reflection in Native AOT applications. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><p>This is not merely a workaround.</p><p>It represents a broader shift from <strong>runtime discovery toward compile-time knowledge</strong>.</p><h2>JSON Serialization Is a Great Example</h2><p>Serialization traditionally relies heavily on reflection.</p><p>Imagine:</p><pre><code><code>var json =
    JsonSerializer.Serialize(customer);</code></code></pre><p>A serializer needs to understand:</p><pre><code><code>Which properties exist?
Which names should they use?
Which converters apply?
How should values be created?</code></code></pre><p>Runtime reflection can answer those questions dynamically.</p><p>For Native AOT, <code>System.Text.Json</code> supports source generation so serialization metadata can be generated during compilation.</p><p>For example:</p><pre><code><code>[JsonSerializable(typeof(Customer))]
[JsonSerializable(typeof(Customer[]))]
internal partial class AppJsonContext
    : JsonSerializerContext
{
}</code></code></pre><p>Then configure ASP.NET Core to use the generated metadata:</p><pre><code><code>builder.Services.ConfigureHttpJsonOptions(
    options =&gt;
    {
        options.SerializerOptions
            .TypeInfoResolverChain
            .Insert(
                0,
                AppJsonContext.Default);
    });</code></code></pre><p>This tells the build system which serialization shapes the application needs.</p><p>ASP.NET Core&#8217;s Native AOT guidance specifically points developers toward JSON source generation because reflection-based serialization is problematic under trimming and Native AOT. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><h2>This Changes How We Think About &#8220;Magic&#8221;</h2><p>Developers often enjoy frameworks that automatically discover everything.</p><p>For example:</p><pre><code><code>Scan every assembly
Find every handler
Find every validator
Find every mapper
Create proxies
Build runtime metadata</code></code></pre><p>That can create an elegant developer experience.</p><p>But it depends on runtime dynamism.</p><p>Native AOT rewards systems that say:</p><pre><code><code>These are my endpoints.

These are my JSON types.

These are my dependencies.

These are my handlers.

These are the capabilities I need.</code></code></pre><p>In other words:</p><p><strong>Explicit architecture becomes easier to compile ahead of time.</strong></p><p>That can initially feel restrictive.</p><p>It can also make application behavior more understandable.</p><h2>NuGet Dependencies Become Part of Your AOT Decision</h2><p>You can write perfectly AOT-friendly application code and still fail to achieve a clean Native AOT publish because of a dependency.</p><p>Imagine:</p><pre><code><code>Your application
      &#8595;
Library A
      &#8595;
Library B
      &#8595;
Library C</code></code></pre><p>Library C uses runtime code generation.</p><p>Now your application inherits that compatibility problem.</p><p>Microsoft&#8217;s Native AOT documentation explicitly notes that popular ASP.NET Core libraries may have compatibility issues when they rely on reflection, conditional runtime loading, or runtime code generation. <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot?tabs=netcore-cli&amp;view=aspnetcore-8.0&amp;utm_source=chatgpt.com">Microsoft Learn</a></p><p>This means dependency selection becomes more important.</p><p>Before committing a service to Native AOT, test its actual dependency graph.</p><p>Do not assume:</p><pre><code><code>"It is a .NET library,
so Native AOT will support it."</code></code></pre><h2>Publish Warnings Are Architecture Feedback</h2><p>Suppose publishing produces an AOT warning.</p><p>The worst response is:</p><pre><code><code>The app still starts.
Ignore it.</code></code></pre><p>Warnings often mean the compiler cannot guarantee that required runtime behavior has been preserved.</p><p>Microsoft explicitly advises reviewing and correcting AOT warnings, and notes that an application producing warnings during publishing may not behave correctly. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><p>Treat these warnings differently from cosmetic compiler noise.</p><p>They are telling you:</p><p><strong>&#8220;Your application expects behavior that is difficult to guarantee in this deployment model.&#8221;</strong></p><p>That is valuable architectural feedback.</p><h2>Native AOT Does Not Support Every ASP.NET Core Feature Equally</h2><p>This is one of the biggest practical limitations.</p><p>ASP.NET Core is a broad platform.</p><p>It includes:</p><pre><code><code>Minimal APIs
MVC
Razor
SignalR
gRPC
Authentication
Static files
WebSockets
Caching
Localization
Health checks
Middleware</code></code></pre><p>Native AOT compatibility varies by feature and continues to evolve.</p><p>Microsoft&#8217;s current guidance says Native AOT is supported for ASP.NET Core application types including Minimal APIs, gRPC, and Worker Services, but not every ASP.NET Core feature or programming model is compatible. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><p>This makes Native AOT particularly attractive for focused services.</p><h2>Minimal APIs Are a Natural Fit</h2><p>Consider a small service:</p><pre><code><code>var builder =
    WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

app.MapGet(
    "/health",
    () =&gt; Results.Ok(
        new { Status = "Healthy" }));

app.Run();</code></code></pre><p>Its architecture is explicit.</p><p>It has:</p><pre><code><code>Few dependencies
Known endpoints
Known serialization types
Limited dynamic behavior
Small feature surface</code></code></pre><p>That is almost the ideal Native AOT profile.</p><p>Compare it with a mature application that dynamically discovers:</p><pre><code><code>Controllers
Plugins
Runtime-generated proxies
Reflection-heavy mapping
Dynamic serialization types
Third-party modules</code></code></pre><p>Migration becomes much more complicated.</p><p>Native AOT therefore often works best when it is an <strong>architectural choice made early</strong>, rather than an optimization bolted onto a highly dynamic application years later.</p><h2>gRPC Is Another Strong Candidate</h2><p>ASP.NET Core gRPC supports Native AOT, and Microsoft provides dedicated guidance for publishing gRPC client and server applications using it. <a href="https://learn.microsoft.com/en-us/aspnet/core/grpc/native-aot?view=aspnetcore-10.0&amp;utm_source=chatgpt.com">Microsoft Learn</a></p><p>That pairing makes sense.</p><p>Protocol Buffers already rely heavily on known contracts.</p><p>A <code>.proto</code> definition gives tooling a clear understanding of:</p><pre><code><code>Messages
Fields
Services
Methods</code></code></pre><p>Much of the required code can therefore be generated ahead of time.</p><p>Again, compile-time knowledge aligns well with AOT.</p><h2>Worker Services Can Benefit Too</h2><p>Native AOT is not only about HTTP APIs.</p><p>Consider a lightweight background service that:</p><pre><code><code>Reads queue message
     &#8595;
Processes it
     &#8595;
Writes result
     &#8595;
Terminates</code></code></pre><p>or workers that frequently scale up and down.</p><p>Fast startup and reduced memory overhead can be useful.</p><p>The smaller and more focused the process, the easier it is often to satisfy Native AOT constraints.</p><p>This suggests an important design lesson:</p><p><strong>Native AOT can be evaluated per service rather than per organization.</strong></p><p>You do not need to declare:</p><pre><code><code>Every .NET application we own
must use Native AOT.</code></code></pre><p>That would be unnecessarily rigid.</p><h2>Native AOT and Microservices</h2><p>Imagine an architecture containing:</p><pre><code><code>Public Web Application
Order API
Pricing API
Image Worker
Email Worker
Reporting Service
Identity Service</code></code></pre><p>Perhaps:</p><pre><code><code>Pricing API
Image Worker
Email Worker</code></code></pre><p>are excellent Native AOT candidates.</p><p>Meanwhile:</p><pre><code><code>Public Web Application
Reporting Service</code></code></pre><p>depend heavily on frameworks or libraries that are not good AOT fits.</p><p>That is fine.</p><p>Deployment models do not have to be ideological.</p><p>Use Native AOT where its advantages outweigh its restrictions.</p><h2>Faster Startup Is Not Faster Everything</h2><p>This point deserves emphasis.</p><p>Native AOT is often described using the word:</p><p><strong>performance</strong></p><p>That can be misunderstood as:</p><pre><code><code>Every request will become faster.</code></code></pre><p>Not necessarily.</p><p>Native AOT&#8217;s most predictable advantages are around areas such as:</p><pre><code><code>Startup
Deployment footprint
Runtime memory overhead</code></code></pre><p>Steady-state request performance depends on the workload.</p><p>JIT compilation also has advantages because runtime compilation can optimize code with knowledge gathered during execution.</p><p>Native AOT therefore should not be sold as:</p><p><strong>&#8220;Turn this on and every benchmark improves.&#8221;</strong></p><p>Benchmark your workload.</p><h2>JIT Has Advantages Too</h2><p>JIT compilation sounds inefficient if we describe it only as:</p><pre><code><code>Compilation work during runtime</code></code></pre><p>But runtime compilation has information that an ahead-of-time compiler may not have.</p><p>It knows:</p><pre><code><code>Actual execution environment
Runtime behavior
Frequently executed paths
Real type usage</code></code></pre><p>Modern .NET uses sophisticated optimization mechanisms around JIT compilation.</p><p>Native AOT gives up some runtime flexibility in exchange for predictability and startup characteristics.</p><p>So the comparison is not:</p><pre><code><code>Old slow JIT
vs
New fast AOT</code></code></pre><p>It is:</p><pre><code><code>Runtime optimization and flexibility
vs
Ahead-of-time predictability and reduced runtime machinery</code></code></pre><p>That is a much more useful mental model.</p><h2>Native AOT Can Increase Publish Complexity</h2><p>A normal build may finish quickly.</p><p>Native AOT publishing has more work to do.</p><p>It needs to:</p><pre><code><code>Analyze dependencies
Perform trimming analysis
Analyze AOT compatibility
Compile native code
Link native output
Produce platform-specific artifacts</code></code></pre><p>So we trade:</p><pre><code><code>More expensive publishing</code></code></pre><p>for:</p><pre><code><code>Less runtime compilation</code></code></pre><p>This is usually reasonable for production builds.</p><p>But it changes CI/CD expectations.</p><p>Your build pipeline should measure:</p><pre><code><code>Publish duration
Artifact size
Build resource consumption
AOT warnings
Deployment startup time</code></code></pre><p>Native AOT optimizes one part of the lifecycle partly by moving cost elsewhere.</p><h2>Cross-Platform Deployment Needs More Planning</h2><p>A traditional .NET application can often publish IL that runs wherever the appropriate .NET runtime exists.</p><p>Native AOT produces platform-specific native executables.</p><p>Microsoft documents supported operating systems and architectures separately for Native AOT and requires publishing for the intended target runtime. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?utm_source=chatgpt.com">Microsoft Learn</a></p><p>So if you deploy to:</p><pre><code><code>Linux x64
Linux Arm64
Windows x64</code></code></pre><p>you should think in terms of separate native artifacts.</p><p>For containerized cloud services, that is often manageable because the target environment is already well defined.</p><p>For software distributed broadly to unknown machines, deployment planning may be more complicated.</p><h2>Native AOT Does Not Remove the Runtime Completely</h2><p>A common misconception is:</p><pre><code><code>Native AOT means no .NET runtime exists.</code></code></pre><p>The better explanation is that the application no longer requires a separately installed .NET runtime or runtime JIT compilation in the normal way.</p><p>The published application is self-contained and includes the runtime components it needs. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?utm_source=chatgpt.com">Microsoft Learn</a></p><p>So:</p><pre><code><code>Native AOT
&#8800;
.NET disappeared</code></code></pre><p>Instead:</p><pre><code><code>Required runtime support
is packaged into the
native application.</code></code></pre><p>That distinction matters when reasoning about binary size and deployment.</p><h2>Reflection-Based Architecture Needs Scrutiny</h2><p>Suppose an application contains:</p><pre><code><code>foreach (
    var assembly in
    AppDomain.CurrentDomain.GetAssemblies())
{
    // Discover implementations dynamically.
}</code></code></pre><p>That kind of architecture may be convenient.</p><p>But ask:</p><p><strong>Could we register those types at compile time instead?</strong></p><p>Instead of:</p><pre><code><code>Runtime scans everything
     &#8595;
Discovers handlers</code></code></pre><p>we might use:</p><pre><code><code>Source generator
     &#8595;
Produces registrations
     &#8595;
Compile</code></code></pre><p>This can reduce startup work and improve AOT compatibility.</p><p>Even if you ultimately decide not to use Native AOT, the exercise may reveal opportunities to make application startup more deterministic.</p><h2>Dynamic Plugin Systems Are Poorer Fits</h2><p>Imagine an application that allows customers to drop arbitrary DLLs into:</p><pre><code><code>/plugins</code></code></pre><p>and loads them dynamically:</p><pre><code><code>Assembly.LoadFrom(pluginPath);</code></code></pre><p>This model depends on runtime discovery and loading.</p><p>Native AOT deployment explicitly limits dynamic loading scenarios such as <code>Assembly.LoadFile</code>. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?utm_source=chatgpt.com">Microsoft Learn</a></p><p>That does not mean the plugin architecture is wrong.</p><p>It means the architecture values something Native AOT deliberately gives up:</p><p><strong>runtime extensibility.</strong></p><p>If runtime extensibility is a core requirement, JIT-based deployment may simply be the better choice.</p><h2>Entity Framework Core Needs Special Attention</h2><p>Many ASP.NET Core applications depend on Entity Framework Core.</p><p>This is an area where developers should check the documentation for the exact EF Core version they intend to deploy rather than assuming compatibility.</p><p>Current Microsoft documentation describes EF Core&#8217;s NativeAOT support and precompiled-query infrastructure as <strong>highly experimental and not yet suitable for production use</strong>. <a href="https://learn.microsoft.com/en-us/ef/core/performance/nativeaot-and-precompiled-queries?utm_source=chatgpt.com">Microsoft Learn</a></p><p>That is a major consideration.</p><p>Suppose your service&#8217;s architecture is:</p><pre><code><code>Minimal API
     &#8595;
EF Core
     &#8595;
SQL Server</code></code></pre><p>You should not evaluate only whether Minimal APIs support Native AOT.</p><p>You must evaluate the entire stack.</p><p>This illustrates a rule worth remembering:</p><p><strong>Your application is only as AOT-compatible as the code it actually needs to execute.</strong></p><h2>Container Size Needs to Be Measured Honestly</h2><p>Native AOT can reduce application footprint and enable smaller container images.</p><p>But container size depends on more than the executable.</p><p>Your image may also contain:</p><pre><code><code>Linux base image
Certificates
Native libraries
ICU data
Diagnostics tools
Configuration
Other operating-system packages</code></code></pre><p>So do not say:</p><pre><code><code>Native AOT reduced our executable,
therefore our container is tiny.</code></code></pre><p>Measure the actual artifact that reaches production.</p><p>For example:</p><pre><code><code>docker images</code></code></pre><p>Compare:</p><pre><code><code>Normal deployment image
Trimmed deployment image
Native AOT image</code></code></pre><p>Then evaluate whether the operational improvement is meaningful.</p><h2>Measure Startup Correctly</h2><p>Startup benchmarks can also mislead.</p><p>Ask:</p><p><strong>What does &#8220;started&#8221; mean?</strong></p><p>Possible definitions include:</p><pre><code><code>Process created
Host initialized
Port listening
Health check succeeds
Database connection ready
First request completed
Service ready for real traffic</code></code></pre><p>The metric that matters operationally is usually:</p><p><strong>Time until the instance can safely serve production traffic.</strong></p><p>A 50 ms process startup is not useful if application initialization then spends five seconds warming a remote dependency.</p><p>Native AOT improves runtime startup characteristics.</p><p>It does not eliminate your own startup work.</p><h2>Do Not Move Expensive Work Into Startup</h2><p>Suppose Native AOT saves:</p><pre><code><code>500 ms</code></code></pre><p>but then <code>Program.cs</code> does:</p><pre><code><code>Download configuration
Warm enormous cache
Scan remote storage
Preload millions of rows
Call five external APIs</code></code></pre><p>for:</p><pre><code><code>12 seconds</code></code></pre><p>Your effective startup remains slow.</p><p>Optimize startup as a complete path:</p><pre><code><code>Process creation
      &#8595;
Host initialization
      &#8595;
Dependency setup
      &#8595;
Application initialization
      &#8595;
Readiness</code></code></pre><p>Native AOT is one part of that chain.</p><h2>Diagnostics Are Part of the Trade-Off</h2><p>Native AOT changes diagnostics too.</p><p>Microsoft documents limitations around debugging and profiling Native AOT applications, and Native debug information is produced separately by default on supported platforms. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?utm_source=chatgpt.com">Microsoft Learn</a></p><p>This does not mean Native AOT applications are impossible to diagnose.</p><p>It means you should verify your actual production toolchain.</p><p>Before migration, test:</p><pre><code><code>Crash dumps
Profilers
APM agents
Tracing
Metrics
Logging
Debug symbols
Incident-response tooling</code></code></pre><p>A service that starts 300 milliseconds faster but becomes dramatically harder for your operations team to diagnose may not be an improvement.</p><p>Performance is only one production requirement.</p><h2>Observability Still Matters</h2><p>Native AOT does not change the need for observability.</p><p>You should still measure:</p><pre><code><code>Startup duration
Memory usage
CPU
Request latency
Throughput
Error rate
Container restart time
Readiness duration
Deployment duration</code></code></pre><p>This connects to our earlier article on <strong>Distributed Tracing with OpenTelemetry</strong>.</p><p>Do not migrate to Native AOT and simply assume the application became better.</p><p>Measure before and after.</p><h2>Build a Comparison</h2><p>Suppose we have a small API.</p><p>Create three deployment variants:</p><pre><code><code>Normal self-contained
Trimmed
Native AOT</code></code></pre><p>Then measure:</p><pre><code><code>Published size
Container image size
Cold startup
Time to readiness
Idle memory
Memory under load
p50 latency
p95 latency
p99 latency
Throughput
CPU
Build time</code></code></pre><p>Now the decision becomes evidence-based.</p><p>Perhaps Native AOT reduces:</p><pre><code><code>Startup: 800 ms &#8594; 120 ms
Container: 150 MB &#8594; 65 MB
Idle memory: 110 MB &#8594; 70 MB</code></code></pre><p>Those numbers are illustrative, not promises.</p><p>Your application must supply the real measurements.</p><p>Maybe those improvements matter enormously.</p><p>Maybe they do not.</p><h2>The Economics Depend on Scale</h2><p>Imagine a service running:</p><pre><code><code>3 instances
24 hours a day</code></code></pre><p>Startup occurs rarely.</p><p>A complex migration may provide little business value.</p><p>Now imagine:</p><pre><code><code>2,000 small instances
frequently scaling
across multiple regions</code></code></pre><p>Even modest per-instance savings can accumulate.</p><p>This is why Microsoft notes that Native AOT can be particularly valuable for workloads with large numbers of deployed instances, including cloud infrastructure and hyperscale services. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?utm_source=chatgpt.com">Microsoft Learn</a></p><p>Optimization should always ask:</p><p><strong>What cost are we actually trying to reduce?</strong></p><h2>A Good Native AOT Candidate</h2><p>Consider a pricing service.</p><p>It has:</p><pre><code><code>Minimal APIs
Small dependency graph
System.Text.Json source generation
No dynamic plugins
No runtime code generation
High instance count
Frequent autoscaling
Strict container memory limits</code></code></pre><p>That is a strong candidate.</p><p>Native AOT aligns with its architecture.</p><p>Now consider an enterprise application containing:</p><pre><code><code>Large MVC architecture
Dynamic plugin ecosystem
Reflection-heavy libraries
Runtime proxy generation
Large dependency graph
Complex ORM requirements
Rare restarts
Plenty of memory</code></code></pre><p>Native AOT may demand substantial engineering effort for limited operational benefit.</p><p>Neither architecture is inherently better.</p><p>They optimize for different requirements.</p><h2>Do Not Rewrite a Healthy Application Just to Say &#8220;AOT&#8221;</h2><p>Native AOT is an engineering tool, not a maturity badge.</p><p>A production system should not be rewritten merely because:</p><pre><code><code>Native sounds faster.</code></code></pre><p>Ask instead:</p><pre><code><code>Is startup a real problem?

Are container images causing deployment friction?

Is memory density important?

Are we frequently scaling instances?

Are our dependencies compatible?

Can we eliminate problematic dynamic behavior?

Can we maintain the resulting architecture?

Can we still diagnose production failures?</code></code></pre><p>If those answers support Native AOT, it can be an excellent choice.</p><p>If not, ordinary JIT-compiled ASP.NET Core remains an extremely capable deployment model.</p><h2>Native AOT Can Influence Architecture Before You Adopt It</h2><p>There is another benefit that is easy to miss.</p><p>You can use AOT compatibility as an architectural signal even if you never publish with Native AOT.</p><p>AOT-friendly code tends to encourage:</p><pre><code><code>Explicit dependencies
Known serialization contracts
Less hidden reflection
Smaller dependency graphs
Compile-time generation
Predictable startup
Focused services</code></code></pre><p>Those properties can improve understandability and maintainability.</p><p>That does not mean all dynamic behavior is bad.</p><p>Dynamic behavior is powerful precisely because it enables capabilities static systems cannot easily provide.</p><p>The lesson is to use it intentionally.</p><h2>How Native AOT Connects to Memory Management</h2><p>Two articles ago, we explored <strong>Memory Management in ASP.NET Core</strong>.</p><p>Native AOT approaches memory from a different direction.</p><p>Memory optimization asked:</p><pre><code><code>How much memory does our running
application allocate and retain?</code></code></pre><p>Native AOT also asks:</p><pre><code><code>How much runtime machinery
does this process need at all?</code></code></pre><p>These strategies complement each other.</p><p>Native AOT will not save an application that:</p><pre><code><code>Caches everything forever
Allocates giant buffers
Creates unbounded queues
Loads huge datasets into memory</code></code></pre><p>Application-level memory discipline still matters.</p><h2>How Native AOT Connects to High-Performance I/O</h2><p>Our previous article covered <strong>High-Performance I/O in ASP.NET Core</strong>.</p><p>Suppose Native AOT reduces startup by hundreds of milliseconds.</p><p>Great.</p><p>But your API still:</p><pre><code><code>Loads 2 GB files into memory
Blocks on synchronous I/O
Copies buffers repeatedly
Ignores cancellation</code></code></pre><p>Native AOT does not fix that.</p><p>These optimizations operate at different layers.</p><pre><code><code>Native AOT
&#8594; startup and deployment model

Streaming and pipelines
&#8594; data movement

Memory management
&#8594; allocation and retention

Backpressure
&#8594; overload control</code></code></pre><p>A genuinely high-performance ASP.NET Core system needs the layers to work together.</p><h2>A Practical Native AOT Adoption Strategy</h2><p>Do not begin with your largest application.</p><p>Start with a small candidate.</p><p><strong>Step 1: Choose a suitable service.</strong></p><p>Prefer one with Minimal APIs, limited dependencies, and little runtime dynamism.</p><p><strong>Step 2: Enable </strong><code>PublishAot</code><strong>.</strong></p><pre><code><code>&lt;PublishAot&gt;true&lt;/PublishAot&gt;</code></code></pre><p><strong>Step 3: Publish immediately.</strong></p><p>Do this before investing heavily in migration.</p><p><strong>Step 4: Read every AOT and trimming warning.</strong></p><p>Identify which problems come from your code and which come from dependencies.</p><p><strong>Step 5: Replace reflection-heavy patterns where practical.</strong></p><p>Prefer source generation and explicit registration.</p><p><strong>Step 6: Configure source-generated JSON serialization.</strong></p><p>Make serialization requirements known during compilation.</p><p><strong>Step 7: Test the actual published executable.</strong></p><p>Do not rely only on <code>dotnet run</code>.</p><p><strong>Step 8: Run your integration test suite against the AOT artifact.</strong></p><p>Exercise real application paths.</p><p><strong>Step 9: Test observability and diagnostics.</strong></p><p>Confirm your production tooling still works.</p><p><strong>Step 10: Benchmark against the existing deployment.</strong></p><p>Compare startup, memory, size, throughput, and latency.</p><p><strong>Step 11: Evaluate operational value.</strong></p><p>Ask whether the measured improvement justifies the restrictions and maintenance cost.</p><p>This prevents Native AOT from becoming a technology experiment disguised as a production requirement.</p><h2>Common Native AOT Mistakes</h2><p>Several mistakes are worth avoiding.</p><p><strong>Assuming Native AOT makes every request faster.</strong> Its most obvious advantages are startup, footprint, and potentially memory demand.</p><p><strong>Ignoring publish warnings.</strong> AOT warnings can identify real runtime compatibility problems.</p><p><strong>Testing only with </strong><code>dotnet run</code><strong>.</strong> The published Native AOT artifact is what matters.</p><p><strong>Assuming every NuGet library is compatible.</strong> Dependencies may rely on reflection or runtime code generation.</p><p><strong>Treating reflection as completely forbidden.</strong> The real issue is whether required runtime behavior can be determined and preserved ahead of time.</p><p><strong>Expecting Native AOT to fix application memory problems.</strong> Unbounded caches and oversized buffers remain unbounded caches and oversized buffers.</p><p><strong>Choosing AOT before measuring startup.</strong> Optimize an actual problem.</p><p><strong>Ignoring diagnostics.</strong> Production operability matters as much as startup speed.</p><p><strong>Migrating a highly dynamic monolith first.</strong> A small focused service is usually a better learning environment.</p><p><strong>Treating AOT as an organization-wide rule.</strong> Different services can use different deployment models.</p><h2>What Successful Native AOT Adoption Looks Like</h2><p>A successful migration does not end with:</p><pre><code><code>Publish succeeded.</code></code></pre><p>It looks more like:</p><pre><code><code>Clean AOT analysis
        &#8595;
Compatible dependency graph
        &#8595;
Integration tests pass
        &#8595;
Observability works
        &#8595;
Smaller production artifact
        &#8595;
Faster time to readiness
        &#8595;
Lower or predictable memory
        &#8595;
No unacceptable feature loss
        &#8595;
Operational benefit exceeds complexity</code></code></pre><p>Only then do we have an optimization.</p><h2>Coming Next</h2><p>In the next article, we&#8217;ll explore <strong>Benchmarking ASP.NET Core Applications: BenchmarkDotNet, Load Testing, and Finding Real Bottlenecks</strong>.</p><p>That article follows naturally from everything we have just discussed.</p><p>Native AOT claims faster startup and potentially lower memory requirements.</p><p>Streaming claims lower memory pressure.</p><p>Pooling claims fewer allocations.</p><p>Backpressure claims greater stability.</p><p>But how do we prove any of it?</p><p>We measure.</p><p>We&#8217;ll separate microbenchmarks from application load tests, examine BenchmarkDotNet, test realistic ASP.NET Core endpoints, understand throughput and latency percentiles, and learn why optimizing the wrong benchmark can make production software worse rather than better.</p><h2>Closing Thoughts</h2><p>Native AOT changes a fundamental assumption about .NET applications.</p><p>Traditionally, .NET has embraced a highly capable runtime environment that can compile code, inspect metadata, discover types, generate implementations, and adapt while the application is running.</p><p>Native AOT deliberately moves in another direction.</p><p>It asks:</p><pre><code><code>What can we know before
the application starts?</code></code></pre><p>If we know the answer during compilation, we can potentially remove work and machinery from runtime.</p><p>That can produce meaningful advantages:</p><pre><code><code>Faster startup
Smaller deployment footprint
Lower memory demand
Faster container activation
Greater deployment density</code></code></pre><p>Microsoft explicitly positions these as key benefits of Native AOT for ASP.NET Core. <a href="https://learn.microsoft.com/aspnet/core/fundamentals/native-aot?utm_source=chatgpt.com">Microsoft Learn</a></p><p>But those benefits come with constraints.</p><p>Dynamic code generation becomes problematic.</p><p>Unbounded reflection becomes problematic.</p><p>Dynamic assembly loading becomes problematic.</p><p>Dependencies must be evaluated.</p><p>Trimming becomes part of application correctness.</p><p>Diagnostics require consideration.</p><p>Some ASP.NET Core features remain incompatible or only partially supported. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?utm_source=chatgpt.com">Microsoft Learn</a></p><p>That is why the correct question is not:</p><pre><code><code>Should ASP.NET Core applications
use Native AOT?</code></code></pre><p>The better question is:</p><pre><code><code>Does this application benefit enough
from Native AOT to justify designing
within its constraints?</code></code></pre><p>For a small, focused, frequently scaled service, the answer may be compelling.</p><p>For a large, dynamic application that rarely restarts, the traditional runtime may remain the better engineering choice.</p><p>And that is the real lesson.</p><p><strong>Native AOT is most powerful when it matches the architecture, not when the architecture is forced to match Native AOT.</strong></p><h2>Subscribe Now</h2><p>Enjoying the series? <a href="https://www.asptoday.com/">Subscribe to </a><strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, advanced architecture deep dives, and production-ready .NET performance strategies. Join our Substack Chat to discuss Native AOT, container optimization, application performance, deployment architecture, and the engineering decisions behind fast, efficient ASP.NET Core systems. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[High-Performance I/O in ASP.NET Core: Streams, Pipelines, and Efficient Data Processing]]></title><description><![CDATA[Build faster ASP.NET Core I/O with streams, pipelines, and efficient data processing. #ASPToday #aspnetcore #dotnet #performance]]></description><link>https://www.asptoday.com/p/high-performance-io-in-aspnet-core</link><guid isPermaLink="false">https://www.asptoday.com/p/high-performance-io-in-aspnet-core</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 22 Sep 2026 15:01:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cwk6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>An <a href="https://www.asptoday.com/p/microservices-architecture-with-aspnet">ASP.NET Core application</a> spends much of its life moving data. Request bodies arrive from clients, database results flow into services, files move through APIs, JSON is parsed and generated, and responses travel back across the network.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cwk6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cwk6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!cwk6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!cwk6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!cwk6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cwk6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2935321,&quot;alt&quot;:&quot;Cargo ships move through canal locks, symbolizing efficient data streaming and controlled I/O. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/216862460?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Cargo ships move through canal locks, symbolizing efficient data streaming and controlled I/O. " title="Cargo ships move through canal locks, symbolizing efficient data streaming and controlled I/O. " srcset="https://substackcdn.com/image/fetch/$s_!cwk6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!cwk6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!cwk6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!cwk6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5943f6c7-ae26-497c-8e31-f9e336aa6870_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When that data is small, almost any reasonable approach works. At scale, however, unnecessary buffering, synchronous I/O, repeated copying, and oversized in-memory payloads can consume memory, block threads, increase latency, and limit throughput. High-performance I/O is therefore not about making the network magically faster. It is about designing the application so data can move through the system efficiently, asynchronously, and with as little unnecessary work as possible.</p><h2>The Real Problem Is Often How We Move the Data</h2><p>In our previous article on <strong><a href="https://www.asptoday.com/p/memory-management-in-aspnet-core">Memory Management in ASP.NET Core</a></strong>, we looked at garbage collection, allocations, the Large Object Heap, and object pooling.</p><p>Consider an API that receives a 500 MB file.</p><p>A straightforward implementation might do this:</p><pre><code><code>Receive 500 MB
     &#8595;
Load 500 MB into memory
     &#8595;
Convert or deserialize it
     &#8595;
Process it
     &#8595;
Build another large result
     &#8595;
Send the response</code></code></pre><p>The file may only be 500 MB, but the application could temporarily consume much more than 500 MB because multiple representations exist at the same time.</p><p>Now imagine ten users performing that operation concurrently.</p><p>The problem is no longer merely file processing.</p><p>It becomes a problem involving:</p><pre><code><code>Memory
Garbage collection
Thread usage
Network throughput
Disk throughput
Concurrency
Backpressure
Latency</code></code></pre><p>A better architecture tries to move data progressively:</p><pre><code><code>Receive chunk
     &#8595;
Process chunk
     &#8595;
Write result
     &#8595;
Receive next chunk</code></code></pre><p>Instead of thinking:</p><p><strong>&#8220;How quickly can I load this data?&#8221;</strong></p><p>think:</p><p><strong>&#8220;How little of this data needs to exist in memory at any one moment?&#8221;</strong></p><p>That shift is fundamental to high-performance I/O.</p><h2>I/O Is Different from CPU Work</h2><p>Imagine two operations.</p><p>The first calculates millions of mathematical values:</p><pre><code><code>CPU
&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;</code></code></pre><p>The processor is actively working.</p><p>The second waits for data from a remote storage service:</p><pre><code><code>Send request
     &#8595;
Wait...
     &#8595;
Wait...
     &#8595;
Data arrives</code></code></pre><p>During much of that time, the CPU has nothing useful to do for that operation.</p><p>This distinction explains why asynchronous programming is so important for server I/O.</p><p>Microsoft&#8217;s ASP.NET Core performance guidance recommends asynchronous APIs for data access and I/O because blocking calls can lead to Thread Pool starvation and degraded response times. </p><p>Consider:</p><pre><code><code>var data = stream.Read(buffer, 0, buffer.Length);</code></code></pre><p>This synchronous operation may occupy a thread while waiting for data.</p><p>The asynchronous equivalent is:</p><pre><code><code>var bytesRead = await stream.ReadAsync(
    buffer,
    cancellationToken);</code></code></pre><p>While the operation is waiting, the thread can be used for other work.</p><p>That matters enormously in a web server handling many concurrent requests.</p><h2>Async Does Not Make the Storage Device Faster</h2><p>This is an important distinction.</p><p>Suppose a storage operation takes 200 milliseconds.</p><p>Changing:</p><pre><code><code>stream.Read(...)</code></code></pre><p>to:</p><pre><code><code>await stream.ReadAsync(...)</code></code></pre><p>does not magically turn the disk or network into a faster device.</p><p>The advantage is scalability.</p><p>With synchronous I/O:</p><pre><code><code>Request A &#8594; Thread waits
Request B &#8594; Thread waits
Request C &#8594; Thread waits
Request D &#8594; Thread waits</code></code></pre><p>With asynchronous I/O:</p><pre><code><code>Request A &#8594; waiting for I/O
Request B &#8594; waiting for I/O
Request C &#8594; waiting for I/O

Threads remain available
for other runnable work</code></code></pre><p>ASP.NET Core can therefore handle large numbers of concurrent I/O-bound operations without requiring one blocked thread for every waiting request.</p><p>This is why <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/best-practices?view=aspnetcore-10.0&amp;utm_source">Microsoft specifically advises against synchronous reads and writes</a> on <code>HttpRequest</code> and <code>HttpResponse</code> bodies. </p><h2>Streams Give Us a Better Mental Model</h2><p>A <code>Stream</code> represents a sequence of bytes.</p><p>It does not necessarily represent a complete object already loaded into memory.</p><p>Streams can represent:</p><pre><code><code>Files
Network connections
HTTP request bodies
HTTP responses
Memory
Compression layers
Cryptographic transformations</code></code></pre><p>For example:</p><pre><code><code>await using var stream = File.OpenRead("report.csv");

var buffer = new byte[8192];

while (true)
{
    var bytesRead = await stream.ReadAsync(buffer);

    if (bytesRead == 0)
        break;

    Process(
        buffer.AsSpan(0, bytesRead));
}</code></code></pre><p>The application does not need the entire file in memory.</p><p>It works on a small region at a time.</p><p>For a 10 GB file, our working buffer could still be only a few kilobytes.</p><p>That is the power of streaming.</p><h2>Buffering and Streaming Are Different Strategies</h2><p>Suppose an application receives:</p><pre><code><code>1 GB request</code></code></pre><p>A buffered approach looks like:</p><pre><code><code>Network
   &#8595;
&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;
Entire request in memory
   &#8595;
Processing</code></code></pre><p>A streaming approach looks more like:</p><pre><code><code>Network
   &#8595;
[chunk]
   &#8595;
Process
   &#8595;
[chunk]
   &#8595;
Process
   &#8595;
[chunk]</code></code></pre><p>Buffering is not inherently bad.</p><p>Small payloads are often easier and perfectly reasonable to buffer.</p><p>The problem is <strong>unbounded buffering</strong>.</p><p>Microsoft&#8217;s ASP.NET Core guidance warns against reading large request or response bodies into a single <code>byte[]</code> or <code>string</code>, partly because sufficiently large allocations enter the Large Object Heap and can contribute to expensive full garbage collections. </p><p>This connects directly to our previous article.</p><p>Memory-efficient I/O often means avoiding the giant allocation entirely.</p><h2>A Common Mistake: ReadToEnd</h2><p>Imagine middleware that needs to inspect a request.</p><p>This is tempting:</p><pre><code><code>using var reader =
    new StreamReader(context.Request.Body);

var body = await reader.ReadToEndAsync();</code></code></pre><p>For a small JSON request, this may be acceptable.</p><p>For a large upload, <code>body</code> may represent the entire request in memory.</p><p>If many requests arrive concurrently, memory consumption scales accordingly.</p><p>Instead, ask:</p><p><strong>Do I actually need the entire body simultaneously?</strong></p><p>If not, process the stream incrementally.</p><h2>Let the Serializer Stream When Possible</h2><p>Suppose an endpoint accepts JSON.</p><p>Instead of:</p><pre><code><code>using var reader =
    new StreamReader(Request.Body);

var json =
    await reader.ReadToEndAsync();

var order =
    JsonSerializer.Deserialize&lt;Order&gt;(json);</code></code></pre><p>we can deserialize directly from the stream:</p><pre><code><code>var order =
    await JsonSerializer.DeserializeAsync&lt;Order&gt;(
        Request.Body,
        cancellationToken:
            HttpContext.RequestAborted);</code></code></pre><p>We remove an unnecessary intermediate string.</p><p>Microsoft&#8217;s performance guidance specifically demonstrates asynchronous JSON deserialization directly from the request body as a preferable pattern. </p><p>The general rule is simple:</p><p><strong>Avoid creating an intermediate representation when the next API can consume the stream directly.</strong></p><h2>Cancellation Belongs in I/O Code</h2><p>Suppose a user begins downloading a large report and then closes the browser.</p><p>Should the server continue spending 30 seconds:</p><pre><code><code>Reading database rows
Generating data
Writing output
Calling storage</code></code></pre><p>for a client that no longer exists?</p><p>Usually not.</p><p>ASP.NET Core exposes request cancellation through:</p><pre><code><code>HttpContext.RequestAborted</code></code></pre><p>or by binding a <code>CancellationToken</code> directly in a Minimal API endpoint:</p><pre><code><code>app.MapGet(
    "/export",
    async (CancellationToken cancellationToken) =&gt;
    {
        // Use cancellationToken throughout.
    });</code></code></pre><p>That token should travel through the I/O path:</p><pre><code><code>await source.ReadAsync(
    buffer,
    cancellationToken);

await destination.WriteAsync(
    buffer,
    cancellationToken);</code></code></pre><p>Cancellation prevents abandoned requests from continuing to consume scarce resources.</p><p>This becomes particularly important for large or slow transfers.</p><h2>Stream Copying Can Be Surprisingly Simple</h2><p>Suppose an application retrieves a file from storage and sends it to the client.</p><p>A wasteful architecture might be:</p><pre><code><code>Storage
   &#8595;
byte[]
   &#8595;
MemoryStream
   &#8595;
another byte[]
   &#8595;
HTTP response</code></code></pre><p>Often we simply need:</p><pre><code><code>Storage stream
      &#8595;
HTTP response stream</code></code></pre><p>For example:</p><pre><code><code>await source.CopyToAsync(
    Response.Body,
    cancellationToken);</code></code></pre><p>Now the application acts more like a conduit.</p><p>Data moves through it rather than accumulating inside it.</p><p>ASP.NET Core also provides result types designed to return stream-based content. <code>FileStreamResult</code>, for example, represents an action result that writes a file from a stream to the HTTP response. </p><h2>But Streams Still Require Buffering Somewhere</h2><p>Streaming does not mean:</p><pre><code><code>No buffers exist.</code></code></pre><p>Network and file I/O require buffers.</p><p>The real question is:</p><p><strong>Who manages those buffers, how large are they, and how often are they copied?</strong></p><p>A conventional stream parser may need to manage:</p><pre><code><code>Partial messages
Buffer boundaries
Unused bytes
Resizing
Copying
Multiple buffers</code></code></pre><p>Imagine receiving newline-separated records:</p><pre><code><code>Alice\nBob\nCharlie\n</code></code></pre><p>A read might end here:</p><pre><code><code>Alice\nBob\nCha</code></code></pre><p>The next read contains:</p><pre><code><code>rlie\n</code></code></pre><p>Our parser must remember:</p><pre><code><code>Cha</code></code></pre><p>and combine it with:</p><pre><code><code>rlie</code></code></pre><p>This sounds simple.</p><p>At high throughput, correctly managing partial buffers becomes surprisingly complicated.</p><p>That is one of the problems <code>System.IO.Pipelines</code> was designed to solve.</p><h2>Enter System.IO.Pipelines</h2><p>Microsoft describes <code>System.IO.Pipelines</code> as a library designed to make high-performance I/O in .NET easier. It addresses much of the complicated buffer-management code required by streaming parsers. </p><p>The core abstractions are:</p><pre><code><code>PipeReader
PipeWriter</code></code></pre><p>Conceptually:</p><pre><code><code>Producer
   &#8595;
PipeWriter
   &#8595;
[managed buffers]
   &#8595;
PipeReader
   &#8595;
Consumer</code></code></pre><p>The writer produces bytes.</p><p>The reader consumes bytes.</p><p>The pipeline manages the memory between them.</p><p>This allows the producer and consumer to operate somewhat independently while sharing an efficient buffering system.</p><h2>ASP.NET Core Already Exposes Pipelines</h2><p>ASP.NET Core gives us both stream and pipeline APIs.</p><p>For requests:</p><pre><code><code>HttpRequest.Body</code></code></pre><p>is a <code>Stream</code>.</p><p>And:</p><pre><code><code>HttpRequest.BodyReader</code></code></pre><p>is a <code>PipeReader</code>.</p><p>For responses:</p><pre><code><code>HttpResponse.Body</code></code></pre><p>is a <code>Stream</code>.</p><p>And:</p><pre><code><code>HttpResponse.BodyWriter</code></code></pre><p>is a <code>PipeWriter</code>.</p><p>Microsoft&#8217;s current request and response documentation explicitly exposes both abstractions and notes that pipelines can provide performance advantages for advanced scenarios. </p><p>This does <strong>not</strong> mean every controller should immediately replace streams with pipelines.</p><p>Streams remain extremely useful.</p><p>Pipelines become particularly valuable when we need efficient parsing or writing on hot I/O paths.</p><h2>Reading with PipeReader</h2><p>A simplified reader loop looks like this:</p><pre><code><code>PipeReader reader =
    HttpContext.Request.BodyReader;

while (true)
{
    ReadResult result =
        await reader.ReadAsync(
            HttpContext.RequestAborted);

    ReadOnlySequence&lt;byte&gt; buffer =
        result.Buffer;

    // Inspect and process available bytes.

    reader.AdvanceTo(
        buffer.Start,
        buffer.End);

    if (result.IsCompleted)
        break;
}</code></code></pre><p>The exact parsing logic depends on the protocol.</p><p>The important point is that the <code>PipeReader</code> provides access to buffered data without forcing us to continually allocate our own arrays.</p><p>Microsoft notes that <code>HttpRequest.BodyReader</code> directly accesses the request body and manages memory for the caller, avoiding an additional request-data copy into a caller-managed buffer. </p><h2>ReadOnlySequence&lt;byte&gt; Matters</h2><p>Pipeline data may not exist in one contiguous block of memory.</p><p>Instead, we may receive:</p><pre><code><code>Segment 1
[ABCDEFG]

Segment 2
[HIJKLMN]

Segment 3
[OPQRST]</code></code></pre><p>Together they form a logical sequence.</p><p><code>ReadOnlySequence&lt;byte&gt;</code> lets us treat those segments as one sequence without first combining them into another giant array.</p><p>That is important because this:</p><pre><code><code>Segment 1
Segment 2
Segment 3
     &#8595;
Allocate giant array
     &#8595;
Copy everything</code></code></pre><p>creates work that may not be necessary.</p><p>High-performance I/O frequently comes down to one principle:</p><p><strong>Move references to data whenever possible instead of repeatedly moving the data itself.</strong></p><h2>AdvanceTo Is Not Optional Housekeeping</h2><p><code>PipeReader</code> requires the consumer to tell it how much data has been processed.</p><p>That is the purpose of:</p><pre><code><code>reader.AdvanceTo(
    consumed,
    examined);</code></code></pre><p>These positions mean different things.</p><p><code>consumed</code> tells the pipeline:</p><p><strong>We no longer need data before this point.</strong></p><p><code>examined</code> tells it:</p><p><strong>We have inspected data up to this point.</strong></p><p>Getting these positions wrong can cause serious problems.</p><p>If we fail to consume data properly, buffers may be retained longer than necessary.</p><p>If we report examined data incorrectly, the reader may behave unexpectedly or wait for data when it should continue processing.</p><p>Microsoft&#8217;s pipeline documentation specifically warns that incorrect <code>PipeReader</code> usage can lead to excessive memory use, hangs, infinite loops, or corrupted processing logic. </p><p>Pipelines remove a lot of buffer-management complexity.</p><p>They do not remove the need to understand ownership.</p><h2>Parsing Delimited Data Efficiently</h2><p>Imagine receiving:</p><pre><code><code>1001,temperature,28.3\n
1002,temperature,28.4\n
1003,temperature,28.7\n</code></code></pre><p>Instead of converting the entire request into one huge string, we can search the available byte sequence for newline delimiters.</p><p>Conceptually:</p><pre><code><code>PipeReader
    &#8595;
Find \n
    &#8595;
Process complete record
    &#8595;
Advance consumed position
    &#8595;
Keep incomplete record buffered
    &#8595;
Read more data</code></code></pre><p>This is exactly the kind of scenario pipelines handle well.</p><p>The parser can work with whatever data has arrived without demanding that the entire payload be available first.</p><h2>Writing with PipeWriter</h2><p>The same idea applies to responses.</p><p>ASP.NET Core exposes:</p><pre><code><code>HttpResponse.BodyWriter</code></code></pre><p>as a <code>PipeWriter</code>.</p><p>We can request writable memory:</p><pre><code><code>var writer =
    HttpContext.Response.BodyWriter;

Memory&lt;byte&gt; memory =
    writer.GetMemory(4096);</code></code></pre><p>Write data into it, tell the writer how many bytes were written:</p><pre><code><code>writer.Advance(bytesWritten);</code></code></pre><p>and eventually flush:</p><pre><code><code>await writer.FlushAsync(
    HttpContext.RequestAborted);</code></code></pre><p>Microsoft notes that <code>BodyWriter</code> provides direct access to the response body while managing memory on behalf of the caller. </p><p>This can avoid some intermediate buffering and copying in performance-sensitive code.</p><h2>Flush Does Not Mean &#8220;Flush Constantly&#8221;</h2><p>Suppose we generate a million small records.</p><p>We could do:</p><pre><code><code>Write record
Flush

Write record
Flush

Write record
Flush</code></code></pre><p>That may create a large number of tiny writes.</p><p>At the opposite extreme:</p><pre><code><code>Write everything
Write everything
Write everything
Never flush until the end</code></code></pre><p>may delay delivery and accumulate too much buffered data.</p><p>The correct strategy lies between them.</p><p>Microsoft&#8217;s ASP.NET Core documentation explains that <code>HttpResponse.BodyWriter</code> buffers data and that the developer must choose when to call <code>FlushAsync</code>, balancing buffer size, network overhead, and how quickly chunks should reach the client. </p><p>So flushing is another form of batching.</p><h2>Batching Is One of the Hidden Keys to I/O Performance</h2><p>Imagine a database import containing 100,000 records.</p><p>Approach A:</p><pre><code><code>Read one record
Write one record
Flush
Repeat 100,000 times</code></code></pre><p>Approach B:</p><pre><code><code>Read manageable batch
Process batch
Write batch
Flush when appropriate
Repeat</code></code></pre><p>The second approach can reduce:</p><pre><code><code>System calls
Network round trips
Database round trips
Flush operations
Protocol overhead</code></code></pre><p>But gigantic batches can increase memory usage and latency.</p><p>Once again, the goal is balance.</p><p>High-performance systems often operate with <strong>bounded batches</strong>.</p><h2>Buffer Size Is a Trade-Off</h2><p>Should every buffer be 4 KB?</p><p>64 KB?</p><p>1 MB?</p><p>There is no universal answer.</p><p>Larger buffers can reduce the number of I/O operations.</p><p>But they also:</p><pre><code><code>Consume more memory per operation
Increase memory pressure under concurrency
May cross LOH thresholds
Can retain unnecessary memory</code></code></pre><p>Imagine:</p><pre><code><code>1 MB buffer
&#215;
5,000 concurrent operations</code></code></pre><p>That theoretically represents:</p><pre><code><code>~5 GB</code></code></pre><p>of buffer capacity alone.</p><p>A buffer that looks harmless in isolation can become expensive at scale.</p><p>This is why buffer sizing must consider <strong>concurrency</strong>, not just single-request throughput.</p><h2>Reuse Buffers When Profiling Justifies It</h2><p>Our previous article introduced:</p><pre><code><code>ArrayPool&lt;byte&gt;</code></code></pre><p>It is highly relevant to stream processing.</p><p>Instead of repeatedly doing:</p><pre><code><code>var buffer =
    new byte[64 * 1024];</code></code></pre><p>a hot processing path can rent:</p><pre><code><code>var buffer =
    ArrayPool&lt;byte&gt;.Shared.Rent(
        64 * 1024);

try
{
    // Read and process.
}
finally
{
    ArrayPool&lt;byte&gt;.Shared.Return(buffer);
}</code></code></pre><p>This can reduce repeated allocations.</p><p>But remember the same rules:</p><pre><code><code>Return exactly once
Do not use after return
Clear sensitive data when necessary
Do not pool without evidence</code></code></pre><p>Pipelines can reduce the need to manage these buffers manually because buffer management is one of the responsibilities the abstraction was designed to simplify.</p><h2>Backpressure Exists Inside I/O Too</h2><p>Our earlier article on <strong>Designing Backpressure in ASP.NET Core</strong> focused on overload at the application level.</p><p>The same concept exists within pipelines.</p><p>Imagine:</p><pre><code><code>Producer
&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;
             &#8595;

Consumer
&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;</code></code></pre><p>The producer generates data much faster than the consumer processes it.</p><p>Without limits:</p><pre><code><code>Buffer
Buffer
Buffer
Buffer
Buffer
Buffer
...</code></code></pre><p>Memory keeps growing.</p><p>A healthy system needs a mechanism that eventually tells the producer:</p><p><strong>Slow down.</strong></p><p><code>System.IO.Pipelines</code> includes pause and resume thresholds that can apply backpressure between the writer and reader. When buffered data grows beyond configured thresholds, flushing can pause until the reader consumes enough data. </p><p>This is a beautiful connection between two parts of our series.</p><p>Backpressure is not only an API gateway concept.</p><p>It exists all the way down to how bytes move through a process.</p><h2>Slow Consumers Are a Real Production Problem</h2><p>Suppose our server can generate a 2 GB export very quickly.</p><p>But the client is connected through a slow mobile network.</p><p>If we generate the entire export into memory first:</p><pre><code><code>Fast server
    &#8595;
2 GB result in memory
    &#8595;
Slow network</code></code></pre><p>the server carries the cost of that speed mismatch.</p><p>Streaming changes the relationship:</p><pre><code><code>Generate
   &#8595;
Write
   &#8595;
Network consumes
   &#8595;
Generate more</code></code></pre><p>The output path itself can influence how quickly data is produced.</p><p>This keeps resource consumption more closely tied to actual delivery.</p><h2>Streaming Database Results</h2><p>Suppose an endpoint exports five million records.</p><p>A naive implementation:</p><pre><code><code>var rows =
    await db.Orders
        .AsNoTracking()
        .ToListAsync(cancellationToken);</code></code></pre><p>Now millions of records may exist simultaneously in memory.</p><p>For workloads where records can be processed independently, asynchronous enumeration can allow progressive processing:</p><pre><code><code>await foreach (
    var order in db.Orders
        .AsNoTracking()
        .AsAsyncEnumerable()
        .WithCancellation(cancellationToken))
{
    await WriteOrderAsync(
        order,
        cancellationToken);
}</code></code></pre><p>Now the architecture becomes:</p><pre><code><code>Database
   &#8595;
Record
   &#8595;
Serialize
   &#8595;
Response</code></code></pre><p>rather than:</p><pre><code><code>Database
   &#8595;
Millions of objects
   &#8595;
Huge in-memory collection
   &#8595;
Serialize</code></code></pre><p>The exact behavior still depends on the database provider, query, and surrounding code, so profiling remains essential.</p><p>But architecturally, streaming opens the door to bounded-memory processing.</p><h2>Pagination Is Still Valuable</h2><p>Streaming is not automatically the right answer for every large query.</p><p>For normal user-facing APIs, pagination is often better.</p><p>Instead of returning:</p><pre><code><code>500,000 products</code></code></pre><p>return:</p><pre><code><code>100 products</code></code></pre><p>with a mechanism to retrieve the next set.</p><p>Microsoft&#8217;s ASP.NET Core performance guidance recommends returning large collections across multiple smaller pages rather than retrieving and returning unnecessarily large datasets. </p><p>Streaming is powerful when the consumer genuinely needs a long sequence.</p><p>Pagination is better when the consumer only needs part of the dataset.</p><h2>File Uploads Need Limits</h2><p>Streaming a request body solves one problem.</p><p>It does not mean:</p><p><strong>Accept unlimited data.</strong></p><p>Imagine an attacker sends:</p><pre><code><code>500 TB upload</code></code></pre><p>very slowly.</p><p>Even if we never buffer the whole file, the request can still consume:</p><pre><code><code>Connections
Bandwidth
Disk
CPU
Application time</code></code></pre><p>for an unreasonable duration.</p><p>Production upload design needs:</p><pre><code><code>Request size limits
Authentication
Timeouts
Cancellation
Storage quotas
Content validation
Concurrency controls
Rate limits</code></code></pre><p>Streaming is a memory-management strategy.</p><p>It is not a complete resource-governance strategy.</p><h2>Beware of Re-Reading Request Bodies</h2><p>HTTP request bodies normally behave like forward-moving streams.</p><p>Once consumed, they are not automatically available for another component to read again.</p><p>Sometimes middleware genuinely needs to inspect a body and allow later components to read it too.</p><p>ASP.NET Core provides:</p><pre><code><code>Request.EnableBuffering();</code></code></pre><p>which enables multiple reads by buffering the request body. Microsoft&#8217;s <code>HttpContext</code> guidance demonstrates this pattern. </p><p>For example:</p><pre><code><code>context.Request.EnableBuffering();

await InspectAsync(
    context.Request.Body,
    context.RequestAborted);

context.Request.Body.Position = 0;

await next(context);</code></code></pre><p>But notice what we just did.</p><p>We deliberately introduced buffering.</p><p>That may be fine for small controlled payloads.</p><p>For very large requests, casually enabling buffering can undermine the memory and I/O benefits we are trying to achieve.</p><h2>Compression Changes the Equation</h2><p>Suppose an API returns:</p><pre><code><code>10 MB JSON</code></code></pre><p>Compression might reduce the bytes transmitted significantly, depending on the content.</p><p>That can improve network performance.</p><p>But compression consumes CPU.</p><p>So the system trades:</p><pre><code><code>CPU work</code></code></pre><p>for:</p><pre><code><code>Fewer network bytes</code></code></pre><p>Whether that is beneficial depends on:</p><pre><code><code>Payload type
Payload size
Network speed
CPU capacity
Compression level
Client support</code></code></pre><p>This illustrates an important performance principle.</p><p>There is rarely one resource called &#8220;performance.&#8221;</p><p>We trade among:</p><pre><code><code>CPU
Memory
Network
Disk
Latency
Throughput</code></code></pre><p>High-performance I/O is about making those trades deliberately.</p><h2>Avoid Sync-Over-Async</h2><p>One particularly harmful pattern is:</p><pre><code><code>var result =
    ReadDataAsync().Result;</code></code></pre><p>or:</p><pre><code><code>ReadDataAsync().Wait();</code></code></pre><p>The asynchronous operation may be waiting for I/O while the calling thread is blocked waiting for the asynchronous operation.</p><p>At scale, this can contribute to Thread Pool starvation.</p><p>Prefer:</p><pre><code><code>var result =
    await ReadDataAsync();</code></code></pre><p>and keep the call chain asynchronous.</p><p>Microsoft explicitly recommends avoiding <code>Task.Wait</code> and <code>.Result</code> in ASP.NET Core hot paths and making the complete I/O call stack asynchronous when asynchronous APIs are available. </p><h2>Do Not Use Task.Run to Hide Blocking I/O</h2><p>Suppose a library exposes only:</p><pre><code><code>ReadSynchronously();</code></code></pre><p>It may be tempting to write:</p><pre><code><code>await Task.Run(
    () =&gt; ReadSynchronously());</code></code></pre><p>That does not transform synchronous I/O into true asynchronous I/O.</p><p>A Thread Pool thread still performs the blocking call.</p><p>Microsoft specifically advises against using <code>Task.Run</code> merely to make synchronous APIs appear asynchronous in ASP.NET Core request processing. </p><p>Whenever possible, choose libraries that expose real asynchronous I/O APIs.</p><h2>.NET 10 Pushes Pipelines Deeper into ASP.NET Core</h2><p>There is an important current development worth noting.</p><p>Starting with .NET 10, several ASP.NET Core JSON request-processing paths use <code>JsonSerializer.DeserializeAsync</code> overloads based on <code>PipeReader</code> instead of <code>Stream</code>, including Minimal API parameter binding, MVC input formatting, and JSON request extensions. Microsoft says most applications benefit from the transition without needing application changes. </p><p>This tells us something important about the direction of the platform.</p><p>Pipelines are not merely an exotic API for specialized network servers.</p><p>They increasingly underpin ordinary ASP.NET Core infrastructure.</p><p>Most developers do not need to interact with that machinery directly.</p><p>But understanding it helps explain where ASP.NET Core gets much of its I/O efficiency.</p><h2>Streams or Pipelines?</h2><p>This is probably the most practical question in this article.</p><p>Use <strong>streams</strong> when:</p><pre><code><code>The operation is straightforward
The existing API expects Stream
You are copying files
You are using compression
The workload is not a measured bottleneck
The simpler abstraction is sufficient</code></code></pre><p>Consider <strong>pipelines</strong> when:</p><pre><code><code>You are building high-throughput middleware
You parse streaming protocols
You process large continuous byte sequences
Buffer management is becoming complicated
Copies and allocations appear in profiling
You need tighter control over producer/consumer flow</code></code></pre><p>Microsoft&#8217;s request and response documentation makes the same broad distinction: streams remain useful and widely supported, while pipelines offer advantages for more performance-sensitive processing. </p><p>Do not choose pipelines because they sound more advanced.</p><p>Choose them because the problem justifies them.</p><h2>Efficient I/O Is Mostly About Avoiding Work</h2><p>When developers hear &#8220;high performance,&#8221; they often imagine clever algorithms.</p><p>For I/O systems, the biggest gains are frequently less glamorous:</p><pre><code><code>Do not load the whole file.

Do not copy bytes unnecessarily.

Do not create a giant string.

Do not block a thread while waiting.

Do not fetch records nobody needs.

Do not flush after every tiny write.

Do not allocate giant buffers per request.

Do not continue work after the client disconnects.</code></code></pre><p>Each avoided operation seems small.</p><p>At scale, avoided work becomes throughput.</p><h2>Observing I/O Performance</h2><p>Optimization requires measurement.</p><p>Useful signals include:</p><pre><code><code>Request duration
Request rate
Response size
Active requests
Thread Pool queue length
CPU utilization
Allocation rate
GC activity
Network throughput
Disk throughput
Database latency
Cancellation rate
Timeout rate</code></code></pre><p>For custom pipelines, also consider:</p><pre><code><code>Buffered bytes
Producer rate
Consumer rate
Flush frequency
Batch size
Time waiting on backpressure</code></code></pre><p>Do not optimize only one metric.</p><p>A change that improves throughput but doubles memory consumption may create a different production problem.</p><h2>Load Test with Slow Clients Too</h2><p>Performance tests often use powerful load generators on fast networks.</p><p>That can hide an important class of problems.</p><p>Real clients may:</p><pre><code><code>Upload slowly
Download slowly
Disconnect halfway
Pause unexpectedly
Operate on unreliable mobile networks</code></code></pre><p>A robust I/O architecture needs to behave well when the consumer or producer is slow.</p><p>Test:</p><pre><code><code>Large payloads
Slow uploads
Slow downloads
Cancellation
Concurrent transfers
Memory-constrained containers
Sudden traffic spikes
Downstream latency</code></code></pre><p>The goal is not merely impressive benchmark throughput.</p><p>The goal is predictable production behavior.</p><h2>A Practical Architecture for Large Data Processing</h2><p>Imagine an ASP.NET Core service that receives large telemetry files, validates records, transforms them, stores selected data, and streams a report back to the caller.</p><p>A poor architecture might be:</p><pre><code><code>Upload
  &#8595;
Entire file in memory
  &#8595;
Entire file converted to string
  &#8595;
Entire file parsed
  &#8595;
List of every record
  &#8595;
Transform every record
  &#8595;
Build complete report
  &#8595;
Send response</code></code></pre><p>A more scalable design could be:</p><pre><code><code>Request stream
      &#8595;
Bounded reader
      &#8595;
Parse records incrementally
      &#8595;
Bounded processing stage
      &#8595;
Persist manageable batches
      &#8595;
Generate report progressively
      &#8595;
Response stream</code></code></pre><p>Now add:</p><pre><code><code>Cancellation
Timeouts
Backpressure
Bounded buffers
Observability</code></code></pre><p>The application no longer treats data as one enormous object.</p><p>It treats data as a <strong>flow</strong>.</p><p>That is the deeper architectural idea behind high-performance I/O.</p><h2>How This Connects to the Last Three Articles</h2><p>The last few articles form a progression.</p><p><strong>Backpressure</strong> taught us that producers cannot be allowed to overwhelm consumers indefinitely.</p><p><strong>Graceful shutdown</strong> taught us that outstanding work needs clear ownership, cancellation, and completion rules.</p><p><strong>Memory management</strong> taught us that allocations, large buffers, and retained objects have real runtime costs.</p><p>High-performance I/O brings those ideas together.</p><p>A production data pipeline should be able to:</p><pre><code><code>Process data incrementally
Bound its memory usage
Slow producers when necessary
Cancel abandoned work
Drain safely during shutdown
Avoid unnecessary allocation
Remain observable under load</code></code></pre><p>That is much more valuable than simply calling the fastest API available.</p><h2>Common High-Performance I/O Mistakes</h2><p>Several mistakes appear repeatedly.</p><p><strong>Reading entire large payloads into memory.</strong> This increases peak memory and can create LOH pressure.</p><p><strong>Using synchronous I/O inside request handling.</strong> Waiting threads reduce scalability.</p><p><strong>Using </strong><code>.Result</code><strong> or </strong><code>.Wait()</code><strong>.</strong> Sync-over-async can contribute to Thread Pool starvation.</p><p><strong>Wrapping blocking work in </strong><code>Task.Run</code><strong>.</strong> It moves the blocking operation rather than eliminating it.</p><p><strong>Using enormous per-request buffers.</strong> Buffer cost multiplies with concurrency.</p><p><strong>Flushing after every tiny write.</strong> Too many small operations can reduce throughput.</p><p><strong>Never flushing.</strong> Excessive buffering can increase latency and memory usage.</p><p><strong>Ignoring cancellation.</strong> Work continues after the caller no longer needs the result.</p><p><strong>Using pipelines without understanding </strong><code>AdvanceTo</code><strong>.</strong> Incorrect buffer ownership can create memory growth or hangs.</p><p><strong>Streaming without resource limits.</strong> Streaming protects memory, not every other finite resource.</p><p><strong>Using pipelines everywhere.</strong> Complexity is not a performance feature.</p><h2>A Better Optimization Order</h2><p>If an ASP.NET Core service has I/O performance problems, optimize in this order:</p><pre><code><code>1. Measure the bottleneck.

2. Remove synchronous blocking.

3. Stop retrieving unnecessary data.

4. Avoid buffering large payloads.

5. Stream where the workload allows it.

6. Remove unnecessary copies.

7. Bound buffers and concurrency.

8. Propagate cancellation.

9. Batch appropriately.

10. Profile again.

11. Introduce pipelines where measurements
    justify the added complexity.

12. Tune buffer sizes only with evidence.</code></code></pre><p>Notice where <code>PipeReader</code> appears.</p><p>Not first.</p><p>Architecture usually produces larger gains than micro-optimization.</p><h2>What Good I/O Architecture Looks Like</h2><p>A healthy ASP.NET Core I/O path has predictable resource usage.</p><p>It does not matter whether the application receives:</p><pre><code><code>10 MB</code></code></pre><p>or:</p><pre><code><code>10 GB</code></code></pre><p>if the workload can be processed incrementally and the amount held in memory remains bounded.</p><p>The ideal shape is:</p><pre><code><code>Source
   &#8595;
Small bounded working set
   &#8595;
Processing
   &#8595;
Small bounded working set
   &#8595;
Destination</code></code></pre><p>rather than:</p><pre><code><code>Source
   &#8595;
EVERYTHING IN MEMORY
   &#8595;
Processing
   &#8595;
ANOTHER HUGE OBJECT
   &#8595;
Destination</code></code></pre><p>That difference often matters more than any low-level optimization.</p><h2>Coming Next</h2><p>In the next article, we&#8217;ll explore <strong>Native AOT in ASP.NET Core: Faster Startup, Smaller Containers, and the Trade-Offs</strong>.</p><p>So far, we have optimized what happens <strong>while an ASP.NET Core application is running</strong>.</p><p>Native AOT changes a different part of the performance story.</p><p>What if the application could start faster, use less memory, and ship without relying on JIT compilation at runtime?</p><p>That sounds ideal.</p><p>But Native AOT also changes assumptions around reflection, dynamic code generation, libraries, serialization, and deployment.</p><p>We&#8217;ll examine where Native AOT delivers real advantages, where compatibility becomes harder, and how to decide whether those trade-offs make sense for a production <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core application</a>.</p><h2>Closing Thoughts</h2><p>High-performance I/O is not really about streams or pipelines.</p><p>Those are tools.</p><p>The deeper problem is controlling how data moves through the application.</p><p>A scalable ASP.NET Core system should not need to load a giant request into memory just to process it. It should not block a Thread Pool thread while waiting for a network response. It should not repeatedly copy the same bytes between unnecessary intermediate buffers. And it should not continue processing expensive data after the caller has disappeared.</p><p>Streams give us a simple way to process data incrementally.</p><p>Asynchronous I/O prevents waiting operations from unnecessarily occupying threads.</p><p><code>PipeReader</code> and <code>PipeWriter</code> provide more advanced buffer management for performance-sensitive workloads.</p><p>Bounded buffers prevent memory from growing without control.</p><p>Backpressure coordinates fast producers with slower consumers.</p><p>Cancellation stops work that no longer has value.</p><p>Together, these patterns change the way we think about large data.</p><p>Instead of asking:</p><pre><code><code>How do we load this efficiently?</code></code></pre><p>ask:</p><pre><code><code>Do we need to load it at all?</code></code></pre><p>Instead of asking:</p><pre><code><code>How large should our buffer be?</code></code></pre><p>ask:</p><pre><code><code>How many of these buffers can exist concurrently?</code></code></pre><p>Instead of asking:</p><pre><code><code>Should we replace Stream with PipeReader?</code></code></pre><p>ask:</p><pre><code><code>Where does profiling show that
buffer management or copying
is actually limiting us?</code></code></pre><p>That is the difference between performance optimization and performance theater.</p><p>The fastest byte is often the byte you never copy.</p><p>The cheapest buffer is often the buffer you never allocate.</p><p>And the most scalable large-data architecture is often the one that <strong>lets data flow through the application instead of forcing the application to hold all of it at once.</strong></p><h2>Subscribe Now</h2><p>Enjoying the series? <a href="https://www.asptoday.com/">Subscribe to </a><strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, advanced architecture deep dives, and production-ready .NET performance strategies. Join our Substack Chat to discuss streaming, pipelines, memory efficiency, performance tuning, and the engineering decisions behind fast, scalable ASP.NET Core systems. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Memory Management in ASP.NET Core: GC, Allocations, LOH, and Object Pooling ]]></title><description><![CDATA[Master GC, allocations, LOH, and object pooling for faster ASP.NET Core apps. #ASPToday #aspnetcore #dotnet #performance #memory]]></description><link>https://www.asptoday.com/p/memory-management-in-aspnet-core</link><guid isPermaLink="false">https://www.asptoday.com/p/memory-management-in-aspnet-core</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 15 Sep 2026 15:02:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!56aB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>An <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> application can have plenty of available CPU, a <a href="https://www.asptoday.com/p/aspnet-core-health-checks-keeping">healthy database</a>, and fast network connections yet still become slow under load because of memory. Every request creates objects, strings, arrays, JSON buffers, collections, and temporary data. Most disappear quickly, which is exactly what .NET&#8217;s garbage collector is designed to handle. Problems begin when an application allocates too much, creates large temporary objects, keeps data alive longer than necessary, or uses pooling without understanding the trade-offs. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!56aB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!56aB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!56aB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!56aB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!56aB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!56aB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2809043,&quot;alt&quot;:&quot;Giant recycling facility sorts and reuses materials, symbolizing GC, allocations, LOH, and pooling. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/215666406?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Giant recycling facility sorts and reuses materials, symbolizing GC, allocations, LOH, and pooling. " title="Giant recycling facility sorts and reuses materials, symbolizing GC, allocations, LOH, and pooling. " srcset="https://substackcdn.com/image/fetch/$s_!56aB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!56aB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!56aB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!56aB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07b71f5-d991-4cc3-aaff-8f7a792e2db1_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we&#8217;ll look inside .NET memory management, understand generations 0, 1, and 2, explore the Large Object Heap (LOH), identify allocation-heavy ASP.NET Core patterns, and use <code>ArrayPool&lt;T&gt;</code> and object pooling to reduce pressure where it matters.</p><h2>Memory Problems Rarely Begin as &#8220;Out of Memory&#8221;</h2><p>Imagine an ASP.NET Core API processing thousands of requests every second.</p><p>Each request allocates:</p><pre><code><code>Request objects
Strings
JSON models
Collections
Temporary arrays
Database results
Response buffers</code></code></pre><p>None seems particularly dangerous. </p><p>Suppose each request creates 50 KB of short-lived managed allocations.</p><p>At 5,000 requests per second:</p><pre><code><code>50 KB &#215; 5,000
&#8776; 250 MB allocated every second</code></code></pre><p>That does <strong>not</strong> necessarily mean the process permanently grows by 250 MB every second.</p><p>Most of those objects quickly become unreachable and can be reclaimed.</p><p>But somebody still has to clean them up.</p><p>That somebody is the garbage collector.</p><p>This is the first important distinction:</p><p><strong>Memory usage and allocation rate are not the same thing.</strong></p><p>An application might stabilize at a reasonable heap size while allocating enormous quantities of temporary memory.</p><p>The symptom may therefore be latency and CPU consumption rather than an obvious memory leak.</p><h2>What the .NET Garbage Collector Actually Does</h2><p>.NET uses automatic memory management.</p><p>When managed objects are created, the runtime allocates space for them on the managed heap. When objects are no longer reachable from application roots, the garbage collector can reclaim their memory. Microsoft describes the GC as the CLR&#8217;s automatic memory manager, responsible for allocation and reclamation of managed memory. </p><p>Conceptually:</p><pre><code><code>Allocate object
      &#8595;
Use object
      &#8595;
References disappear
      &#8595;
Object becomes unreachable
      &#8595;
GC eventually reclaims memory</code></code></pre><p>That means we usually do not write:</p><pre><code><code>free(customer);
free(order);
free(response);</code></code></pre><p>as we would with manual memory-management models.</p><p>This dramatically reduces an entire category of bugs.</p><p>But automatic does not mean free.</p><p>The garbage collector still consumes resources to determine which objects remain alive and reclaim memory from those that do not.</p><h2>Allocation Is Designed to Be Fast</h2><p>The managed heap is optimized for quick allocation.</p><p>At a simplified level, the runtime can often allocate a new object by advancing a pointer in available heap space. Microsoft notes that this makes managed-heap allocation extremely efficient. </p><p>Imagine:</p><pre><code><code>| object | object | object | free space........ |
                           &#8593;
                    allocation pointer</code></code></pre><p>Create another object:</p><pre><code><code>| object | object | object | NEW | free space... |
                                 &#8593;
                          allocation pointer</code></code></pre><p>The problem is not usually:</p><p><strong>Creating one object is slow.</strong></p><p>The problem is:</p><p><strong>Creating millions of unnecessary objects forces the memory-management system to work harder.</strong></p><p>This distinction matters when optimizing ASP.NET Core.</p><h2>Why the GC Uses Generations</h2><p>Most objects in server applications have short lives.</p><p>A request arrives.</p><p>Objects are created.</p><p>The response is returned.</p><p>Those request-specific objects become unreachable.</p><p>.NET exploits this pattern through a <strong><a href="https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/?">generational garbage collector</a></strong>.</p><p>The managed heap has three logical generations:</p><pre><code><code>Generation 0
Generation 1
Generation 2</code></code></pre><p>Generation 0 contains the youngest objects.</p><p>Generation 1 acts as an intermediate stage.</p><p>Generation 2 contains longer-lived objects.</p><p><a href="https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals?">Microsoft</a> explains that this arrangement is based partly on the observation that newer objects generally have shorter lifetimes and that collecting only part of the heap is cheaper than repeatedly examining everything. </p><h2>Generation 0: Where Most Temporary Objects Begin</h2><p>Imagine an API request:</p><pre><code><code>app.MapGet("/products", async (ProductService service) =&gt;
{
    var products = await service.GetProductsAsync();

    var response = new ProductResponse
    {
        Items = products
    };

    return response;
});</code></code></pre><p>Several objects may exist only for that request.</p><p>Those short-lived allocations are exactly what generation 0 is designed to handle.</p><p>Conceptually:</p><pre><code><code>Request arrives
      &#8595;
Temporary objects created
      &#8595;
Request completes
      &#8595;
Objects become unreachable
      &#8595;
Gen 0 collection
      &#8595;
Memory reclaimed</code></code></pre><p>Generation 0 collections happen relatively frequently and are intended to be inexpensive compared with collecting older generations.</p><p>The best outcome for many temporary request objects is simple:</p><p><strong>Die young.</strong></p><h2>Generation 1: The Waiting Room</h2><p>What if an object survives a generation 0 collection?</p><p>It may be promoted to generation 1.</p><p>Generation 1 acts as a buffer between very short-lived objects and genuinely long-lived objects. If those objects survive further collection, they can eventually move to generation 2. </p><p>Think of it as:</p><pre><code><code>Gen 0
"Probably temporary"

   &#8595; survives

Gen 1
"Maybe longer-lived"

   &#8595; survives

Gen 2
"Apparently this object is sticking around"</code></code></pre><p>Promotion itself is not automatically bad.</p><p>Long-lived objects are normal.</p><p>Configuration objects, singleton state, application caches, and other data may legitimately live for a long time.</p><p>Problems appear when objects survive longer <strong>accidentally</strong>.</p><h2>Generation 2: Expensive Territory</h2><p>Generation 2 contains long-lived managed objects.</p><p>A generation 2 collection is effectively a full managed-heap collection because younger generations are collected as well. </p><p>This matters for server applications.</p><p>Frequent Gen 0 collections may be perfectly normal.</p><p>Frequent Gen 2 collections deserve closer investigation.</p><p>They can indicate:</p><pre><code><code>High allocation pressure
Too many surviving objects
Large object allocations
Oversized caches
Long-lived references
Memory pressure</code></code></pre><p>The goal is not:</p><p><strong>Never run Gen 2 GC.</strong></p><p>That would be unrealistic.</p><p>The goal is to avoid application behavior that causes unnecessarily frequent expensive collections.</p><h2>Garbage Collection Can Affect Latency</h2><p>During parts of garbage collection, managed application threads may need to be suspended. Microsoft documents thread suspension as part of GC processing. </p><p>For an interactive web application, this makes memory a latency issue.</p><p>Imagine typical response latency:</p><pre><code><code>p50:  45 ms
p95:  90 ms
p99: 130 ms</code></code></pre><p>Then under high allocation pressure:</p><pre><code><code>p50:   48 ms
p95:  160 ms
p99:  900 ms</code></code></pre><p>Average performance may still appear reasonable.</p><p>Users in the tail experience pauses.</p><p>That is why memory optimization is often about <strong>predictability</strong>, not merely reducing the number displayed in Task Manager.</p><h2>Allocation Rate Is One of the Most Useful Signals</h2><p>Suppose two services both use 1 GB of managed memory.</p><p>Service A allocates:</p><pre><code><code>20 MB/sec</code></code></pre><p>Service B allocates:</p><pre><code><code>1.5 GB/sec</code></code></pre><p>Those applications have radically different memory behavior.</p><p>Heap size alone cannot explain it.</p><p>For performance investigations, useful signals include:</p><pre><code><code>Allocation rate
Managed heap size
Gen 0 collections
Gen 1 collections
Gen 2 collections
LOH size
GC pause time
Time spent in GC
Process working set</code></code></pre><p>Tools such as <code>dotnet-counters</code>, <code>dotnet-trace</code>, <code>dotnet-gcdump</code>, Visual Studio diagnostics, and profiling products can help identify where memory is being allocated and retained.</p><p>Measure first.</p><p>Do not begin by randomly converting code to pools and spans.</p><h2>Temporary Allocations Hide in Innocent Code</h2><p>Consider:</p><pre><code><code>var message =
    "Customer " + customer.Id +
    " placed order " + order.Id +
    " at " + DateTime.UtcNow;</code></code></pre><p>One execution is irrelevant.</p><p>A hot path executing millions of times may be different.</p><p>The same applies to:</p><pre><code><code>LINQ projections
Repeated ToList()
String transformations
Serialization
Large arrays
Repeated buffer creation
Regex work
Collection copying
Intermediate DTOs</code></code></pre><p>None of these APIs is inherently bad.</p><p>Context determines whether allocation matters.</p><h2>Hot Paths Deserve Different Rules</h2><p>Suppose this endpoint receives 20 requests per day:</p><pre><code><code>POST /admin/rebuild-index</code></code></pre><p>Shaving three small allocations from it probably has no practical value.</p><p>Now consider:</p><pre><code><code>GET /telemetry</code></code></pre><p>called 30,000 times per second.</p><p>A small per-request improvement can become significant.</p><p>This gives us a useful optimization rule:</p><p><strong>Allocation cost = allocation size &#215; execution frequency.</strong></p><p>Do not optimize code because an allocation exists.</p><p>Optimize because measurements show the allocation occurs often enough to matter.</p><h2>The Large Object Heap Changes the Picture</h2><p>.NET treats large managed objects differently.</p><p>Objects at or above the LOH threshold, traditionally 85,000 bytes, are placed on the <strong><a href="https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/large-object-heap?">Large Object Heap</a></strong>. Microsoft documents that large objects are collected with generation 2 and are normally treated differently because moving large blocks of memory is expensive. </p><p>Typical examples include large arrays:</p><pre><code><code>var buffer = new byte[100_000];</code></code></pre><p>or sufficiently large arrays of other types.</p><p>Conceptually:</p><pre><code><code>Small allocations
      &#8595;
Small Object Heap

Large allocation
      &#8595;
Large Object Heap</code></code></pre><p>The threshold is about object size in bytes, not whether the code looks complicated.</p><h2>Why Large Objects Cost More</h2><p>Imagine repeatedly doing:</p><pre><code><code>var buffer = new byte[1_000_000];</code></code></pre><p>Process something.</p><p>Discard it.</p><p>Then repeat.</p><p>The object is temporary, but each allocation is roughly one megabyte.</p><p>At 1,000 operations per second:</p><pre><code><code>~1 GB of large allocations every second</code></code></pre><p>This creates very different pressure from allocating tiny request objects.</p><p>Microsoft&#8217;s ASP.NET Core performance guidance specifically recommends minimizing large-object allocations in hot paths and notes that frequent large allocations can lead to expensive generation 2 collections and inconsistent performance. </p><h2>The LOH Is Usually Not Compacted Like the Small Heap</h2><p>Normal GC compaction can move surviving objects together to remove gaps.</p><p>Large objects are expensive to copy.</p><p>For that reason, the LOH is ordinarily swept rather than routinely compacted. Freed regions can later be reused for other allocations. .NET does provide mechanisms for requesting LOH compaction in specific scenarios, but routine manual compaction is not a substitute for fixing an unhealthy allocation pattern. </p><p>Imagine:</p><pre><code><code>AAAA BBBBBBB CCCC DDDDDD</code></code></pre><p>Some large objects die:</p><pre><code><code>AAAA ........ CCCC ......</code></code></pre><p>Those gaps can be reused.</p><p>But repeated differently sized allocations can make memory behavior less efficient.</p><p>This is one reason <strong>LOH fragmentation</strong> may become relevant in allocation-heavy applications.</p><h2>A Common ASP.NET Core Example: Large Buffers</h2><p>Suppose an application receives large files.</p><p>A naive implementation might repeatedly create large arrays:</p><pre><code><code>var buffer = new byte[1024 * 1024];

while (true)
{
    var read = await stream.ReadAsync(
        buffer,
        cancellationToken);

    if (read == 0)
        break;

    await ProcessAsync(
        buffer.AsMemory(0, read),
        cancellationToken);
}</code></code></pre><p>The buffer itself is one megabyte.</p><p>If this allocation happens once for an occasional operation, no crisis exists.</p><p>If thousands of concurrent requests repeatedly create similar buffers, allocation pressure becomes significant.</p><p>This is where pooling becomes interesting.</p><h2>ArrayPool&lt;T&gt;: Rent Instead of Repeatedly Allocate</h2><p>.NET provides <code>ArrayPool&lt;T&gt;</code> for scenarios where arrays can be reused.</p><p>Instead of:</p><pre><code><code>var buffer = new byte[1024 * 1024];</code></code></pre><p>we can rent:</p><pre><code><code>using System.Buffers;

var pool = ArrayPool&lt;byte&gt;.Shared;
var buffer = pool.Rent(1024 * 1024);

try
{
    // Use the buffer.
}
finally
{
    pool.Return(buffer);
}</code></code></pre><p>The important change is conceptual.</p><p>Without pooling:</p><pre><code><code>Allocate
Use
Discard

Allocate
Use
Discard

Allocate
Use
Discard</code></code></pre><p>With pooling:</p><pre><code><code>Rent
Use
Return
     &#8595;
Rent again</code></code></pre><p>Microsoft&#8217;s ASP.NET Core performance guidance specifically recommends <code>ArrayPool&lt;T&gt;</code> as one option for reducing repeated large-<a href="https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1?view=net-10.0&amp;">array</a> allocations. </p><h2>Pooling Does Not Mean &#8220;Free Memory&#8221;</h2><p>This is a crucial distinction.</p><p>When an array is returned to a pool, we are not necessarily returning its memory to the operating system.</p><p>We are making that array available for reuse.</p><p>That means pooling trades:</p><pre><code><code>Fewer repeated allocations</code></code></pre><p>for:</p><pre><code><code>Reusable memory retained by the pool</code></code></pre><p>That can be a very good trade in a hot path.</p><p>But pooling everything can increase retained memory and complexity without improving meaningful performance.</p><h2>Always Return What You Rent</h2><p>This is wrong:</p><pre><code><code>var buffer =
    ArrayPool&lt;byte&gt;.Shared.Rent(1024 * 1024);

await ProcessAsync(buffer);

// Forgot Return()</code></code></pre><p>The pool loses the opportunity to reuse that buffer efficiently.</p><p>Use <code>try/finally</code>:</p><pre><code><code>var buffer =
    ArrayPool&lt;byte&gt;.Shared.Rent(1024 * 1024);

try
{
    await ProcessAsync(buffer);
}
finally
{
    ArrayPool&lt;byte&gt;.Shared.Return(buffer);
}</code></code></pre><p>The ownership rule should be obvious:</p><p><strong>The code that rents the buffer must ensure it is returned exactly once after nobody needs it.</strong></p><h2>Never Use a Buffer After Returning It</h2><p>Consider:</p><pre><code><code>ArrayPool&lt;byte&gt;.Shared.Return(buffer);

await SendLaterAsync(buffer);</code></code></pre><p>That is dangerous.</p><p>After <code>Return</code>, the array no longer belongs exclusively to you.</p><p>Another operation may rent the same array and modify it.</p><p>This can create bugs that appear only under concurrency.</p><p>The safe mental model is:</p><pre><code><code>Rent
  &#8595;
You own temporary access
  &#8595;
Finish all usage
  &#8595;
Return
  &#8595;
Do not touch again</code></code></pre><p>Pooling makes lifetime discipline more important.</p><h2>Sensitive Data Requires Extra Thought</h2><p>Suppose a pooled array temporarily contains:</p><pre><code><code>Authentication tokens
Personal information
Payment-related data
Secrets</code></code></pre><p>Returning the array does not inherently mean every byte has been erased.</p><p><code>ArrayPool&lt;T&gt;.Return</code> supports clearing an array when appropriate:</p><pre><code><code>ArrayPool&lt;byte&gt;.Shared.Return(
    buffer,
    clearArray: true);</code></code></pre><p>Clearing has a cost.</p><p>Whether it is necessary depends on the data, security boundary, and pool usage.</p><p>Performance optimization should never quietly create a data-exposure problem.</p><h2>ObjectPool&lt;T&gt; Extends the Same Idea</h2><p>Arrays are not the only reusable objects.</p><p>ASP.NET Core provides object-pooling infrastructure for objects that are expensive to create and can safely be reset and reused.</p><p>Conceptually:</p><pre><code><code>Create expensive object
Use
Discard
Create expensive object
Use
Discard</code></code></pre><p>becomes:</p><pre><code><code>Get from pool
Use
Reset
Return

Get from pool
Use
Reset
Return</code></code></pre><p>A simplified example might involve a reusable <code>StringBuilder</code>:</p><pre><code><code>public sealed class StringBuilderPolicy
    : PooledObjectPolicy&lt;StringBuilder&gt;
{
    public override StringBuilder Create()
        =&gt; new(capacity: 1024);

    public override bool Return(
        StringBuilder builder)
    {
        if (builder.Capacity &gt; 64 * 1024)
            return false;

        builder.Clear();
        return true;
    }
}</code></code></pre><p>Registration could look like:</p><pre><code><code>builder.Services.AddSingleton&lt;ObjectPool&lt;StringBuilder&gt;&gt;(
    serviceProvider =&gt;
    {
        var provider =
            new DefaultObjectPoolProvider();

        return provider.Create(
            new StringBuilderPolicy());
    });</code></code></pre><p>Then:</p><pre><code><code>var builder = pool.Get();

try
{
    builder.Append("Order ");
    builder.Append(order.Id);

    return builder.ToString();
}
finally
{
    pool.Return(builder);
}</code></code></pre><p>The policy decides whether an object is healthy enough to reuse.</p><h2>Why Reject an Oversized Pooled Object?</h2><p>Look at this line:</p><pre><code><code>if (builder.Capacity &gt; 64 * 1024)
    return false;</code></code></pre><p>Imagine one unusual request forces the builder to grow enormously.</p><p>If we blindly return it to the pool, the application may retain that oversized buffer for future tiny requests.</p><p>Pooling can therefore accidentally turn a temporary memory spike into longer-lived memory.</p><p>A good pool policy asks:</p><p><strong>Is this object worth retaining?</strong></p><p>Sometimes the correct answer is no.</p><h2>Do Not Pool Cheap Objects</h2><p>Suppose we have:</p><pre><code><code>public sealed class Coordinate
{
    public int X { get; set; }
    public int Y { get; set; }
}</code></code></pre><p>Creating one is cheap.</p><p>Adding pooling introduces:</p><pre><code><code>Pool management
Reset logic
Ownership rules
Concurrency considerations
More complicated code
Potential stale state</code></code></pre><p>Pooling is not automatically an optimization.</p><p>It is most useful when objects are:</p><pre><code><code>Expensive to allocate or initialize
Created very frequently
Reusable safely
Easy to reset
Measured as a meaningful allocation cost</code></code></pre><p>The last condition is the most important.</p><h2>Reuse Can Create State Bugs</h2><p>Suppose:</p><pre><code><code>public sealed class WorkBuffer
{
    public string? CustomerId { get; set; }
    public List&lt;string&gt; Items { get; } = new();
}</code></code></pre><p>We return it to a pool without resetting it.</p><p>The next request rents it.</p><p>Now:</p><pre><code><code>Previous CustomerId remains
Previous Items remain</code></code></pre><p>That is both a correctness problem and potentially a privacy problem.</p><p>Every pooled type needs a clear reset contract.</p><p>If resetting it correctly is complicated, pooling may not be worth the risk.</p><h2>IDisposable Solves a Different Problem</h2><p>Memory management discussions often confuse GC with <code>IDisposable</code>.</p><p>Suppose:</p><pre><code><code>using var stream = File.OpenRead(path);</code></code></pre><p>Why dispose the stream if .NET has garbage collection?</p><p>Because the GC manages <strong>managed memory</strong>.</p><p>Some objects wrap resources such as:</p><pre><code><code>File handles
Sockets
Native memory
Operating-system handles</code></code></pre><p>Those resources often need deterministic release.</p><p>Microsoft recommends explicit cleanup for unmanaged resources, commonly through <code>Dispose</code>, even though the managed wrapper itself remains subject to garbage collection. </p><p>So:</p><pre><code><code>GC
&#8594; managed memory lifetime

Dispose
&#8594; deterministic resource cleanup</code></code></pre><p>They solve related but different problems.</p><h2>A Managed Memory Leak Is Still Possible</h2><p>Garbage collection does not mean memory leaks are impossible.</p><p>The GC reclaims objects that are <strong>unreachable</strong>.</p><p>If your application accidentally keeps references to objects, the GC correctly assumes they are still needed.</p><p>For example:</p><pre><code><code>private static readonly List&lt;byte[]&gt; _history = new();

public void Store(byte[] data)
{
    _history.Add(data);
}</code></code></pre><p>If nothing removes entries:</p><pre><code><code>Request
 &#8595;
Array added
 &#8595;
Static list keeps reference
 &#8595;
Object remains reachable
 &#8595;
GC cannot reclaim it</code></code></pre><p>That is a managed memory leak.</p><p>The GC is doing exactly what it should.</p><p>The application is telling it the objects remain alive.</p><h2>Common Sources of Retention</h2><p>Look carefully at:</p><pre><code><code>Static collections
Unbounded caches
Event subscriptions
Long-lived delegates
Queues
Background work
Singletons holding request data
Large object graphs
Improperly managed timers</code></code></pre><p>Again, the important question is not:</p><p><strong>Who allocated this object?</strong></p><p>It is often:</p><p><strong>Who is still referencing it?</strong></p><p>Memory profilers are particularly useful here because they can show paths from GC roots to objects that should have disappeared.</p><h2>Caches Are Deliberate Memory Retention</h2><p>Caching improves performance by keeping data available.</p><p>That means a cache is intentionally preventing objects from being collected.</p><p>This can be useful:</p><pre><code><code>Database query
     &#8595;
Expensive result
     &#8595;
Cache
     &#8595;
Reuse</code></code></pre><p>But an unbounded cache can become a memory problem.</p><p>ASP.NET Core&#8217;s <code>IMemoryCache</code> supports mechanisms for setting entry sizes and configuring a size limit when the application needs explicit cache-size accounting. Microsoft also warns that applications should not depend on cached data for correctness. </p><p>A cache needs a policy.</p><p>Ask:</p><pre><code><code>How much can it hold?

When do entries expire?

What happens under memory pressure?

Are entries bounded by count or size?

Can unusually large entries enter the cache?</code></code></pre><p>&#8220;It&#8217;s cached&#8221; is not a memory-management strategy.</p><h2>Streaming Can Beat Buffering</h2><p>Suppose an endpoint generates a 200 MB export.</p><p>One approach:</p><pre><code><code>Load everything
      &#8595;
Build giant in-memory representation
      &#8595;
Serialize everything
      &#8595;
Return response</code></code></pre><p>Memory consumption may scale with the entire response.</p><p>A streaming design can instead process smaller pieces:</p><pre><code><code>Read chunk
   &#8595;
Write chunk
   &#8595;
Release/reuse buffer
   &#8595;
Read next chunk</code></code></pre><p>Peak memory can remain far lower.</p><p>This is one reason our next article on <strong>High-Performance I/O in ASP.NET Core</strong> naturally follows this one.</p><p>Memory and I/O design are deeply connected.</p><h2>Avoid Unnecessary Copies</h2><p>Consider a large payload passing through several layers:</p><pre><code><code>Network buffer
     &#8595; copy
byte[]
     &#8595; copy
MemoryStream
     &#8595; copy
serializer buffer
     &#8595; copy
response</code></code></pre><p>Every copy consumes memory bandwidth and may create additional allocations.</p><p>Modern .NET APIs increasingly use abstractions such as:</p><pre><code><code>Span&lt;T&gt;
Memory&lt;T&gt;
ReadOnlySpan&lt;T&gt;
ReadOnlyMemory&lt;T&gt;</code></code></pre><p>to work with existing memory regions without always creating new arrays.</p><p>These tools are powerful, but they should solve a measured problem.</p><p>Turning ordinary application code into complex span-based code without evidence rarely improves maintainability.</p><h2>Avoid Materializing Data Too Early</h2><p>Suppose:</p><pre><code><code>var records = await query.ToListAsync();</code></code></pre><p>loads 500,000 records.</p><p>If we only need to process them sequentially, holding the entire result set may be unnecessary.</p><p>A streaming or batched approach may provide:</p><pre><code><code>Lower peak memory
Shorter object lifetimes
Less GC pressure
More predictable behavior</code></code></pre><p>The same principle applies to:</p><pre><code><code>File processing
Database exports
JSON processing
Message batches
Analytics pipelines</code></code></pre><p>Ask whether the entire dataset genuinely needs to exist in memory at once.</p><h2>String Work Can Be Surprisingly Expensive</h2><p>Web applications process strings constantly:</p><pre><code><code>URLs
Headers
JSON
Logs
Identifiers
HTML
Database values</code></code></pre><p>Repeated transformations can generate temporary objects.</p><p>For example:</p><pre><code><code>var normalized =
    input.Trim()
         .ToLowerInvariant()
         .Replace(" ", "-");</code></code></pre><p>That is perfectly acceptable in most ordinary code.</p><p>But if profiling identifies it as a dominant allocation source in a very hot path, it may deserve attention.</p><p>Do not prematurely optimize every string operation.</p><p>Use profiling to find the few that matter.</p><h2>Logging Can Create Allocation Pressure</h2><p>Logging is essential, but high-volume logging deserves design attention.</p><p>Imagine:</p><pre><code><code>_logger.LogInformation(
    $"Processing order {order.Id} for {customer.Name}");</code></code></pre><p>String interpolation may perform work before the logger decides whether the message will actually be emitted.</p><p>Prefer structured logging:</p><pre><code><code>_logger.LogInformation(
    "Processing order {OrderId} for {CustomerName}",
    order.Id,
    customer.Name);</code></code></pre><p>For extremely hot logging paths, .NET logging also supports high-performance patterns such as source-generated logging.</p><p>The broader lesson is familiar:</p><p><strong>Frequently executed infrastructure code matters because tiny costs multiply.</strong></p><h2>Do Not Call GC.Collect() as a Routine Fix</h2><p>A developer sees memory usage increase and writes:</p><pre><code><code>GC.Collect();</code></code></pre><p>The graph drops.</p><p>Problem solved?</p><p>Usually not.</p><p>Microsoft&#8217;s GC documentation says explicit <code>GC.Collect</code> calls are unnecessary in almost all normal cases because the collector determines when collections should occur. </p><p>Forced collection can:</p><pre><code><code>Trigger expensive work prematurely
Promote surviving objects
Disturb the collector's adaptive behavior
Create latency spikes
Hide the actual retention problem</code></code></pre><p>There are specialized cases where manual GC control is justified.</p><p>A normal ASP.NET Core request path is rarely one of them.</p><h2>High Memory Usage Is Not Automatically a Leak</h2><p>Suppose process memory rises from:</p><pre><code><code>300 MB</code></code></pre><p>to:</p><pre><code><code>1.2 GB</code></code></pre><p>and stays there.</p><p>That alone does not prove a leak.</p><p>The runtime may retain memory for efficient future allocations.</p><p>Pools may retain reusable buffers.</p><p>Caches may legitimately hold data.</p><p>The operating system and GC may manage committed and reserved memory differently from what a simple process-memory graph suggests.</p><p>A stronger leak pattern looks like:</p><pre><code><code>Load
 &#8595;
Memory rises
 &#8595;
Load falls
 &#8595;
Live managed data never stabilizes
 &#8595;
Repeat
 &#8595;
Baseline continues rising</code></code></pre><p>You need heap and retention evidence, not merely a frightening graph.</p><h2>Containers Make Memory Limits More Important</h2><p>ASP.NET Core applications increasingly run inside containers.</p><p>A container may have a strict memory limit even if the physical machine has far more memory.</p><p>The .NET GC has runtime settings for memory constraints, including hard heap limits and percentage-based limits. Current .NET documentation also describes LOH behavior in memory-constrained environments. </p><p>The practical lesson is:</p><p><strong>Test the application inside realistic memory limits.</strong></p><p>A service that behaves beautifully on a developer workstation with 64 GB of RAM may behave very differently inside a tightly constrained production container.</p><h2>Server GC and Workstation GC</h2><p>.NET supports different GC modes designed for different workload characteristics.</p><p>ASP.NET Core server workloads commonly operate with server-oriented GC behavior, which is designed for throughput and multiprocessor server scenarios.</p><p>But configuration is not something to change casually.</p><p>GC mode affects:</p><pre><code><code>Heap organization
Concurrency
Throughput
Memory consumption
Pause behavior</code></code></pre><p>Before tuning runtime GC configuration, first fix obvious application-level problems:</p><pre><code><code>Unbounded retention
Huge temporary buffers
Excessive allocation rates
Poor cache limits
Needless materialization
Missing pooling in proven hot paths</code></code></pre><p>Changing GC switches cannot compensate for fundamentally wasteful code.</p><h2>Measure Before and After Pooling</h2><p>Suppose profiling shows:</p><pre><code><code>Allocation rate: 900 MB/sec</code></code></pre><p>and 500 MB/sec comes from temporary 256 KB buffers.</p><p>That is an excellent pooling candidate.</p><p>After introducing <code>ArrayPool&lt;byte&gt;</code>:</p><pre><code><code>Allocation rate: 420 MB/sec</code></code></pre><p>Now we have evidence.</p><p>Compare that with:</p><pre><code><code>Allocation rate: 25 MB/sec</code></code></pre><p>and a developer spends two days pooling tiny DTOs to reduce it to:</p><pre><code><code>24.7 MB/sec</code></code></pre><p>The optimization increased complexity for almost no practical gain.</p><p>Performance engineering needs economics.</p><h2>A Practical Memory-Safe Processing Pipeline</h2><p>Imagine an ASP.NET Core endpoint that processes large uploaded datasets.</p><p>A naive design:</p><pre><code><code>Upload
  &#8595;
Read entire file into byte[]
  &#8595;
Convert entire file to string
  &#8595;
Parse complete document
  &#8595;
Build giant List&lt;Record&gt;
  &#8595;
Transform everything
  &#8595;
Serialize complete result
  &#8595;
Return</code></code></pre><p>Peak memory can become several times larger than the original file.</p><p>A more disciplined design:</p><pre><code><code>Upload stream
     &#8595;
Rent bounded buffer
     &#8595;
Read chunk
     &#8595;
Parse incrementally
     &#8595;
Process records
     &#8595;
Write result incrementally
     &#8595;
Reuse buffer
     &#8595;
Return buffer</code></code></pre><p>Now memory usage is related more closely to <strong>working-set size</strong> than total input size.</p><p>That is a major architectural improvement.</p><h2>Memory Management Connects to Backpressure</h2><p>Two articles ago, we explored <strong>Designing Backpressure in ASP.NET Core</strong>.</p><p>The connection is direct.</p><p>Suppose each active request needs 5 MB of working memory.</p><p>At:</p><pre><code><code>20 concurrent requests</code></code></pre><p>we may need roughly:</p><pre><code><code>100 MB</code></code></pre><p>for that working data.</p><p>At:</p><pre><code><code>2,000 concurrent requests</code></code></pre><p>the same design theoretically demands:</p><pre><code><code>10 GB</code></code></pre><p>before considering the rest of the application.</p><p>Reducing allocations helps.</p><p>But sometimes the correct solution is not to make each request 10% cheaper.</p><p>It is to <strong>limit concurrency</strong>.</p><p>Memory is another finite resource that backpressure protects.</p><h2>Memory Management Connects to Graceful Shutdown</h2><p>Our previous article covered <strong>Graceful Shutdown and Application Lifecycle in ASP.NET Core</strong>.</p><p>Memory behavior affects lifecycle too.</p><p>Suppose an instance is under extreme memory pressure when deployment begins.</p><p>Thousands of queued operations hold large buffers.</p><p>Background workers are processing oversized batches.</p><p>The application now needs to drain before termination.</p><p>A bounded, memory-conscious architecture:</p><pre><code><code>Has fewer outstanding operations
Uses controlled buffer sizes
Limits concurrent work
Keeps queues bounded</code></code></pre><p>and therefore shuts down more predictably.</p><p>Performance, resilience, and lifecycle management are not separate concerns.</p><p>They reinforce one another.</p><h2>A Practical Investigation Workflow</h2><p>Suppose production shows periodic latency spikes and high memory usage.</p><p>Do not immediately rewrite the application.</p><p>Start with questions.</p><p><strong>1. Is memory actually growing continuously?</strong></p><p>Observe the baseline over time.</p><p><strong>2. What is the allocation rate?</strong></p><p>High churn may matter even if heap size is stable.</p><p><strong>3. Which generations are collecting?</strong></p><p>Frequent Gen 2 activity deserves investigation.</p><p><strong>4. Is the LOH growing or churning?</strong></p><p>Look for large arrays, buffers, and serialized payloads.</p><p><strong>5. Which types dominate allocations?</strong></p><p>Profile them.</p><p><strong>6. Which objects dominate retained memory?</strong></p><p>Allocation and retention are different problems.</p><p><strong>7. Why are retained objects still reachable?</strong></p><p>Follow references back to GC roots.</p><p><strong>8. Are caches and queues bounded?</strong></p><p>Unbounded structures are frequent offenders.</p><p><strong>9. Can large workloads stream instead of buffer?</strong></p><p>Often, this creates bigger gains than micro-optimizations.</p><p><strong>10. Would pooling materially reduce a measured hot allocation?</strong></p><p>Only then introduce the additional lifetime complexity.</p><p>This process turns &#8220;our app uses too much memory&#8221; into an engineering problem we can actually solve.</p><h2>Common Memory Management Mistakes</h2><p>Several mistakes appear repeatedly in ASP.NET Core applications.</p><p><strong>Optimizing every allocation.</strong> Most allocations are harmless. Focus on hot paths.</p><p><strong>Assuming GC means leaks cannot happen.</strong> Reachable objects cannot be reclaimed.</p><p><strong>Repeatedly allocating large arrays.</strong> LOH churn can create expensive GC behavior.</p><p><strong>Using </strong><code>GC.Collect()</code><strong> as routine maintenance.</strong> It usually treats the symptom, not the cause.</p><p><strong>Pooling everything.</strong> Pools add ownership, reset, security, and retention complexity.</p><p><strong>Forgetting to return rented buffers.</strong> Pooling only works when lifetimes are controlled.</p><p><strong>Using pooled memory after returning it.</strong> Another caller may already own it.</p><p><strong>Keeping giant pooled objects forever.</strong> Reuse can accidentally increase retained memory.</p><p><strong>Creating unbounded caches.</strong> A cache is deliberate memory retention and needs limits.</p><p><strong>Loading entire datasets unnecessarily.</strong> Streaming and batching often reduce peak memory dramatically.</p><p><strong>Watching only process memory.</strong> Allocation rate, live heap, generations, LOH, and GC pauses tell a richer story.</p><h2>What Good Memory Behavior Looks Like</h2><p>A healthy ASP.NET Core service does not necessarily use very little memory.</p><p>A service with 4 GB available may deliberately use a meaningful portion of it for caches and runtime efficiency.</p><p>Healthy behavior looks more like:</p><pre><code><code>Predictable working set
Bounded caches
Bounded queues
Stable live heap
Controlled LOH usage
Reasonable allocation rate
Infrequent expensive collections
Low GC pause impact
No continuous unexplained growth</code></code></pre><p>Memory exists to be used.</p><p>The objective is not:</p><p><strong>Make the graph as low as possible.</strong></p><p>It is:</p><p><strong>Use memory deliberately enough that performance remains predictable under realistic load.</strong></p><h2>Coming Next</h2><p>In the next article, we&#8217;ll explore <strong>High-Performance I/O in ASP.NET Core: Streams, Pipelines, and Efficient Data Processing</strong>.</p><p>Memory management taught us why repeatedly allocating and copying large buffers can become expensive.</p><p>The natural next question is:</p><p><strong>How can we move large amounts of data without constantly creating those buffers in the first place?</strong></p><p>We&#8217;ll look at streaming, <code>Stream</code>, <code>PipeReader</code>, <code>PipeWriter</code>, buffering, backpressure within I/O pipelines, large request and response bodies, and the patterns that help ASP.NET Core process data efficiently without loading everything into memory.</p><h3>Closing Thoughts</h3><p>.NET&#8217;s garbage collector is extremely good at managing memory, but it cannot decide whether your application should have created an object in the first place.</p><p>That distinction sits at the heart of ASP.NET Core memory performance.</p><p>Generation 0 makes short-lived allocations inexpensive.</p><p>Generations 1 and 2 allow the runtime to treat surviving objects differently.</p><p>The Large Object Heap handles allocations that are expensive to move.</p><p><code>ArrayPool&lt;T&gt;</code> and object pools allow selected expensive objects to be reused.</p><p>Streaming can prevent giant intermediate representations.</p><p>Bounded caches and queues prevent intentional retention from becoming uncontrolled growth.</p><p>None of these tools means that allocation itself is bad.</p><p>In fact, ordinary managed allocation is one of .NET&#8217;s strengths.</p><p>The problem is <strong>unnecessary allocation at scale</strong>.</p><p>A few temporary objects in an endpoint called once per minute do not deserve heroic optimization. The same pattern executed tens of thousands of times per second may deserve serious attention.</p><p>So when an ASP.NET Core application&#8217;s memory graph starts looking uncomfortable, do not begin with:</p><pre><code><code>How do we force the GC to clean this up?</code></code></pre><p>Begin with:</p><pre><code><code>What are we allocating?

How often are we allocating it?

How long does it stay alive?

Why does it stay alive?

Does the whole object need to exist?

Could the data be streamed?

Could an expensive buffer be safely reused?

Is concurrency creating more live memory than the process can support?</code></code></pre><p>Those questions lead to durable improvements.</p><p>The best memory optimization is often not making the garbage collector work faster.</p><p>It is <strong>giving the garbage collector less unnecessary work to do.</strong></p><h3>Subscribe Now</h3><p>Enjoying the series? Subscribe to <strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, advanced architecture deep dives, and production-ready .NET performance strategies. Join our Substack Chat to discuss garbage collection, memory diagnostics, application performance, and the engineering decisions behind fast, reliable ASP.NET Core systems. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Graceful Shutdown and Application Lifecycle in ASP.NET Core: Deploy Without Dropping Work]]></title><description><![CDATA[Deploy ASP.NET Core apps without dropping work using graceful shutdown and lifecycle patterns. #ASPToday #aspnetcore #dotnet #resilience]]></description><link>https://www.asptoday.com/p/graceful-shutdown-and-application</link><guid isPermaLink="false">https://www.asptoday.com/p/graceful-shutdown-and-application</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 08 Sep 2026 15:01:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!t2Xa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Stopping an <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> application sounds simple until that application is processing a payment, writing an order, consuming a message, generating a report, or running a backgaround job at the exact moment shutdown begins. In production, processes disappear routinely during deployments, container replacements, scaling events, restarts, and infrastructure maintenance. A graceful shutdown gives the application time to stop taking new work, signal running operations, finish or safely abandon what is already in progress, release resources, and exit cleanly. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!t2Xa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!t2Xa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!t2Xa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!t2Xa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!t2Xa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!t2Xa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2654439,&quot;alt&quot;:&quot;Harbor crews safely dock ships before a storm, symbolizing graceful application shutdown. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/214376452?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Harbor crews safely dock ships before a storm, symbolizing graceful application shutdown. " title="Harbor crews safely dock ships before a storm, symbolizing graceful application shutdown. " srcset="https://substackcdn.com/image/fetch/$s_!t2Xa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!t2Xa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!t2Xa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!t2Xa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e8cb25b-bd69-434a-83bb-fe8f6a41ff25_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we'll explore the ASP.NET Core application lifecycle, <code>IHostedService</code>, <code>BackgroundService</code>, cancellation tokens, shutdown timeouts, readiness, queue draining, and the design decisions that prevent routine deployments from becoming data-loss events. </p><h2>Your Application Will Eventually Be Stopped</h2><p>Developers spend enormous effort thinking about how applications start.</p><p>We configure <a href="https://www.asptoday.com/p/dependency-injection-in-aspnet-core">dependency injection</a>.</p><p>We establish database connections.</p><p>We warm caches.</p><p>We initialize services.</p><p>We start background workers.</p><p>But production applications also need to <strong>stop correctly</strong>.</p><p>Consider a checkout request:</p><pre><code><code>Customer
   &#8595;
ASP.NET Core
   &#8595;
Create Order
   &#8595;
Reserve Inventory
   &#8595;
Charge Payment
   &#8595;
Send Confirmation</code></code></pre><p>Now imagine a deployment begins after the payment succeeds but before the order is saved.</p><p>If the process disappears immediately, we may have:</p><pre><code><code>Payment charged
Order missing
Confirmation missing</code></code></pre><p>Nothing was wrong with the business logic.</p><p>The application simply disappeared at the wrong moment.</p><p>Graceful shutdown exists to make that transition controlled rather than abrupt.</p><h2>Graceful Shutdown Is Not &#8220;Wait Forever&#8221;</h2><p>A graceful shutdown does not mean:</p><p><strong>Keep the process alive until absolutely everything finishes.</strong></p><p>That would create a different problem.</p><p>A stuck operation could prevent a deployment indefinitely.</p><p>Instead, graceful shutdown means:</p><ol><li><p>Shutdown is requested.<br></p></li><li><p>The application stops taking inappropriate new work.<br></p></li><li><p>Running components are notified.<br></p></li><li><p>In-flight work gets a limited opportunity to finish.<br></p></li><li><p>Resources are cleaned up.<br></p></li><li><p>The process exits.<br></p></li></ol><p>There is always a boundary.</p><p>The real architectural question is:</p><p><strong>What can we safely accomplish before that boundary is reached?</strong></p><h2>The ASP.NET Core Host Has a Lifecycle</h2><p>Modern ASP.NET Core applications run inside the .NET Generic Host.</p><p>The host coordinates application startup, dependency injection, logging, configuration, hosted services, and shutdown.</p><p>A simplified lifecycle looks like this:</p><pre><code><code>Process starts
     &#8595;
Host starts
     &#8595;
Services initialize
     &#8595;
Application becomes ready
     &#8595;
Normal operation
     &#8595;
Shutdown requested
     &#8595;
Services stop
     &#8595;
Resources released
     &#8595;
Process exits</code></code></pre><p>ASP.NET Core&#8217;s server abstraction supports graceful stopping. <code>IServer.StopAsync</code> tells the server to stop processing requests and shut down gracefully where possible. </p><p>This lifecycle gives our code a chance to react.</p><p>But that opportunity only helps if the application is designed to use it.</p><h2>What Triggers Shutdown?</h2><p>Shutdown can begin for many reasons:</p><pre><code><code>New deployment
Container replacement
Manual restart
Scale-in event
Host shutdown
Operating system shutdown
Ctrl+C during development
SIGTERM in a container
Application-requested shutdown</code></code></pre><p>A routine rolling deployment is therefore an application lifecycle event.</p><p>Suppose version 17 is running:</p><pre><code><code>Instance A  v17
Instance B  v17
Instance C  v17</code></code></pre><p>Version 18 is deployed.</p><p>An orchestrator may gradually replace those processes:</p><pre><code><code>Instance A  v18
Instance B  v17
Instance C  v17</code></code></pre><p>then:</p><pre><code><code>Instance A  v18
Instance B  v18
Instance C  v17</code></code></pre><p>and eventually:</p><pre><code><code>Instance A  v18
Instance B  v18
Instance C  v18</code></code></pre><p>Each old process needs to leave safely.</p><p>Graceful shutdown is therefore part of deployment architecture, not merely application cleanup.</p><h2>ASP.NET Core Gives Us Lifecycle Signals</h2><p>For application-wide lifecycle events, modern .NET exposes <code>IHostApplicationLifetime</code>.</p><p>It provides cancellation tokens associated with important host transitions:</p><pre><code><code>public sealed class LifecycleLogger
{
    public LifecycleLogger(
        IHostApplicationLifetime lifetime,
        ILogger&lt;LifecycleLogger&gt; logger)
    {
        lifetime.ApplicationStarted.Register(() =&gt;
            logger.LogInformation(
                "Application started"));

        lifetime.ApplicationStopping.Register(() =&gt;
            logger.LogInformation(
                "Application stopping"));

        lifetime.ApplicationStopped.Register(() =&gt;
            logger.LogInformation(
                "Application stopped"));
    }
}</code></code></pre><p>These signals let components observe the application&#8217;s lifecycle.</p><p>An important naming detail is worth noting. Older examples may use <code>IApplicationLifetime</code>. That interface is deprecated in favor of <code>IHostApplicationLifetime</code>. </p><p>For new ASP.NET Core applications, use the current hosting abstraction.</p><h2>ApplicationStopping Is a Warning Bell</h2><p>Conceptually, <code>ApplicationStopping</code> means:</p><p><strong>The host is shutting down. Prepare to stop.</strong></p><p>This is not the moment to begin ten minutes of new processing.</p><p>It is a signal to:</p><pre><code><code>Stop creating new internal work
Stop polling for more work
Signal long-running operations
Flush small buffers where appropriate
Prepare resources for disposal</code></code></pre><p>The application should already know what &#8220;prepare to stop&#8221; means.</p><p>Trying to invent that policy inside a shutdown callback is too late.</p><h2>Hosted Services Fit Naturally Into the Lifecycle</h2><p>ASP.NET Core background processing is commonly implemented with <code>IHostedService</code> or <code>BackgroundService</code>.</p><p><code>IHostedService</code> defines:</p><pre><code><code>Task StartAsync(CancellationToken cancellationToken);

Task StopAsync(CancellationToken cancellationToken);</code></code></pre><p>The host calls these methods as part of service startup and shutdown. </p><p>A simple service might look like:</p><pre><code><code>public sealed class CacheService : IHostedService
{
    private readonly ILogger&lt;CacheService&gt; _logger;

    public CacheService(
        ILogger&lt;CacheService&gt; logger)
    {
        _logger = logger;
    }

    public Task StartAsync(
        CancellationToken cancellationToken)
    {
        _logger.LogInformation("Cache service started");

        return Task.CompletedTask;
    }

    public Task StopAsync(
        CancellationToken cancellationToken)
    {
        _logger.LogInformation("Cache service stopping");

        return Task.CompletedTask;
    }
}</code></code></pre><p>For continuous background work, however, <code>BackgroundService</code> is usually more convenient.</p><h2>BackgroundService and the Stopping Token</h2><p>Consider a worker that processes queued jobs:</p><pre><code><code>public sealed class OrderWorker : BackgroundService
{
    private readonly OrderQueue _queue;
    private readonly ILogger&lt;OrderWorker&gt; _logger;

    public OrderWorker(
        OrderQueue queue,
        ILogger&lt;OrderWorker&gt; logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var order =
                await _queue.ReadAsync(stoppingToken);

            await ProcessOrderAsync(
                order,
                stoppingToken);
        }
    }
}</code></code></pre><p><code>BackgroundService.ExecuteAsync</code> represents the lifetime of the long-running background operation, and the host waits during shutdown for that execution to finish. The stopping token is how the service learns that shutdown has begun. </p><p>That token is not decorative.</p><p>If our code ignores it:</p><pre><code><code>await ProcessOrderAsync(order);</code></code></pre><p>the host may be trying to stop while our worker behaves as though nothing happened.</p><p>Cancellation needs to flow through the application.</p><h2>Pass Cancellation Tokens Downward</h2><p>Suppose:</p><pre><code><code>protected override async Task ExecuteAsync(
    CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        var job = await GetJobAsync(stoppingToken);

        await ProcessAsync(job, stoppingToken);
    }
}</code></code></pre><p>Then:</p><pre><code><code>private async Task ProcessAsync(
    Job job,
    CancellationToken cancellationToken)
{
    var data = await LoadDataAsync(
        job,
        cancellationToken);

    await SaveResultAsync(
        data,
        cancellationToken);
}</code></code></pre><p>And where supported:</p><pre><code><code>await dbContext.SaveChangesAsync(
    cancellationToken);</code></code></pre><p>The signal travels through the operation:</p><pre><code><code>Host stopping
     &#8595;
BackgroundService
     &#8595;
Business service
     &#8595;
Database / HTTP / I/O</code></code></pre><p>Now shutdown can actually influence the work being performed.</p><p>Without propagation, the top-level token does very little.</p><h2>Cancellation Does Not Mean Failure</h2><p>A shutdown cancellation token means:</p><p><strong>The application would like this operation to stop.</strong></p><p>That is different from an unexpected exception.</p><p>A well-designed worker may treat <code>OperationCanceledException</code> during normal shutdown as expected.</p><p>For example:</p><pre><code><code>protected override async Task ExecuteAsync(
    CancellationToken stoppingToken)
{
    try
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await ProcessNextAsync(stoppingToken);
        }
    }
    catch (OperationCanceledException)
        when (stoppingToken.IsCancellationRequested)
    {
        // Expected during shutdown.
    }
}</code></code></pre><p>Do not fill production error logs with alarming stack traces for a shutdown behavior the application intentionally requested.</p><p>At the same time, do not swallow unrelated exceptions.</p><p>Shutdown cancellation and genuine failures are different operational signals.</p><h2>Stop Taking New Background Work</h2><p>Imagine a worker:</p><pre><code><code>Read message
Process message
Read message
Process message
Read message
...</code></code></pre><p>Shutdown begins immediately after processing completes.</p><p>Should the worker fetch another message?</p><p>Usually, no.</p><p>A good shutdown sequence is:</p><pre><code><code>Shutdown requested
       &#8595;
Stop fetching new jobs
       &#8595;
Handle current job safely
       &#8595;
Exit worker</code></code></pre><p>Compare that with:</p><pre><code><code>Shutdown requested
       &#8595;
Fetch another job
       &#8595;
Start expensive work
       &#8595;
Shutdown deadline expires
       &#8595;
Process terminated</code></code></pre><p>The second design creates avoidable risk.</p><h2>But Should Current Work Finish or Be Cancelled?</h2><p>This depends on the operation.</p><p>Suppose a worker is generating a thumbnail.</p><p>Cancelling it halfway through may be harmless.</p><p>The job can be retried later.</p><p>Now suppose the worker is processing:</p><pre><code><code>Charge credit card</code></code></pre><p>Simply abandoning the operation can create uncertainty.</p><p>Did the external provider receive the request?</p><p>Did it charge the card?</p><p>Did our application save the result?</p><p>This is why graceful shutdown cannot guarantee correctness by itself.</p><p>Business-critical work also needs durable design.</p><h2>Graceful Shutdown Is Not a Durability Mechanism</h2><p>This distinction is fundamental.</p><p>Graceful shutdown helps during <strong>expected</strong> termination.</p><p>It cannot protect you from:</p><pre><code><code>Power loss
Kernel crash
Out-of-memory termination
Hardware failure
Process crash
kill -9
Infrastructure failure</code></code></pre><p>Microsoft&#8217;s hosted-service guidance explicitly notes that <code>StopAsync</code> might not be called if the process shuts down unexpectedly. </p><p>Therefore, never design:</p><pre><code><code>Important data exists only in memory
       &#8595;
We'll save it during StopAsync</code></code></pre><p>for data you cannot afford to lose.</p><p>Shutdown hooks are cleanup opportunities.</p><p>They are not transactional storage.</p><h2>Durable Work Needs Durable State</h2><p>Suppose an HTTP request accepts an expensive video-processing job.</p><p>This design is fragile:</p><pre><code><code>Request
   &#8595;
Add job to in-memory List&lt;Job&gt;
   &#8595;
Return 202 Accepted</code></code></pre><p>If the process crashes:</p><pre><code><code>List&lt;Job&gt; disappears</code></code></pre><p>A better design for important work might use:</p><pre><code><code>Request
   &#8595;
Persist job / durable broker
   &#8595;
Return 202 Accepted
   &#8595;
Worker processes job</code></code></pre><p>Now application shutdown does not determine whether the job exists.</p><p>The next worker instance can continue.</p><p>This connects directly to our previous articles on messaging, background processing, idempotency, and resilient distributed systems.</p><h2>Queue Draining Needs a Clear Meaning</h2><p>&#8220;Drain the queue before shutdown&#8221; sounds ideal.</p><p>But consider a queue containing 20,000 jobs.</p><p>Each takes one second.</p><p>A single worker would need hours.</p><p>Waiting for the entire queue is clearly inappropriate.</p><p>Instead, draining usually means something more controlled:</p><pre><code><code>Stop accepting/fetching new work
Finish limited in-flight work
Return unstarted durable work to the system
Exit</code></code></pre><p>For an in-memory queue, the decision is harder because anything left behind disappears with the process.</p><p>That is another reason durable queues matter for work that must survive restarts.</p><h2>In-Flight Work Needs Ownership Rules</h2><p>Distributed workers create another problem.</p><p>Suppose:</p><pre><code><code>Worker A receives Job 123
Worker A begins processing
Worker A shuts down
Worker B receives Job 123</code></code></pre><p>Could both complete the job?</p><p>Potentially.</p><p>A production architecture therefore needs concepts such as:</p><pre><code><code>Acknowledgement
Visibility timeout
Lease
Idempotency
Deduplication
Transactional state</code></code></pre><p>Graceful shutdown reduces the chance of interruption.</p><p>Idempotent processing reduces the damage when interruption still happens.</p><p>These patterns complement each other.</p><h2>The Shutdown Timeout Is a Budget</h2><p>Graceful shutdown cannot continue forever.</p><p>The .NET host has a shutdown timeout that governs how long graceful stopping is allowed before cancellation tells shutdown work to stop waiting gracefully. <code>HostOptions</code> exposes <code>ShutdownTimeout</code> for this purpose. </p><p>It can be configured:</p><pre><code><code>builder.Services.Configure&lt;HostOptions&gt;(options =&gt;
{
    options.ShutdownTimeout =
        TimeSpan.FromSeconds(45);
});</code></code></pre><p>Do not interpret this as:</p><p><strong>Our application now has permission to take 45 seconds to stop.</strong></p><p>Think of it as:</p><p><strong>45 seconds is the maximum shutdown budget available to the host&#8217;s graceful-stop process.</strong></p><p>Microsoft&#8217;s current hosted-service guidance documents a default 30-second cancellation timeout for graceful shutdown in the Generic Host scenario it describes. </p><p>Deployment infrastructure may impose its own deadline as well, so the effective budget must be understood end to end.</p><h2>More Shutdown Time Is Not Automatically Better</h2><p>If a service routinely needs three minutes to stop, increasing the timeout to five minutes may hide a design problem.</p><p>Ask why.</p><p>Perhaps:</p><pre><code><code>Worker ignores cancellation
Database query cannot finish promptly
Queue keeps accepting work
External API call has no timeout
Large buffer is flushed only at shutdown
Background task was never designed to stop</code></code></pre><p>Increasing the timeout can be appropriate.</p><p>But it should follow understanding, not replace it.</p><p>A healthy service should usually stop predictably.</p><h2>StopAsync Is for Deliberate Shutdown Work</h2><p>If you need additional cleanup, override <code>StopAsync</code>.</p><p>For example:</p><pre><code><code>public sealed class MetricsWorker : BackgroundService
{
    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await CollectAsync(stoppingToken);
        }
    }

    public override async Task StopAsync(
        CancellationToken cancellationToken)
    {
        await FlushFinalMetricsAsync(cancellationToken);

        await base.StopAsync(cancellationToken);
    }
}</code></code></pre><p>When overriding <code>BackgroundService.StartAsync</code> or <code>StopAsync</code>, Microsoft&#8217;s Worker Service guidance says to call and await the base implementation so the service starts and shuts down properly. </p><p>Keep shutdown work bounded.</p><p>Do not turn <code>StopAsync</code> into a second application runtime.</p><h2>Cleanup Is Different from Business Completion</h2><p>Good shutdown cleanup might include:</p><pre><code><code>Stop timers
Stop polling
Flush a small telemetry buffer
Close producers
Dispose custom resources
Release leases
Publish worker status</code></code></pre><p>Riskier shutdown logic includes:</p><pre><code><code>Process every remaining customer order
Perform large data migrations
Upload gigabytes of pending files
Retry failed integrations indefinitely
Save the application's only copy of critical state</code></code></pre><p>If correctness depends on a complex action successfully completing during shutdown, the architecture is fragile.</p><h2>HTTP Requests Need Graceful Treatment Too</h2><p>Background workers are only half the story.</p><p>An ASP.NET Core instance may have active HTTP requests when termination begins.</p><p>Conceptually, a graceful deployment should move toward:</p><pre><code><code>Stop routing new traffic
        &#8595;
Allow in-flight requests time to finish
        &#8595;
Stop application</code></code></pre><p>This is commonly called <strong>connection draining</strong> or <strong>request draining</strong>.</p><p>The exact mechanics depend on where the application runs.</p><p>A reverse proxy, load balancer, container orchestrator, and ASP.NET Core host may all participate.</p><p>The key principle is universal:</p><p><strong>Do not deliberately send fresh traffic to an instance that is already leaving service.</strong></p><h2>Readiness and Liveness Are Different Questions</h2><p>Health checks become important here.</p><p>A <strong>liveness</strong> signal roughly answers:</p><p><strong>Is this process alive enough that restarting it is unnecessary?</strong></p><p>A <strong>readiness</strong> signal answers:</p><p><strong>Should this instance currently receive traffic?</strong></p><p>Those are not the same question.</p><p>During shutdown, an instance can be:</p><pre><code><code>Alive: Yes
Ready for new traffic: No</code></code></pre><p>That state is useful.</p><p>The instance can remain alive long enough to finish existing work while being removed from the pool for new requests.</p><h2>The Ideal Deployment Sequence</h2><p>A simplified rolling-deployment sequence looks like:</p><pre><code><code>1. New instance starts
2. New instance initializes
3. New instance becomes ready
4. Load balancer begins routing traffic to it
5. Old instance becomes unready
6. New traffic stops going to old instance
7. Existing work drains
8. Shutdown signal propagates
9. Background workers stop
10. Old process exits</code></code></pre><p>The exact ordering can vary by hosting environment.</p><p>But the principle is consistent.</p><p><strong>Arrival and departure should both be controlled transitions.</strong></p><h2>Startup Is Also Part of the Lifecycle</h2><p>Graceful shutdown gets most of the attention, but startup mistakes can also cause outages.</p><p>Suppose a container starts and immediately reports itself ready.</p><p>Then it spends 20 seconds:</p><pre><code><code>Loading configuration
Connecting to dependencies
Warming critical cache
Initializing local state</code></code></pre><p>Traffic arrives during those 20 seconds.</p><p>Requests fail.</p><p>The process is alive, but the application is not ready.</p><p>A mature lifecycle design therefore considers:</p><pre><code><code>Starting
Ready
Draining
Stopping
Stopped</code></code></pre><p>rather than simply:</p><pre><code><code>Running
Not running</code></code></pre><h2>Keep StartAsync Short</h2><p><code>IHostedService.StartAsync</code> is part of host startup.</p><p>Microsoft&#8217;s hosted-service documentation advises keeping it short because hosted services participate in application startup, and long initialization can delay the rest of the application. </p><p>Avoid:</p><pre><code><code>public async Task StartAsync(
    CancellationToken cancellationToken)
{
    await ProcessTenMillionRecordsAsync();
}</code></code></pre><p>If long-running processing belongs in the service, it generally belongs in <code>ExecuteAsync</code>, with readiness and dependencies designed appropriately.</p><p>Startup should establish the conditions necessary for safe operation.</p><p>It should not become an uncontrolled batch job.</p><h2>Request Cancellation Matters During Normal Operation Too</h2><p>ASP.NET Core exposes:</p><pre><code><code>HttpContext.RequestAborted</code></code></pre><p>for HTTP request cancellation.</p><p>For example:</p><pre><code><code>app.MapGet(
    "/reports/{id}",
    async (
        int id,
        ReportService reports,
        CancellationToken cancellationToken) =&gt;
    {
        return await reports.GetAsync(
            id,
            cancellationToken);
    });</code></code></pre><p>In Minimal APIs, a <code>CancellationToken</code> parameter can be bound to request cancellation.</p><p>That matters when clients disconnect, timeouts occur, or infrastructure terminates a request.</p><p>The same design principle applies:</p><p><strong>Stop doing work when the result is no longer useful, unless business correctness requires that work to continue independently.</strong></p><p>If the latter is true, the operation probably belongs in a durable asynchronous workflow rather than being tied entirely to the HTTP request lifetime.</p><h2>Transactions Need Careful Boundaries</h2><p>Suppose shutdown occurs during:</p><pre><code><code>await using var transaction =
    await db.Database.BeginTransactionAsync(
        cancellationToken);

order.Status = OrderStatus.Confirmed;

await db.SaveChangesAsync(cancellationToken);

await transaction.CommitAsync(cancellationToken);</code></code></pre><p>Cancellation before commit may allow the transaction to roll back.</p><p>That is often preferable to leaving a partially completed local operation.</p><p>But distributed workflows are more complicated.</p><p>If you already called an external payment provider before the local transaction was cancelled, rolling back your database does not reverse the external side effect.</p><p>This is where patterns such as:</p><pre><code><code>Idempotency
Outbox messaging
Saga compensation
Durable workflow state</code></code></pre><p>become important.</p><p>Graceful shutdown cannot turn distributed operations into atomic transactions.</p><h2>Do Not Start Fire-and-Forget Work from Requests</h2><p>Consider:</p><pre><code><code>app.MapPost("/orders", async order =&gt;
{
    _ = SendConfirmationAsync(order);

    return Results.Ok();
});</code></code></pre><p>The HTTP response returns.</p><p>The task continues in the process.</p><p>Then deployment begins.</p><p>The process exits.</p><p>Did the email send?</p><p>Maybe.</p><p>This is one reason uncontrolled fire-and-forget work is dangerous in server applications.</p><p>If work matters after the request ends, give it an explicit owner.</p><p>For example:</p><pre><code><code>Request
   &#8595;
Persist work
   &#8595;
Background worker
   &#8595;
Execute
   &#8595;
Record completion</code></code></pre><p>Now lifecycle behavior is visible and manageable.</p><h2>Timers Need Shutdown Awareness</h2><p>Periodic tasks can create subtle problems.</p><p>Imagine:</p><pre><code><code>Every 60 seconds:
Generate invoices</code></code></pre><p>Shutdown begins at second 59.</p><p>The timer fires.</p><p>A large invoice batch starts while the process is already supposed to be leaving.</p><p>Periodic services should respect shutdown state before starting another cycle.</p><p>A <code>PeriodicTimer</code> pattern makes this easier:</p><pre><code><code>protected override async Task ExecuteAsync(
    CancellationToken stoppingToken)
{
    using var timer =
        new PeriodicTimer(
            TimeSpan.FromMinutes(1));

    while (await timer.WaitForNextTickAsync(
        stoppingToken))
    {
        await RunCycleAsync(stoppingToken);
    }
}</code></code></pre><p>When shutdown cancellation arrives, the loop can end instead of deliberately starting another round of work.</p><h2>Shutdown Should Be Idempotent Too</h2><p>Cleanup code should tolerate being called in a partially cleaned-up state.</p><p>Suppose shutdown logic performs:</p><pre><code><code>Stop consumer
Flush buffer
Release lease
Close connection</code></code></pre><p>What happens if:</p><pre><code><code>Consumer already stopped?
Buffer already empty?
Lease already expired?
Connection already closed?</code></code></pre><p>Cleanup should generally remain safe.</p><p>Shutdown paths receive less day-to-day testing than normal request paths, which makes simple, repeatable behavior especially valuable.</p><h2>Logging During Shutdown</h2><p>Shutdown needs observability.</p><p>Useful lifecycle logs include:</p><pre><code><code>Shutdown requested
Readiness disabled
Active requests remaining
Background workers stopping
Current job completed
Queue consumer stopped
Resources disposed
Shutdown completed
Shutdown deadline exceeded</code></code></pre><p>Include timestamps and durations.</p><p>For example:</p><pre><code><code>_logger.LogInformation(
    "Worker shutdown started. ActiveJob={JobId}",
    activeJobId);</code></code></pre><p>Then:</p><pre><code><code>_logger.LogInformation(
    "Worker shutdown completed in {ElapsedMs} ms",
    elapsed.TotalMilliseconds);</code></code></pre><p>When a deployment intermittently loses work, these logs can reveal whether shutdown was requested, whether the application reacted, and whether it ran out of time.</p><h2>Measure Shutdown, Do Not Merely Log It</h2><p>Useful metrics include:</p><pre><code><code>Shutdown duration
In-flight requests at shutdown
Active background jobs
Jobs interrupted
Jobs safely returned to queue
Shutdown timeout count
Outstanding messages
Lease release failures</code></code></pre><p>Over time, you may discover:</p><pre><code><code>Normal shutdown: 4 seconds
p95 shutdown: 11 seconds
Worst shutdown: 29 seconds</code></code></pre><p>That gives you something concrete to compare with infrastructure termination deadlines.</p><p>Lifecycle behavior should be observable just like request latency and error rates.</p><h2>Connect Shutdown with Backpressure</h2><p>Our previous article covered <strong><a href="https://www.asptoday.com/p/designing-backpressure-in-aspnet">Designing Backpressure in ASP.NET Core</a></strong>.</p><p>Backpressure and graceful shutdown solve different problems, but they reinforce each other.</p><p>Backpressure asks:</p><p><strong>How do we avoid accepting more work than we can process?</strong></p><p>Graceful shutdown asks:</p><p><strong>How do we safely stop processing when this instance must disappear?</strong></p><p>Imagine an instance with an unbounded queue of 100,000 jobs.</p><p>A shutdown request arrives.</p><p>Gracefully draining that backlog may be impossible.</p><p>Now imagine a bounded workload:</p><pre><code><code>20 active operations
100 queued jobs maximum</code></code></pre><p>The amount of local work is controlled.</p><p>Shutdown becomes far more predictable.</p><p>Bounded systems are easier to stop.</p><h2>The Dangerous Combination</h2><p>A particularly fragile application has all of these:</p><pre><code><code>Unbounded queues
Fire-and-forget tasks
No cancellation propagation
Long external calls
No request draining
No durable job state
Huge shutdown cleanup
No lifecycle metrics</code></code></pre><p>It may run beautifully during quiet development tests.</p><p>Production deployments reveal the weaknesses.</p><p>The goal is not to add a shutdown callback to this architecture.</p><p>The goal is to design work ownership and lifecycle boundaries clearly enough that shutdown becomes routine.</p><h2>A Practical Worker Design</h2><p>Consider an order-export worker.</p><p>We want it to:</p><ul><li><p>Stop fetching new exports when shutdown begins<br></p></li><li><p>Allow the current export a reasonable opportunity to finish<br></p></li><li><p>Avoid losing unstarted jobs<br></p></li><li><p>Remain safe if the process dies unexpectedly<br></p></li></ul><p>A production architecture might be:</p><pre><code><code>Durable Export Queue
        &#8595;
Worker claims job
        &#8595;
Persist processing state
        &#8595;
Generate export
        &#8595;
Persist result
        &#8595;
Acknowledge job</code></code></pre><p>During shutdown:</p><pre><code><code>Shutdown signal
      &#8595;
Stop claiming jobs
      &#8595;
Finish or cancel current job
      &#8595;
If incomplete, leave/release durable job
      &#8595;
Exit</code></code></pre><p>Now correctness does not depend entirely on the process surviving.</p><p>That is the key shift.</p><h2>A Practical HTTP Design</h2><p>Now consider an order API.</p><p>During a rolling deployment:</p><pre><code><code>Load Balancer
      &#8595;
Instance A
Instance B
Instance C</code></code></pre><p>Instance B is selected for termination.</p><p>The ideal behavior is:</p><pre><code><code>Instance B marked not ready
       &#8595;
New requests routed to A and C
       &#8595;
B finishes in-flight requests
       &#8595;
Hosted services stop
       &#8595;
B exits</code></code></pre><p>Meanwhile, a new instance becomes ready and joins the pool.</p><p>From the user&#8217;s perspective, the deployment may be invisible.</p><p>That is what we mean by deploying without dropping work.</p><h2>Common Graceful Shutdown Mistakes</h2><p>Several mistakes appear repeatedly.</p><p><strong>Ignoring cancellation tokens.</strong> The application receives a shutdown signal, but lower-level operations never see it.</p><p><strong>Using </strong><code>StopAsync</code><strong> as emergency storage.</strong> Critical state should already be durable.</p><p><strong>Accepting new work while draining.</strong> The finish line keeps moving.</p><p><strong>Setting an enormous timeout.</strong> This can hide services that do not know how to stop.</p><p><strong>Assuming </strong><code>StopAsync</code><strong> always runs.</strong> Crashes and forced termination exist.</p><p><strong>Fire-and-forget request work.</strong> Nobody owns the task after the request completes.</p><p><strong>Confusing liveness with readiness.</strong> A process can be alive while intentionally refusing new traffic.</p><p><strong>Draining an entire durable backlog locally.</strong> The next healthy instance can process durable queued work.</p><p><strong>Testing startup but never shutdown.</strong> Half the lifecycle remains unverified.</p><h2>Test Shutdown Deliberately</h2><p>Do not wait for production deployments to test this.</p><p>Create a long-running request.</p><p>Start a background job.</p><p>Begin a database operation.</p><p>Fill a queue.</p><p>Then terminate the application gracefully.</p><p>Observe:</p><pre><code><code>Did new work stop?

Did existing requests finish?

Did workers receive cancellation?

Did jobs survive restart?

Did database state remain valid?

Were duplicate operations created?

Did the process exit within its budget?

Did the replacement instance become ready first?</code></code></pre><p>Then test harder scenarios.</p><p>Kill the process without graceful shutdown.</p><p>Restart infrastructure.</p><p>Interrupt network connections.</p><p>Terminate workers halfway through jobs.</p><p>Graceful shutdown testing tells you whether normal termination works.</p><p>Crash testing tells you whether correctness depends too heavily on normal termination.</p><p>You need both.</p><h2>The Goal Is Predictable Departure</h2><p>We often describe highly available systems in terms of adding instances.</p><p>But removing instances safely is just as important.</p><p>A production service should know how to enter:</p><pre><code><code>Ready</code></code></pre><p>and how to leave it.</p><p>It should know when to stop accepting work.</p><p>It should know which operations can be cancelled.</p><p>It should know which must survive the process.</p><p>It should know how long it may take.</p><p>And it should tell operators what is happening while it shuts down.</p><p>A process that cannot leave safely is not truly disposable infrastructure.</p><h2>How This Fits Our ASP.NET Core Journey</h2><p>Our recent articles have focused increasingly on behavior under real production conditions.</p><p><strong>.NET Aspire</strong> gave us a model for orchestrating and observing distributed application resources.</p><p><strong>Configuration at Scale</strong> showed how operational settings can evolve safely.</p><p><strong>Designing Backpressure</strong> addressed what happens when demand exceeds processing capacity.</p><p>Graceful shutdown completes another part of that picture.</p><p>Production applications must handle both pressure and change.</p><p>Traffic spikes.</p><p>Dependencies slow down.</p><p>Instances scale out.</p><p>Instances scale in.</p><p>New versions deploy.</p><p>Old versions disappear.</p><p>The application lifecycle is therefore not a startup detail. It is part of the architecture.</p><h2>Coming Next</h2><p>In the next article, we&#8217;ll explore <strong>Memory Management in ASP.NET Core: GC, Allocations, LOH, and Object Pooling</strong>.</p><p>Graceful shutdown helps an application release work safely when the process is intentionally ending. Memory management asks a different question:</p><p><strong>How do we keep the process healthy while it continues running?</strong></p><p>We&#8217;ll look at garbage collection, allocation pressure, the Large Object Heap, temporary objects, pooling, memory diagnostics, and the coding patterns that can turn a seemingly fast ASP.NET Core application into a high-latency, memory-hungry service under real production load.</p><h2>Closing Thoughts</h2><p>Graceful shutdown is not about making an application impossible to interrupt.</p><p>That is unrealistic.</p><p>It is about making <strong>expected interruption predictable</strong>.</p><p>When shutdown begins, an ASP.NET Core application should understand the transition. It should stop creating unnecessary new work, propagate cancellation, allow appropriate in-flight operations to complete, preserve important work durably, release resources, and exit within a known time budget.</p><p><code>IHostedService</code>, <code>BackgroundService</code>, <code>IHostApplicationLifetime</code>, cancellation tokens, server shutdown behavior, and <code>HostOptions.ShutdownTimeout</code> give us the framework-level building blocks. </p><p>But the framework cannot decide whether an unfinished payment should be retried, whether a queued message may be processed twice, whether an export can be abandoned, or whether a job must survive a crash.</p><p>Those decisions belong to the architecture.</p><p>The strongest production design therefore assumes two things at the same time:</p><p><strong>The application will usually get a chance to shut down gracefully.</strong></p><p>And:</p><p><strong>One day, it will not.</strong></p><p>Design for both, and routine deployments become far less dramatic.</p><h2>Subscribe Now</h2><p>Enjoying the series? Subscribe to <strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, advanced architecture deep dives, and production-ready .NET strategies. Join our Substack Chat to discuss deployment patterns, application lifecycle design, resilience, and the real-world challenges of running modern ASP.NET Core systems. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Designing Backpressure in ASP.NET Core: Handling Overload Without Crashing Your System]]></title><description><![CDATA[Protect ASP.NET Core apps from overload with bounded queues, concurrency limits, and load shedding. #ASPToday #aspnetcore #dotnet #resilience]]></description><link>https://www.asptoday.com/p/designing-backpressure-in-aspnet</link><guid isPermaLink="false">https://www.asptoday.com/p/designing-backpressure-in-aspnet</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 01 Sep 2026 15:02:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!T8pJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A healthy <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> application can become unstable without a single bug in its business logic. All it takes is more work arriving than the system can process. Requests accumulate, queues grow, memory usage rises, database connections disappear, latency explodes, and eventually the entire application may fail. Backpressure prevents that chain reaction. Instead of accepting unlimited work, the system deliberately slows producers, limits concurrency, bounds queues, rejects excess requests, or sheds less important work.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!T8pJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!T8pJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!T8pJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!T8pJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!T8pJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!T8pJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2622755,&quot;alt&quot;:&quot;Engineers control dam spillways under heavy pressure, symbolizing backpressure and load management. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/213656095?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Engineers control dam spillways under heavy pressure, symbolizing backpressure and load management. " title="Engineers control dam spillways under heavy pressure, symbolizing backpressure and load management. " srcset="https://substackcdn.com/image/fetch/$s_!T8pJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!T8pJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!T8pJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!T8pJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d5c968-d016-42de-86cf-4d670af5c8bb_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we'll build a practical backpressure strategy using ASP.NET Core rate limiting, bounded <code>Channel&lt;T&gt;</code> queues, concurrency controls, cancellation, and graceful degradation, while looking closely at the trade-offs that determine whether an overloaded application bends or breaks. </p><h2>What Is Backpressure?</h2><p>Imagine a busy restaurant kitchen.</p><p>Orders arrive every few seconds.</p><p>When the kitchen can prepare 40 meals every 10 minutes and customers order 30, everything works well.</p><p>Now 200 orders arrive.</p><p>The kitchen cannot magically cook five times faster.</p><p>If the restaurant keeps accepting unlimited orders, tickets begin covering the walls. Customers wait longer. Staff make mistakes. Ingredients run out. Eventually, even orders that could have been completed quickly become trapped behind hundreds of others.</p><p>Software behaves surprisingly similarly.</p><p>Suppose an ASP.NET Core service can reliably process:</p><pre><code><code>500 requests per second</code></code></pre><p>but receives:</p><pre><code><code>2,000 requests per second</code></code></pre><p>We have two rates:</p><pre><code><code>Arrival rate:      2,000/sec
Processing rate:     500/sec</code></code></pre><p>The missing 1,500 requests do not disappear.</p><p>Unless the system deliberately controls them, they wait somewhere.</p><p>That is where the trouble begins.</p><p><strong>Backpressure is how a system communicates that it cannot safely accept work as quickly as work is arriving.</strong></p><h2>Overload Is Different from Failure</h2><p>This distinction is important.</p><p>A failed database is unavailable.</p><p>An overloaded database may still be perfectly healthy. It simply has more work than it can process efficiently.</p><p>Likewise, an ASP.NET Core application can be running normally while becoming progressively overloaded.</p><p>At first:</p><pre><code><code>CPU: 45%
Latency: 80 ms
Queue: 10</code></code></pre><p>Then:</p><pre><code><code>CPU: 80%
Latency: 450 ms
Queue: 2,000</code></code></pre><p>Eventually:</p><pre><code><code>CPU: 100%
Latency: 12 seconds
Queue: 50,000
Memory: critical</code></code></pre><p>By the time health checks begin failing, the real problem may have started minutes earlier.</p><p>Backpressure attempts to stop the system from reaching that final state.</p><h2>Why Unlimited Queues Are Dangerous</h2><p>Queues are useful because they separate producers from consumers.</p><p>Suppose an API receives image-processing jobs.</p><p>Instead of making users wait while every image is processed, the endpoint adds jobs to a queue.</p><pre><code><code>HTTP Request
     &#8595;
Job Queue
     &#8595;
Background Worker
     &#8595;
Image Processing</code></code></pre><p>That seems resilient.</p><p>But what happens if requests arrive faster than the worker processes them?</p><p>If the queue has no meaningful limit:</p><pre><code><code>10 jobs
100 jobs
10,000 jobs
500,000 jobs</code></code></pre><p>The queue may simply move the overload somewhere else.</p><p>An in-memory queue consumes memory.</p><p>A durable broker consumes storage and increases processing delay.</p><p>A database-backed queue grows.</p><p>Users may receive an immediate &#8220;accepted&#8221; response while their job will not actually begin for hours.</p><p>A queue absorbs <strong>temporary bursts</strong>.</p><p>It does not create processing capacity.</p><h2>The Basic Backpressure Equation</h2><p>Every system has some sustainable throughput.</p><p>Conceptually:</p><pre><code><code>Incoming Work &lt;= Processing Capacity</code></code></pre><p>When this remains true, the system is stable.</p><p>When:</p><pre><code><code>Incoming Work &gt; Processing Capacity</code></code></pre><p>for long enough, something has to happen.</p><p>We can:</p><ul><li><p>Make producers wait<br></p></li><li><p>Queue a limited amount of work<br></p></li><li><p>Reject excess work<br></p></li><li><p>Drop replaceable work<br></p></li><li><p>Reduce the cost of each request<br></p></li><li><p>Scale processing capacity<br></p></li></ul><p>What we should not do is pretend capacity is unlimited.</p><h2>Backpressure Begins with Knowing Your Limits</h2><p>Before setting limits, we need to understand what actually constrains the application.</p><p>It might be:</p><pre><code><code>CPU
Memory
Database connections
Database throughput
HTTP connections
Thread-pool pressure
External API quotas
Queue consumers
Disk throughput</code></code></pre><p>Consider an endpoint that calls a payment provider.</p><p>Your ASP.NET Core server might handle thousands of concurrent requests.</p><p>But perhaps the payment provider safely supports only 100 concurrent operations.</p><p>Your application&#8217;s theoretical capacity does not matter.</p><p>The dependency is the bottleneck.</p><p>A good backpressure strategy protects the <strong>scarce resource</strong>, not merely the web server.</p><h2>Concurrency Is Often More Important Than Request Rate</h2><p>Suppose two endpoints each receive 100 requests per second.</p><p>Endpoint A takes 10 milliseconds.</p><p>Endpoint B takes 5 seconds.</p><p>Their impact is completely different.</p><p>Rate alone does not describe pressure.</p><p>For expensive work, we often care about how many operations are executing simultaneously.</p><p>ASP.NET Core&#8217;s current rate-limiting infrastructure includes a concurrency limiter that can restrict the number of simultaneous requests and optionally bound the number waiting for permits. </p><p>For example:</p><pre><code><code>using System.Threading.RateLimiting;

builder.Services.AddRateLimiter(options =&gt;
{
    options.AddConcurrencyLimiter(
        "expensive",
        limiter =&gt;
        {
            limiter.PermitLimit = 20;
            limiter.QueueLimit = 10;
            limiter.QueueProcessingOrder =
                QueueProcessingOrder.OldestFirst;
        });
});</code></code></pre><p>Then:</p><pre><code><code>app.UseRateLimiter();

app.MapPost("/reports", GenerateReport)
   .RequireRateLimiting("expensive");</code></code></pre><p>Now only 20 requests can perform that expensive work concurrently.</p><p>Another 10 may wait.</p><p>Beyond that, requests are rejected.</p><p>That rejection is not necessarily a failure of the architecture.</p><p>It may be exactly what keeps the architecture alive.</p><h2>Rate Limiting and Backpressure Are Related, but Different</h2><p>Rate limiting asks:</p><p><strong>How much work may arrive over a period of time?</strong></p><p>Backpressure asks:</p><p><strong>What should happen when work arrives faster than we can safely process it?</strong></p><p>Rate limiting is therefore one tool for implementing overload protection.</p><p>ASP.NET Core supports several rate-limiting approaches, including fixed-window, sliding-window, token-bucket, and concurrency limiters. Policies can be global or attached to selected endpoints. Microsoft recommends load testing rate-limiting configurations before production deployment. </p><p>The correct limiter depends on what we are protecting.</p><h2>Different Endpoints Need Different Limits</h2><p>A common mistake is setting one global number for the entire API.</p><p>Consider:</p><pre><code><code>GET /products
POST /orders
POST /reports
GET /health</code></code></pre><p>These endpoints do dramatically different amounts of work.</p><p>A cached product request may cost almost nothing.</p><p>Generating a complex report might use significant CPU, memory, database time, and storage.</p><p>Treating them equally wastes capacity.</p><p>Instead, policies should reflect resource cost.</p><pre><code><code>Products &#8594; generous limit
Orders   &#8594; moderate limit
Reports  &#8594; strict concurrency limit
Health   &#8594; lightweight separate path</code></code></pre><p>Backpressure becomes much more effective when it understands workload classes.</p><h2>Protect Downstream Dependencies</h2><p>Imagine:</p><pre><code><code>ASP.NET Core API
       &#8595;
Payment Provider</code></code></pre><p>Your <a href="https://www.asptoday.com/p/exploring-minimal-apis-in-aspnet">API</a> receives 2,000 requests per second.</p><p>The provider safely handles 200.</p><p>Without outbound control, your application becomes an amplifier.</p><p>It accepts huge amounts of work and sends the overload downstream.</p><p>A better architecture deliberately limits calls to the dependency.</p><p>.NET&#8217;s current resilience libraries support strategies including rate limiting and concurrency limiting as part of resilience pipelines. Microsoft distinguishes inbound rate control from outbound concurrency limiting, which can protect dependencies from excessive parallel work. </p><p>Conceptually:</p><pre><code><code>2,000 incoming
      &#8595;
ASP.NET Core
      &#8595;
Concurrency Control
      &#8595;
200 safe operations
      &#8595;
Dependency</code></code></pre><p>Protecting your own service while destroying the next service is not resilience.</p><h2>Bounded Channels</h2><p>For in-process producer-consumer workloads, <code>System.Threading.Channels</code> gives us an excellent backpressure primitive.</p><p>Instead of:</p><pre><code><code>Channel.CreateUnbounded&lt;Job&gt;();</code></code></pre><p>we can create:</p><pre><code><code>var channel = Channel.CreateBounded&lt;Job&gt;(
    new BoundedChannelOptions(100)
    {
        FullMode = BoundedChannelFullMode.Wait
    });</code></code></pre><p>The queue can contain at most 100 items.</p><p>When it fills, producers do not continue adding unlimited work. With <code>Wait</code>, asynchronous writes wait until capacity becomes available. Microsoft explicitly describes this situation as the writer experiencing backpressure when producers outrun consumers. </p><p>That small difference changes the stability characteristics of the entire system.</p><h2>A Practical Background Queue</h2><p>Let&#8217;s create a simple queue.</p><pre><code><code>public sealed record ProcessingJob(
    Guid Id,
    string FileName);

public sealed class JobQueue
{
    private readonly Channel&lt;ProcessingJob&gt; _channel;

    public JobQueue()
    {
        _channel =
            Channel.CreateBounded&lt;ProcessingJob&gt;(
                new BoundedChannelOptions(100)
                {
                    FullMode =
                        BoundedChannelFullMode.Wait,
                    SingleReader = true,
                    SingleWriter = false
                });
    }

    public ValueTask QueueAsync(
        ProcessingJob job,
        CancellationToken cancellationToken)
    {
        return _channel.Writer.WriteAsync(
            job,
            cancellationToken);
    }

    public IAsyncEnumerable&lt;ProcessingJob&gt;
        ReadAllAsync(
            CancellationToken cancellationToken)
    {
        return _channel.Reader.ReadAllAsync(
            cancellationToken);
    }
}</code></code></pre><p>A worker consumes the queue:</p><pre><code><code>public sealed class ProcessingWorker : BackgroundService
{
    private readonly JobQueue _queue;

    public ProcessingWorker(JobQueue queue)
    {
        _queue = queue;
    }

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        await foreach (
            var job in _queue.ReadAllAsync(stoppingToken))
        {
            await ProcessAsync(job, stoppingToken);
        }
    }

    private Task ProcessAsync(
        ProcessingJob job,
        CancellationToken cancellationToken)
    {
        // Perform the real work.
        return Task.CompletedTask;
    }
}</code></code></pre><p>Now the producer and consumer have an explicit capacity relationship.</p><h2>What Does &#8220;Wait&#8221; Really Mean?</h2><p>Suppose the queue contains 100 items.</p><p>Another producer calls:</p><pre><code><code>await queue.QueueAsync(job, cancellationToken);</code></code></pre><p>With <code>BoundedChannelFullMode.Wait</code>, that write waits asynchronously until space is available. </p><p>This propagates pressure backward.</p><pre><code><code>Worker slow
    &#8595;
Queue fills
    &#8595;
Producer waits
    &#8595;
Request slows</code></code></pre><p>That may sound undesirable.</p><p>But compare it with:</p><pre><code><code>Worker slow
    &#8595;
Queue grows forever
    &#8595;
Memory rises
    &#8595;
GC pressure rises
    &#8595;
Everything slows
    &#8595;
Process dies</code></code></pre><p>Waiting can be healthier than pretending everything is fine.</p><h2>Waiting Forever Is Not Backpressure</h2><p>There is another trap.</p><p>Suppose the queue is full and an HTTP request waits 15 minutes for capacity.</p><p>Technically, we bounded the queue.</p><p>Operationally, the user experience is terrible.</p><p>Backpressure needs limits at multiple levels.</p><p>For example:</p><pre><code><code>using var timeout =
    CancellationTokenSource.CreateLinkedTokenSource(
        httpContext.RequestAborted);

timeout.CancelAfter(TimeSpan.FromSeconds(2));

try
{
    await queue.QueueAsync(job, timeout.Token);

    return Results.Accepted();
}
catch (OperationCanceledException)
{
    return Results.StatusCode(
        StatusCodes.Status503ServiceUnavailable);
}</code></code></pre><p>Now the system gives the queue a short opportunity to recover.</p><p>If it cannot, we stop waiting.</p><h2>Sometimes Dropping Work Is Correct</h2><p>Not every piece of work deserves to wait.</p><p><code>BoundedChannelFullMode</code> supports several behaviors when capacity is exhausted:</p><pre><code><code>Wait
DropNewest
DropOldest
DropWrite</code></code></pre><p>Microsoft&#8217;s current Channels documentation defines these modes explicitly. <code>Wait</code> blocks asynchronous producers until capacity becomes available, while the drop modes discard particular items when the channel is full. </p><p>Which one should we use?</p><p>It depends entirely on what the data means.</p><h2>Never Drop Business-Critical Commands Casually</h2><p>Imagine the queue contains:</p><pre><code><code>ChargeCustomer
ShipOrder
CancelSubscription</code></code></pre><p>Dropping one silently is unacceptable.</p><p>For durable business operations, we may need a persistent message broker, durable queue, or transactional design instead of an in-memory channel.</p><p>Now imagine the queue contains:</p><pre><code><code>Refresh dashboard temperature
Refresh dashboard temperature
Refresh dashboard temperature</code></code></pre><p>If the dashboard only needs the latest reading, keeping stale updates may be pointless.</p><p>Dropping old telemetry can be completely reasonable.</p><p>Backpressure is not just a technical decision.</p><p>It is a <strong>business semantics decision</strong>.</p><h2>Freshness Can Matter More Than Completeness</h2><p>Consider a live monitoring dashboard.</p><p>Updates arrive:</p><pre><code><code>10:01:01 CPU 40%
10:01:02 CPU 42%
10:01:03 CPU 44%
...
10:01:30 CPU 91%</code></code></pre><p>If the consumer falls behind, displaying every historical intermediate value may mean the user sees conditions from 30 seconds ago.</p><p>Dropping older updates could be better.</p><p>For financial transactions, that would be disastrous.</p><p>For live telemetry, it might be exactly right.</p><p>Ask:</p><p><strong>Does the consumer need every event, or does it need the latest state?</strong></p><p>The answer changes the backpressure policy.</p><h2>Load Shedding</h2><p>Sometimes the safest response to overload is simply:</p><p><strong>No.</strong></p><p>This is load shedding.</p><p>Instead of accepting work we are unlikely to complete successfully, we reject it early.</p><p>For an HTTP API, that might mean returning an appropriate overload response such as <code>429 Too Many Requests</code> for rate-limited traffic or, depending on the failure mode and API contract, <code>503 Service Unavailable</code>.</p><p>ASP.NET Core&#8217;s rate-limiting middleware provides rejection handling and can expose retry information for limiter algorithms that can estimate when permits will become available. </p><p>Early rejection protects resources.</p><p>Late failure wastes them.</p><h2>Reject Before Expensive Work Begins</h2><p>Consider this sequence:</p><pre><code><code>Authenticate
&#8595;
Load 50 MB payload
&#8595;
Query database
&#8595;
Allocate large objects
&#8595;
Discover system overloaded
&#8595;
Reject</code></code></pre><p>We have already spent substantial resources.</p><p>Where possible, overload controls should operate before expensive work begins.</p><pre><code><code>Request
&#8595;
Cheap validation
&#8595;
Capacity check
&#8595;
Accepted?
   &#9500;&#9472;&#9472; No &#8594; Reject
   &#9492;&#9472;&#9472; Yes &#8594; Expensive work</code></code></pre><p>This is one reason middleware and endpoint-level limiting can be effective.</p><h2>Queueing Has a Latency Cost</h2><p>Suppose an operation normally takes:</p><pre><code><code>200 ms</code></code></pre><p>We allow 1,000 requests to queue.</p><p>Under overload, the 1,000th request may wait far longer than 200 milliseconds before processing even begins.</p><p>The service may technically be completing every request.</p><p>But the latency distribution becomes terrible.</p><p>This is why queue size should not be chosen simply based on available memory.</p><p>Ask:</p><p><strong>How long are we willing to make a user wait?</strong></p><p>A useful queue capacity follows from acceptable waiting time and processing throughput.</p><h2>Little&#8217;s Law Gives Us a Useful Mental Model</h2><p>Queueing theory provides a useful relationship:</p><pre><code><code>Items in system
&#8776;
Arrival rate &#215; Time in system</code></code></pre><p>If throughput is 100 jobs per second and we allow 10 seconds of waiting, hundreds or thousands of operations can accumulate depending on the workload.</p><p>The exact mathematics can become sophisticated, but the practical lesson is simple:</p><p><strong>Large queues create large latency.</strong></p><p>A queue of 100,000 is not automatically safer than a queue of 1,000.</p><p>It may merely hide overload longer.</p><h2>Cancellation Is Part of Backpressure</h2><p>Suppose a client disconnects while an expensive request is still running.</p><p>If the result is no longer useful, continuing the work wastes capacity.</p><p>ASP.NET Core exposes request cancellation through:</p><pre><code><code>HttpContext.RequestAborted</code></code></pre><p>Pass cancellation tokens through the application.</p><pre><code><code>app.MapGet(
    "/report",
    async (
        HttpContext context,
        ReportService reports) =&gt;
    {
        return await reports.GenerateAsync(
            context.RequestAborted);
    });</code></code></pre><p>Then continue passing the token to operations that support cancellation.</p><pre><code><code>Client leaves
     &#8595;
Request cancelled
     &#8595;
Database/API work cancelled where supported
     &#8595;
Capacity becomes available</code></code></pre><p>Cancellation is not merely a convenience.</p><p>During overload, it can reclaim scarce resources.</p><h2>Timeouts Also Release Capacity</h2><p>A downstream call that normally takes 100 milliseconds begins taking 30 seconds.</p><p>Without a timeout, requests pile up.</p><pre><code><code>Request 1 waiting
Request 2 waiting
Request 3 waiting
...
Request 5,000 waiting</code></code></pre><p>Timeouts establish a limit on how long resources can remain committed to work that is no longer useful.</p><p>This connects directly with the resilience patterns we covered earlier.</p><p>Retries, however, require special caution.</p><h2>Retries Can Make Overload Worse</h2><p>Suppose a dependency is overloaded.</p><p>Requests fail.</p><p>Every caller retries three times.</p><p>Now:</p><pre><code><code>1,000 original requests</code></code></pre><p>can become thousands of attempts.</p><p>The system is already struggling, and retries generate even more work.</p><p>This is sometimes called a retry storm.</p><p>Our earlier <strong><a href="https://www.asptoday.com/p/advanced-retry-strategies-in-aspnet">Advanced Retry Strategies</a></strong> article discussed exponential backoff and jitter. During overload, those ideas become especially important.</p><p>Retries should be:</p><ul><li><p>Limited<br></p></li><li><p>Delayed<br></p></li><li><p>Used only for appropriate transient failures<br></p></li><li><p>Combined with cancellation and circuit breaking<br></p></li></ul><p>Resilience patterns must cooperate.</p><p>A retry policy that ignores system capacity can reduce resilience instead of improving it.</p><h2>Graceful Degradation</h2><p>Rejecting requests is not our only option.</p><p>Sometimes we can make requests cheaper.</p><p>Imagine an e-commerce homepage normally includes:</p><pre><code><code>Product catalog
Personal recommendations
Live inventory
Reviews
Recently viewed
Promotions</code></code></pre><p>During extreme load, perhaps recommendations are temporarily unavailable.</p><p>The application can still return:</p><pre><code><code>Product catalog
Live inventory
Promotions</code></code></pre><p>The experience is reduced, but useful.</p><p>This is graceful degradation.</p><h2>Define Critical and Optional Work</h2><p>A useful architecture explicitly distinguishes:</p><pre><code><code>Critical
Important
Optional</code></code></pre><p>For checkout:</p><pre><code><code>Payment              Critical
Inventory reservation Critical
Recommendation update Optional
Analytics event        Optional</code></code></pre><p>When resources become scarce, optional work should not compete equally with payment processing.</p><p>That might mean:</p><ul><li><p>Separate queues<br></p></li><li><p>Separate concurrency limits<br></p></li><li><p>Lower-priority processing<br></p></li><li><p>Dropping non-critical telemetry<br></p></li><li><p>Temporarily disabling expensive enhancements<br></p></li></ul><p>A system that knows what matters most can degrade intelligently.</p><h2>Bulkheads Prevent One Workload from Consuming Everything</h2><p>Imagine two operations sharing the same application:</p><pre><code><code>Checkout
Reporting</code></code></pre><p>Reporting becomes extremely busy.</p><p>If both workloads share every resource without limits, reporting may consume all available capacity.</p><p>Checkout slows down.</p><p>A bulkhead-style design isolates capacity.</p><pre><code><code>Checkout
   &#8595;
Dedicated capacity

Reporting
   &#8595;
Limited capacity</code></code></pre><p>.NET&#8217;s resilience tooling includes rate-limiting and concurrency-limiting strategies that can help constrain workloads and dependencies. </p><p>The principle is similar to watertight compartments on a ship.</p><p>One flooded compartment should not sink everything.</p><h2>Backpressure Across Service Boundaries</h2><p>Now imagine:</p><pre><code><code>API
 &#8595;
Orders
 &#8595;
Payments
 &#8595;
Fraud
 &#8595;
External Provider</code></code></pre><p>Fraud slows down.</p><p>Payments begins waiting.</p><p>Orders begins waiting.</p><p>The API begins waiting.</p><p>Pressure travels backward through the dependency chain.</p><p>That is actually useful information.</p><p>The dangerous alternative is allowing every layer to create an enormous queue.</p><pre><code><code>API queue
   &#8595;
Orders queue
   &#8595;
Payments queue
   &#8595;
Fraud queue</code></code></pre><p>Now nobody knows where the real bottleneck is, and requests may spend minutes moving between queues.</p><p>Distributed systems need an intentional overload strategy across boundaries.</p><h2>Backpressure and Message Brokers</h2><p>Durable brokers such as <a href="https://www.asptoday.com/p/implementing-event-driven-architecture">RabbitMQ</a> or <a href="https://www.asptoday.com/p/aspnet-core-and-azure-service-bus">Azure Service Bus</a> can absorb larger bursts than an in-memory queue.</p><p>But the same principle remains.</p><p>Suppose producers generate:</p><pre><code><code>10 million messages/hour</code></code></pre><p>while consumers process:</p><pre><code><code>5 million messages/hour</code></code></pre><p>The backlog grows by five million every hour.</p><p>A durable queue prevents immediate memory exhaustion, but the system is still unsustainable.</p><p>Monitor:</p><pre><code><code>Queue depth
Oldest message age
Enqueue rate
Completion rate
Consumer utilization
Failure rate</code></code></pre><p><strong>Queue age</strong> is particularly important.</p><p>A queue of 10,000 messages may be fine if they are processed in five seconds.</p><p>A queue of 500 may be disastrous if the oldest message has been waiting six hours.</p><h2>Observability Makes Backpressure Measurable</h2><p>We should know when the system is applying pressure.</p><p>Useful metrics include:</p><pre><code><code>Current queue depth
Queue capacity
Rejected requests
Dropped items
Time waiting for capacity
Active operations
Available concurrency permits
Request latency
Dependency latency
Cancellation count</code></code></pre><p>ASP.NET Core&#8217;s rate-limiting middleware exposes built-in metrics that can help monitor how limiting affects application behavior. </p><p>These signals tell us not just that the application is overloaded, but <strong>how it is protecting itself</strong>.</p><h2>Watch Percentiles, Not Just Averages</h2><p>Suppose average latency is:</p><pre><code><code>250 ms</code></code></pre><p>That sounds healthy.</p><p>But perhaps:</p><pre><code><code>p50  = 100 ms
p95  = 900 ms
p99  = 8 seconds</code></code></pre><p>A small but meaningful group of users is having a terrible experience.</p><p>Queueing often appears in tail latency before averages look alarming.</p><p>Watch p95 and p99 latency alongside queue depth and rejection rates.</p><p>Those signals can reveal pressure much earlier.</p><h2>Autoscaling Is Not a Substitute for Backpressure</h2><p>A common response to overload is:</p><p><strong>Add more instances.</strong></p><p>Scaling is useful.</p><p>But it is not instantaneous, unlimited, or always effective.</p><p>Suppose ten API instances all connect to the same database.</p><p>Adding another 50 instances may simply generate more database traffic.</p><pre><code><code>More API instances
        &#8595;
More database connections
        &#8595;
Database overload</code></code></pre><p>Backpressure remains necessary even in autoscaled systems.</p><p>Scaling increases capacity.</p><p>Backpressure protects capacity.</p><p>We often need both.</p><h2>Backpressure at the Edge</h2><p>Our recent Edge Computing article introduced workloads operating close to users and devices.</p><p>Backpressure becomes especially important there because edge hardware often has fixed resources.</p><p>A small edge computer may have:</p><pre><code><code>4 CPU cores
8 GB RAM
Limited storage
Intermittent connectivity</code></code></pre><p>Thousands of sensors cannot be allowed to create unlimited local work.</p><p>The edge service might:</p><ul><li><p>Sample telemetry<br></p></li><li><p>Drop stale readings<br></p></li><li><p>Batch updates<br></p></li><li><p>Bound local queues<br></p></li><li><p>Prioritize alarms<br></p></li><li><p>Delay cloud synchronization<br></p></li></ul><p>Again, the policy depends on the meaning of the data.</p><p>A safety alarm must not be treated like a routine temperature sample.</p><h2>Backpressure and Dynamic Configuration</h2><p>Our previous article explored <strong>Configuration at Scale in ASP.NET Core</strong>.</p><p>Many overload controls are natural candidates for configuration:</p><pre><code><code>QueueCapacity
MaximumConcurrency
RequestTimeout
BatchSize</code></code></pre><p>But changing them dynamically requires care.</p><p>Suppose:</p><pre><code><code>QueueCapacity = 10,000</code></code></pre><p>changes to:</p><pre><code><code>QueueCapacity = 100</code></code></pre><p>while 8,000 items already exist.</p><p>What happens?</p><p>Configuration can change a number.</p><p>It cannot automatically define the transition semantics.</p><p>As we discussed in the previous article, dynamic settings need safe operational boundaries.</p><h2>A Complete Overload Strategy</h2><p>Let&#8217;s imagine a document-processing API.</p><p>Users upload documents that require expensive analysis.</p><p>A robust architecture might look like:</p><pre><code><code>Client
  &#8595;
ASP.NET Core
  &#8595;
Request Rate Limit
  &#8595;
Validation
  &#8595;
Bounded Job Queue
  &#8595;
Worker Pool
  &#8595;
Concurrency Limit
  &#8595;
Document Processor</code></code></pre><p>Now suppose traffic spikes.</p><p>First, the queue absorbs a small burst.</p><p>Then it reaches its configured capacity.</p><p>Producers begin waiting briefly.</p><p>If capacity does not return quickly enough, new work is rejected.</p><p>Workers remain at controlled concurrency.</p><p>The processor never receives more parallel work than it can safely handle.</p><p>Metrics show increasing queue depth and rejection rates.</p><p>Autoscaling may add consumers if infrastructure allows it.</p><p>The system becomes slower or rejects some work.</p><p>But it remains alive.</p><p>That is successful overload handling.</p><h2>What Failure Looks Like Without Backpressure</h2><p>Now remove the limits.</p><pre><code><code>Traffic spike
    &#8595;
Unlimited requests accepted
    &#8595;
Unlimited jobs queued
    &#8595;
Memory increases
    &#8595;
Workers increase concurrency
    &#8595;
Database connections exhausted
    &#8595;
Latency increases
    &#8595;
Requests time out
    &#8595;
Clients retry
    &#8595;
More traffic
    &#8595;
Application collapses</code></code></pre><p>Notice the feedback loop.</p><p>Slow systems create retries.</p><p>Retries create additional load.</p><p>Additional load makes systems slower.</p><p>Backpressure breaks that loop before it becomes catastrophic.</p><h2>What Good Overload Looks Like</h2><p>This sounds strange, but a well-designed overloaded system may intentionally return errors.</p><p>Imagine:</p><pre><code><code>95% requests succeed normally
5% rejected quickly</code></code></pre><p>Compare that with:</p><pre><code><code>100% requests accepted
100% become extremely slow
70% eventually time out</code></code></pre><p>The first system may appear less friendly because it says &#8220;no.&#8221;</p><p>In reality, it protects the majority of users.</p><p>Fast rejection is often better than false acceptance.</p><h2>Choosing the Right Strategy</h2><p>There is no universal backpressure policy.</p><p>For each workload, ask:</p><p><strong>Can the producer wait?</strong></p><p>If yes, bounded waiting may work.</p><p><strong>Can the work be dropped?</strong></p><p>If yes, choose what can safely be discarded.</p><p><strong>Must the work eventually complete?</strong></p><p>Use durable storage or messaging rather than relying solely on memory.</p><p><strong>Is freshness more important than completeness?</strong></p><p>Consider dropping stale updates.</p><p><strong>Is the dependency concurrency-sensitive?</strong></p><p>Limit simultaneous operations.</p><p><strong>Can functionality degrade?</strong></p><p>Disable or simplify optional work.</p><p><strong>Can the caller retry later?</strong></p><p>Reject quickly and communicate that clearly.</p><p>These are architectural decisions, not just framework settings.</p><h2>Test the Breaking Point</h2><p>You cannot design overload protection entirely from assumptions.</p><p>Load testing should answer questions such as:</p><pre><code><code>At what throughput does latency rise sharply?

Which dependency saturates first?

How large can queues become?

When do requests begin timing out?

Does memory remain stable?

Does rejection happen early enough?

Does the application recover when load falls?</code></code></pre><p>Microsoft specifically recommends stress testing ASP.NET Core rate-limiting policies before deploying them to production. </p><p>That last question is especially important.</p><p>A resilient system should not merely survive overload.</p><p>It should <strong>recover cleanly afterward</strong>.</p><h2>Recovery Matters</h2><p>Suppose traffic spikes for five minutes and then returns to normal.</p><p>If the application accepted a massive backlog during those five minutes, it may remain overloaded for another hour.</p><p>The incident continues after the traffic spike ends.</p><p>With bounded queues and controlled admission:</p><pre><code><code>Traffic spike
    &#8595;
Capacity reached
    &#8595;
Excess rejected
    &#8595;
Traffic normalizes
    &#8595;
Small backlog clears
    &#8595;
Normal operation resumes</code></code></pre><p>Bounded systems recover more predictably because the amount of accumulated work is itself bounded.</p><h2>Do Not Optimize for Zero Rejections</h2><p>A production system that never rejects work may simply have enormous unused capacity.</p><p>Or it may be accepting work it cannot complete.</p><p>The goal is not:</p><pre><code><code>Reject nothing</code></code></pre><p>The goal is:</p><pre><code><code>Provide the best useful service
within safe operating limits</code></code></pre><p>During normal conditions, users should rarely encounter backpressure.</p><p>During abnormal conditions, backpressure should prevent a localized capacity problem from becoming a total outage.</p><h2>How This Fits Our ASP.NET Core Journey</h2><p>Our recent articles have been moving steadily from building individual ASP.NET Core features toward operating entire production systems.</p><p><strong>.NET Aspire</strong> showed us how distributed resources can be orchestrated, discovered, and observed together.</p><p><strong>Configuration at Scale</strong> showed us how operational behavior can change safely without redeploying every service.</p><p>Backpressure introduces another essential production concern.</p><p>Even a perfectly orchestrated, correctly configured application has finite capacity.</p><p>Eventually, some resource reaches its limit.</p><p>At that moment, the architecture needs to know what to do.</p><p>Wait?</p><p>Queue?</p><p>Reject?</p><p>Drop?</p><p>Degrade?</p><p>Scale?</p><p>The worst answer is to have no answer at all.</p><h2>Coming Next</h2><p>In the next article, we&#8217;ll explore <strong>Graceful Shutdown and Application Lifecycle in ASP.NET Core: Deploy Without Dropping Work</strong>.</p><p>Backpressure protects an application while it is running under load. But production systems also need to stop safely.</p><p>We&#8217;ll examine what happens to active HTTP requests, background jobs, queues, database operations, and external calls when an ASP.NET Core instance receives a shutdown signal, and how cancellation tokens, readiness changes, draining, shutdown timeouts, and application lifecycle hooks can help deployments complete without silently losing work.</p><h2>Final Thoughts</h2><p>Backpressure is ultimately about accepting that every system has limits.</p><p>CPU is finite.</p><p>Memory is finite.</p><p>Database connections are finite.</p><p>External APIs have limits.</p><p>Workers can process only so much work at once.</p><p>Ignoring those limits does not make them disappear. It simply delays the point where they become visible, usually until the application is already in trouble.</p><p>ASP.NET Core and modern .NET give us strong building blocks for handling overload deliberately. Rate limiting can control incoming traffic. Concurrency limiting can protect expensive resources. Bounded channels can prevent in-process queues from growing indefinitely. Cancellation and timeouts can release capacity that is no longer useful. Load shedding and graceful degradation can preserve critical functionality when full service is impossible.</p><p>The most important change, however, is architectural.</p><p>Do not ask only:</p><p><strong>How much traffic can our application handle?</strong></p><p>Ask:</p><p><strong>What exactly will our application do when traffic exceeds that amount?</strong></p><p>If the answer is deliberate, measurable, and tested, overload becomes something the system can manage rather than something that unexpectedly takes it down.</p><h2>Subscribe Now</h2><p>Enjoying the series? <a href="https://www.asptoday.com/">Subscribe to </a><strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, advanced architecture deep dives, and production-focused .NET engineering guides. Join our Substack Chat to discuss scalability, resilience, distributed systems, and the challenges of keeping modern ASP.NET Core applications reliable under real-world load. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Configuration at Scale in ASP.NET Core: Dynamic Settings, Validation, and Safe Reloading]]></title><description><![CDATA[Build safer ASP.NET Core configuration with validation, dynamic settings, and controlled reloads. #ASPToday #aspnetcore #dotnet #configuration]]></description><link>https://www.asptoday.com/p/configuration-at-scale-in-aspnet</link><guid isPermaLink="false">https://www.asptoday.com/p/configuration-at-scale-in-aspnet</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 25 Aug 2026 15:03:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!oMYG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Configuration starts simply in <a href="https://asptoday.substack.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a>. We add a few values to <code>appsettings.json</code>, inject <code>IConfiguration</code>, and move on. But as applications grow across services, environments, regions, and deployment platforms, configuration becomes part of the architecture itself. A single incorrect setting can disable a feature, overwhelm a dependency, or prevent an application from starting. Modern ASP.NET Core gives us powerful tools for strongly typed settings, validation, environment-specific configuration, and runtime updates. The challenge is using those tools safely. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oMYG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oMYG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!oMYG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!oMYG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!oMYG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oMYG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2671932,&quot;alt&quot;:&quot;Miniature engineers adjust a giant watch mechanism, symbolizing safe configuration changes. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/212511659?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Miniature engineers adjust a giant watch mechanism, symbolizing safe configuration changes. " title="Miniature engineers adjust a giant watch mechanism, symbolizing safe configuration changes. " srcset="https://substackcdn.com/image/fetch/$s_!oMYG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!oMYG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!oMYG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!oMYG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38323388-6443-4be0-a52b-d6c8705b97b0_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we&#8217;ll build a <a href="https://asptoday.substack.com/p/building-scalable-applications-with">scalable</a> configuration approach that validates settings early, supports controlled changes, and allows applications to react to updated configuration without introducing unpredictable production behavior.</p><h3>Configuration Looks Easy Until the Application Grows</h3><p>Consider a small ASP.NET Core application.</p><p>Its configuration might contain:</p><pre><code><code>{
  "Catalog": {
    "PageSize": 25
  }
}</code></code></pre><p>Reading the value is straightforward.</p><pre><code><code>var pageSize =
    builder.Configuration.GetValue&lt;int&gt;("Catalog:PageSize");</code></code></pre><p>For a small application, this works perfectly well.</p><p>Now imagine the same system two years later.</p><p>It contains:</p><ul><li><p>Multiple ASP.NET Core services</p></li><li><p>Development, staging, and production environments</p></li><li><p>Several geographic regions</p></li><li><p>Feature settings</p></li><li><p>Retry policies</p></li><li><p>API endpoints</p></li><li><p>Cache expiration rules</p></li><li><p>Queue settings</p></li><li><p>Connection information</p></li><li><p>Operational limits</p></li></ul><p>Suddenly, configuration is no longer just a file.</p><p>It is a distributed operational dependency.</p><h3>Why Configuration Deserves Architectural Attention</h3><p>A configuration change can alter application behavior without changing application code.</p><p>That is powerful.</p><p>It is also dangerous.</p><p>Imagine someone changes:</p><pre><code><code>{
  "Payments": {
    "TimeoutSeconds": 30
  }
}</code></code></pre><p>to:</p><pre><code><code>{
  "Payments": {
    "TimeoutSeconds": 3000
  }
}</code></code></pre><p>The application still compiles.</p><p>The deployment may succeed.</p><p>But requests could now remain active far longer than intended.</p><p>Configuration errors are particularly difficult because they often look like application failures.</p><p>The code has not changed, yet production behavior has.</p><p>That is why mature systems treat configuration with many of the same disciplines applied to code.</p><h3>Understanding ASP.NET Core Configuration Providers</h3><p>ASP.NET Core builds configuration from one or more configuration providers.</p><p>Common sources include:</p><ul><li><p>JSON files</p></li><li><p>Environment variables</p></li><li><p>Command-line arguments</p></li><li><p>User secrets during development</p></li><li><p>In-memory configuration</p></li><li><p>External configuration systems</p></li></ul><p>These sources are combined into a single configuration model.</p><p>A simplified setup might look like:</p><pre><code><code>var builder = WebApplication.CreateBuilder(args);

builder.Configuration
    .AddJsonFile(
        "appsettings.json",
        optional: false,
        reloadOnChange: true)
    .AddJsonFile(
        $"appsettings.{builder.Environment.EnvironmentName}.json",
        optional: true,
        reloadOnChange: true)
    .AddEnvironmentVariables();</code></code></pre><p>The order matters because later providers can override values supplied by earlier providers.</p><p>That gives us a useful layering model.</p><pre><code><code>Base Configuration
        &#8595;
Environment Configuration
        &#8595;
Environment Variables
        &#8595;
Final Effective Configuration</code></code></pre><p>The application consumes the final result.</p><h3>Configuration Precedence Matters</h3><p>Suppose <code>appsettings.json</code> contains:</p><pre><code><code>{
  "Catalog": {
    "PageSize": 20
  }
}</code></code></pre><p>Production configuration contains:</p><pre><code><code>{
  "Catalog": {
    "PageSize": 50
  }
}</code></code></pre><p>And an environment variable supplies:</p><pre><code><code>Catalog__PageSize=100</code></code></pre><p>The effective value may become:</p><pre><code><code>100</code></code></pre><p>That flexibility is useful for deployments, but it can also create confusion.</p><p>When debugging configuration problems, the important question is often not:</p><p><strong>What does </strong><code>appsettings.json</code><strong> say?</strong></p><p>It is:</p><p><strong>Which provider supplied the value the application is actually using?</strong></p><h3>Stop Passing IConfiguration Everywhere</h3><p>A common early-stage pattern looks like this:</p><pre><code><code>public class CatalogService
{
    private readonly IConfiguration _configuration;

    public CatalogService(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public int GetPageSize()
    {
        return _configuration
            .GetValue&lt;int&gt;("Catalog:PageSize");
    }
}</code></code></pre><p>This works.</p><p>But it has several weaknesses.</p><p>The service depends on string-based configuration paths.</p><p>Typos become runtime problems.</p><p>The expected configuration structure is hidden inside application logic.</p><p>Validation is difficult.</p><p>Testing requires constructing configuration.</p><p>For larger applications, the Options pattern is usually a better approach.</p><h3>Strongly Typed Configuration</h3><p>Let&#8217;s define configuration as a class.</p><pre><code><code>public sealed class CatalogOptions
{
    public const string SectionName = "Catalog";

    public int PageSize { get; init; }

    public int CacheMinutes { get; init; }
}</code></code></pre><p>Our configuration becomes:</p><pre><code><code>{
  "Catalog": {
    "PageSize": 50,
    "CacheMinutes": 10
  }
}</code></code></pre><p>Then register it:</p><pre><code><code>builder.Services.Configure&lt;CatalogOptions&gt;(
    builder.Configuration.GetSection(
        CatalogOptions.SectionName));</code></code></pre><p>Now application code works with normal C# properties rather than configuration strings.</p><pre><code><code>public class CatalogService
{
    private readonly CatalogOptions _options;

    public CatalogService(
        IOptions&lt;CatalogOptions&gt; options)
    {
        _options = options.Value;
    }
}</code></code></pre><p>This gives configuration a clear contract.</p><h3>Why Strong Typing Matters at Scale</h3><p>Compare these:</p><pre><code><code>configuration["Payments:Retry:MaximumAttempts"]</code></code></pre><p>and:</p><pre><code><code>options.MaximumAttempts</code></code></pre><p>The second approach is easier to understand, test, refactor, and validate.</p><p>More importantly, it tells developers exactly which settings a component expects.</p><p>Configuration stops being an invisible collection of strings and becomes part of the application&#8217;s type system.</p><h3>Configuration Must Be Validated</h3><p>Strong typing does not automatically make configuration correct.</p><p>This is perfectly valid C#:</p><pre><code><code>{
  "Catalog": {
    "PageSize": -500,
    "CacheMinutes": 0
  }
}</code></code></pre><p>The values bind successfully.</p><p>But they make no business sense.</p><p>ASP.NET Core&#8217;s options infrastructure allows us to validate configuration.</p><p>For example:</p><pre><code><code>builder.Services
    .AddOptions&lt;CatalogOptions&gt;()
    .Bind(
        builder.Configuration.GetSection(
            CatalogOptions.SectionName))
    .Validate(
        options =&gt; options.PageSize &gt; 0,
        "PageSize must be greater than zero.")
    .Validate(
        options =&gt; options.CacheMinutes &gt; 0,
        "CacheMinutes must be greater than zero.");</code></code></pre><p>Now invalid values can be detected deliberately rather than producing strange behavior later.</p><h3>Fail Fast with ValidateOnStart</h3><p>There is another important question.</p><p><strong>When should invalid configuration be discovered?</strong></p><p>Suppose a payment configuration is invalid.</p><p>One option is to discover the problem when the first customer attempts a payment.</p><p>That is far too late.</p><p>For critical configuration, we usually want the application to fail during startup.</p><pre><code><code>builder.Services
    .AddOptions&lt;PaymentOptions&gt;()
    .Bind(
        builder.Configuration.GetSection("Payments"))
    .Validate(
        options =&gt; options.TimeoutSeconds &gt; 0,
        "Payment timeout must be positive.")
    .ValidateOnStart();</code></code></pre><p>Now configuration is checked when the application starts.</p><p>This is an important production principle:</p><p><strong>A service that cannot operate correctly should fail before it starts accepting traffic.</strong></p><h3>Data Annotation Validation</h3><p>For straightforward rules, data annotations can make option classes easy to understand.</p><pre><code><code>public sealed class PaymentOptions
{
    [Range(1, 120)]
    public int TimeoutSeconds { get; init; }

    [Range(0, 10)]
    public int MaximumRetries { get; init; }
}</code></code></pre><p>Then:</p><pre><code><code>builder.Services
    .AddOptions&lt;PaymentOptions&gt;()
    .BindConfiguration("Payments")
    .ValidateDataAnnotations()
    .ValidateOnStart();</code></code></pre><p>The rules now live directly beside the properties they protect.</p><h3>Cross-Property Validation</h3><p>Real configuration often has relationships between values.</p><p>Imagine:</p><pre><code><code>public sealed class UploadOptions
{
    public long WarningSizeBytes { get; init; }

    public long MaximumSizeBytes { get; init; }
}</code></code></pre><p>Both numbers could individually be positive while still being logically incorrect.</p><p>For example:</p><pre><code><code>WarningSize = 100 MB
MaximumSize = 50 MB</code></code></pre><p>We need a rule such as:</p><pre><code><code>.Validate(
    options =&gt;
        options.WarningSizeBytes &lt;
        options.MaximumSizeBytes,
    "WarningSizeBytes must be lower than MaximumSizeBytes.")</code></code></pre><p>This is why configuration validation should express <strong>operational rules</strong>, not merely type correctness.</p><h3>IOptions, IOptionsSnapshot, and IOptionsMonitor</h3><p>ASP.NET Core provides several ways to consume options, and understanding the differences becomes important when configuration can change.</p><p>The three common interfaces are:</p><pre><code><code>IOptions&lt;T&gt;
IOptionsSnapshot&lt;T&gt;
IOptionsMonitor&lt;T&gt;</code></code></pre><p>They sound similar, but they serve different purposes.</p><h3>IOptions&lt;T&gt;</h3><p><code>IOptions&lt;T&gt;</code> provides an options value that is effectively consumed as a stable value by the application.</p><p>It works well when configuration is not expected to change during the process lifetime.</p><pre><code><code>public CatalogService(
    IOptions&lt;CatalogOptions&gt; options)
{
    _options = options.Value;
}</code></code></pre><p>For many settings, this is exactly what we want.</p><p><a href="https://asptoday.substack.com/p/microservices-architecture-with-aspnet">Database architecture</a>, protocol choices, and other structural settings should not necessarily change while the application is running.</p><h3>IOptionsSnapshot&lt;T&gt;</h3><p><code>IOptionsSnapshot&lt;T&gt;</code> is scoped and recalculates options once per request scope.</p><p>This can be useful in request-based applications where updated configuration should become visible to subsequent requests.</p><pre><code><code>public CatalogController(
    IOptionsSnapshot&lt;CatalogOptions&gt; options)
{
    _options = options.Value;
}</code></code></pre><p>A request sees a consistent snapshot.</p><p>A later request can receive updated settings.</p><h3>IOptionsMonitor&lt;T&gt;</h3><p><code>IOptionsMonitor&lt;T&gt;</code> is designed for scenarios where an application needs access to the current option value and may need to react when that value changes.</p><pre><code><code>public class PricingService
{
    private readonly IOptionsMonitor&lt;PricingOptions&gt; _options;

    public PricingService(
        IOptionsMonitor&lt;PricingOptions&gt; options)
    {
        _options = options;
    }

    public decimal Calculate(decimal price)
    {
        var settings = _options.CurrentValue;

        return price * settings.Multiplier;
    }
}</code></code></pre><p>If the underlying configuration provider supports reloads, <code>CurrentValue</code> can reflect updated configuration.</p><h3>Reacting to Configuration Changes</h3><p><code>IOptionsMonitor&lt;T&gt;</code> also exposes change notifications.</p><pre><code><code>public class PricingConfigurationObserver
{
    private readonly IDisposable? _subscription;

    public PricingConfigurationObserver(
        IOptionsMonitor&lt;PricingOptions&gt; options,
        ILogger&lt;PricingConfigurationObserver&gt; logger)
    {
        _subscription = options.OnChange(updated =&gt;
        {
            logger.LogInformation(
                "Pricing configuration changed. Multiplier: {Multiplier}",
                updated.Multiplier);
        });
    }
}</code></code></pre><p>This looks extremely powerful.</p><p>Change a configuration value and the application reacts immediately.</p><p>But this is also where configuration becomes dangerous.</p><h3>Dynamic Configuration Is Not Automatically Better</h3><p>Imagine changing:</p><pre><code><code>MaximumConcurrentJobs = 100</code></code></pre><p>to:</p><pre><code><code>MaximumConcurrentJobs = 10</code></code></pre><p>while 75 jobs are already running.</p><p>What should happen?</p><p>Should 65 jobs be cancelled?</p><p>Should the new limit apply only to future work?</p><p>Should the application wait until concurrency naturally falls below 10?</p><p>The configuration system cannot answer those questions.</p><p>They are application semantics.</p><p>This leads to one of the most important rules in dynamic configuration:</p><p><strong>Being able to reload a setting does not mean the setting is safe to reload.</strong></p><h3>Classify Configuration by Behavior</h3><p>A useful production approach is to classify settings into three groups.</p><h4>Static Settings</h4><p>These require application restart.</p><p>Examples:</p><ul><li><p>Fundamental infrastructure choices</p></li><li><p>Certain hosting settings</p></li><li><p>Major dependency topology</p></li><li><p>Settings used only during service registration</p></li></ul><h4>Reloadable Settings</h4><p>These can change safely while the application is running.</p><p>Examples might include:</p><ul><li><p>Some timeout values</p></li><li><p>UI behavior</p></li><li><p>Operational thresholds</p></li><li><p>Non-critical feature behavior</p></li></ul><h4>Controlled Dynamic Settings</h4><p>These can change at runtime, but only through explicit application logic.</p><p>Examples include:</p><ul><li><p>Concurrency limits</p></li><li><p>Pricing rules</p></li><li><p>Traffic percentages</p></li><li><p>Resource thresholds</p></li></ul><p>This classification prevents teams from treating every configuration value as equally dynamic.</p><h3>Safe Reloading Requires Boundaries</h3><p>Suppose we have:</p><pre><code><code>public sealed class WorkerOptions
{
    public int BatchSize { get; init; }
}</code></code></pre><p>A background worker could read:</p><pre><code><code>var batchSize =
    _optionsMonitor.CurrentValue.BatchSize;</code></code></pre><p>before beginning each batch.</p><p>That gives us a natural boundary.</p><pre><code><code>Batch 1 uses 100
        &#8595;
Configuration changes to 50
        &#8595;
Batch 2 uses 50</code></code></pre><p>The current batch remains consistent.</p><p>The new value applies to future work.</p><p>That is far safer than changing behavior halfway through an operation.</p><h3>Configuration Should Be Immutable During an Operation</h3><p>Consider a request that performs several calculations.</p><p>If configuration can change halfway through the request, we could theoretically get:</p><pre><code><code>Step 1 &#8594; Tax rate 10%
Step 2 &#8594; configuration reload
Step 3 &#8594; Tax rate 12%</code></code></pre><p>One operation has now used two different business rules.</p><p>For important calculations, capture the required configuration once at the start of the operation.</p><p>Then use that snapshot consistently.</p><p>Dynamic configuration should change <strong>between operations</strong>, not unpredictably inside them.</p><h3>What Happens When Reloaded Configuration Is Invalid?</h3><p>This is where production systems need careful design.</p><p>Suppose:</p><pre><code><code>{
  "Worker": {
    "BatchSize": 100
  }
}</code></code></pre><p>changes to:</p><pre><code><code>{
  "Worker": {
    "BatchSize": -10
  }
}</code></code></pre><p>The new value is invalid.</p><p>A mature system should not blindly adopt it.</p><p>Validation needs to remain part of the options pipeline, and operationally you should also make configuration failures visible through logging and monitoring.</p><p>The goal is simple:</p><pre><code><code>New Configuration
       &#8595;
Validation
   /       \
Valid     Invalid
 &#8595;           &#8595;
Use it    Surface failure</code></code></pre><p>Never make &#8220;the file changed&#8221; equivalent to &#8220;the change is safe.&#8221;</p><h3>External Configuration Systems</h3><p>Once an organization runs many application instances, local JSON files become difficult to manage dynamically.</p><p>Imagine 50 instances of the same service.</p><p>Changing a JSON file manually on each server is obviously not a scalable strategy.</p><p>This is where centralized configuration providers can help.</p><p>Depending on the environment, organizations may use systems such as:</p><ul><li><p>Azure App Configuration</p></li><li><p>Cloud configuration services</p></li><li><p>Kubernetes configuration mechanisms</p></li><li><p>Dedicated internal configuration platforms</p></li></ul><p>The architectural pattern becomes:</p><pre><code><code>Central Configuration
        &#8595;
    Application A
    Application B
    Application C
    Application D</code></code></pre><p>Teams can manage settings centrally rather than maintaining independent copies everywhere.</p><h3>Configuration Is Not the Same as Secrets</h3><p>This distinction matters.</p><p>Configuration might contain:</p><pre><code><code>PageSize = 50
RetryCount = 3
FeatureMode = Standard</code></code></pre><p>Secrets include things such as:</p><pre><code><code>Database password
API credential
Private key
Signing secret</code></code></pre><p>Both may enter an application through configuration providers, but they should not necessarily be managed in the same place or with the same access policies.</p><p>Secrets deserve stronger controls, auditing, rotation, and restricted visibility.</p><p>Do not turn <code>appsettings.json</code> into a password vault.</p><h3>Environment Variables</h3><p>Environment variables remain extremely useful for containerized and cloud deployments.</p><p>Nested configuration keys use double underscores.</p><p>For example:</p><pre><code><code>Payments__TimeoutSeconds=20</code></code></pre><p>maps to:</p><pre><code><code>Payments:TimeoutSeconds</code></code></pre><p>This allows deployment platforms to override application defaults without modifying files inside the application package.</p><h3>Environment-Specific Files</h3><p>ASP.NET Core commonly uses files such as:</p><pre><code><code>appsettings.json
appsettings.Development.json
appsettings.Production.json</code></code></pre><p>This is useful for environment-specific defaults.</p><p>But avoid allowing these files to drift into completely different application configurations.</p><p>If production contains 40 settings that development does not even know exist, configuration becomes difficult to test.</p><p>The environments should differ in <strong>values</strong>, not unexpectedly in the fundamental shape of configuration.</p><h3>Validate Production-Like Configuration Before Production</h3><p>Configuration validation during application startup is valuable.</p><p>But discovering an invalid setting during the production deployment is still later than ideal.</p><p>A stronger delivery pipeline validates production configuration earlier.</p><p>Conceptually:</p><pre><code><code>Configuration Change
       &#8595;
Schema / Options Validation
       &#8595;
Automated Tests
       &#8595;
Staging
       &#8595;
Production</code></code></pre><p>Treat configuration changes as deployable changes.</p><p>They deserve review and testing.</p><h3>Configuration Versioning</h3><p>Imagine an incident begins at 14:32.</p><p>The code has been running successfully for three days.</p><p>What changed?</p><p>A mature configuration system should make it possible to answer:</p><pre><code><code>What changed?
Who changed it?
When?
From what value?
To what value?
Which services received it?</code></code></pre><p>This is why version history becomes important at scale.</p><p>Configuration without history makes incident investigation much harder.</p><h3>Rollback Matters</h3><p>If configuration can be changed quickly, it should also be possible to reverse the change quickly.</p><p>Suppose a new timeout causes failures.</p><p>A rollback path should look like:</p><pre><code><code>Version 41
   &#8595;
Version 42
   &#8595;
Problems detected
   &#8595;
Rollback
   &#8595;
Version 41</code></code></pre><p>The ability to restore a known-good configuration is often more valuable during an incident than the ability to edit individual values manually.</p><h3>Avoid Giant Global Configuration Objects</h3><p>Another common mistake is creating:</p><pre><code><code>public class ApplicationSettings
{
    // 150 unrelated properties
}</code></code></pre><p>Then every service depends on it.</p><p>This creates unnecessary coupling.</p><p>Instead, define configuration around capabilities.</p><pre><code><code>PaymentOptions
CatalogOptions
CacheOptions
ShippingOptions
NotificationOptions</code></code></pre><p>Each component receives only the configuration it needs.</p><p>This mirrors the same modularity principles we apply to application architecture.</p><h3>Names Matter</h3><p>Avoid settings such as:</p><pre><code><code>Timeout = 30
Limit = 100
Enabled = true</code></code></pre><p>Thirty what?</p><p>Seconds?</p><p>Milliseconds?</p><p>What limit?</p><p>What exactly is enabled?</p><p>Prefer explicit names:</p><pre><code><code>RequestTimeoutSeconds
MaximumConcurrentRequests
EnableAutomaticRetry</code></code></pre><p>Configuration is an interface between developers and operators.</p><p>Clarity matters.</p><h3>Do Not Use Configuration as a Database</h3><p>Once dynamic configuration becomes convenient, teams sometimes begin putting everything into it.</p><p>That is a mistake.</p><p>Configuration is excellent for controlling application behavior.</p><p>It is not a replacement for transactional business data.</p><p>A customer&#8217;s current account balance does not belong in configuration.</p><p>Neither does an order&#8217;s status.</p><p>Ask:</p><p><strong>Is this describing how the application should behave, or is this information the application owns?</strong></p><p>If it is business data, store it as business data.</p><h3>Feature Flags Are a Separate Concern</h3><p>Feature flags and configuration overlap, but they solve different problems.</p><p>Configuration usually describes operational behavior.</p><p>Feature flags control whether particular functionality is available, often for specific users, tenants, or rollout groups.</p><p>For example:</p><pre><code><code>RequestTimeoutSeconds = 20</code></code></pre><p>is configuration.</p><pre><code><code>EnableNewCheckoutForBetaCustomers = true</code></code></pre><p>is closer to feature management.</p><p>Keeping that distinction clear prevents configuration systems from becoming overloaded with rollout logic.</p><h3>Observing Configuration Changes</h3><p>Configuration changes should produce operational signals.</p><p>At minimum, important changes should be logged.</p><p>For example:</p><pre><code><code>logger.LogInformation(
    "Catalog configuration updated. PageSize={PageSize}",
    options.PageSize);</code></code></pre><p>Be careful not to log secrets.</p><p>For important systems, useful telemetry can include:</p><ul><li><p>Configuration reload count</p></li><li><p>Validation failures</p></li><li><p>Last successful refresh</p></li><li><p>Configuration version</p></li><li><p>Refresh latency</p></li></ul><p>Configuration is an operational dependency, so it deserves observability.</p><h3>Health Checks and Configuration</h3><p>Some configuration failures can also affect service health.</p><p>Suppose an application depends on centrally refreshed configuration and has not successfully refreshed for several hours.</p><p>Whether that should make the service unhealthy depends on the system.</p><p>Do not automatically mark a service unhealthy simply because a configuration provider is temporarily unavailable if the application can safely continue using its last known configuration.</p><p>This is an important resilience principle:</p><p><strong>A temporary inability to refresh configuration should not necessarily destroy an otherwise healthy application.</strong></p><h3>Last Known Good Configuration</h3><p>That leads to another useful pattern.</p><p>Imagine the configuration service becomes unavailable.</p><p>The application already has a valid configuration.</p><p>Should it stop working?</p><p>Often, no.</p><p>A resilient architecture can continue using the last known good configuration while reporting that refresh attempts are failing.</p><pre><code><code>Configuration Provider
        X
        &#9474;
Application
        &#9474;
        &#9492;&#9472;&#9472; Continue with last known good settings</code></code></pre><p>This prevents a configuration outage from automatically becoming an application outage.</p><p>Of course, some security-sensitive settings may require stricter behavior.</p><p>Again, the correct policy depends on what the setting controls.</p><h3>Configuration Across Multiple Instances</h3><p>Suppose we run ten instances of an Orders API.</p><p>A configuration value changes.</p><p>The instances may not all observe the update at exactly the same instant.</p><p>For a brief period:</p><pre><code><code>Instance 1 &#8594; Version 18
Instance 2 &#8594; Version 18
Instance 3 &#8594; Version 17
Instance 4 &#8594; Version 18</code></code></pre><p>Can the system tolerate that?</p><p>For many operational settings, yes.</p><p>For some business rules, absolutely not.</p><p>If every instance must change atomically, ordinary dynamic configuration may be the wrong mechanism.</p><p>This is another reason to distinguish operational settings from transactional business state.</p><h3>Configuration and .NET Aspire</h3><p>In our previous article, we explored how .NET Aspire helps describe and connect distributed applications.</p><p>Configuration fits naturally into that story.</p><p>Aspire can help applications receive connection information and resource references without requiring developers to manually coordinate endpoints across multiple projects.</p><p>The larger architectural principle remains the same:</p><p><strong>Applications should depend on named capabilities and structured configuration rather than scattered hardcoded infrastructure details.</strong></p><p>As distributed systems grow, this becomes increasingly important.</p><h3>A Practical Production Pattern</h3><p>Imagine an ASP.NET Core order-processing service.</p><p>It needs:</p><pre><code><code>OrderProcessingOptions
PaymentOptions
ShippingOptions
QueueOptions</code></code></pre><p>Each option type is:</p><ol><li><p>Strongly typed.</p></li><li><p>Bound to its own configuration section.</p></li><li><p>Validated.</p></li><li><p>Validated during startup when appropriate.</p></li><li><p>Classified as static or reloadable.</p></li><li><p>Monitored when dynamic changes are required.</p></li></ol><p>Critical settings fail startup if invalid.</p><p>Reloadable settings change only at safe operational boundaries.</p><p>Configuration changes are logged.</p><p>The configuration platform maintains version history.</p><p>Rollbacks are possible.</p><p>Secrets are managed separately.</p><p>This is no longer &#8220;reading settings.&#8221;</p><p>It is configuration engineering.</p><h3>A Real-World Scenario</h3><p>Imagine a large ticket-booking platform.</p><p>During normal traffic:</p><pre><code><code>MaximumSearchResults = 100
SearchTimeoutSeconds = 5
MaximumConcurrentSearches = 500
CacheMinutes = 10</code></code></pre><p>A major concert goes on sale.</p><p>Traffic suddenly increases dramatically.</p><p>Operations decides to reduce:</p><pre><code><code>MaximumSearchResults</code></code></pre><p>from:</p><pre><code><code>100</code></code></pre><p>to:</p><pre><code><code>50</code></code></pre><p>The goal is to reduce backend work per search.</p><p>A properly designed configuration system allows the change to be introduced without rebuilding the application.</p><p>New requests use the updated limit.</p><p>Existing requests continue using the configuration they started with.</p><p><a href="https://asptoday.substack.com/p/deep-dive-into-distributed-tracing">Telemetry</a> confirms the new configuration version has reached application instances.</p><p>Latency begins falling.</p><p>If the change causes unexpected behavior, operations rolls back to the previous version.</p><p>That is the value of dynamic configuration when it is designed correctly.</p><h3>The Configuration Maturity Ladder</h3><p>We can think of configuration maturity in stages.</p><pre><code><code>Level 1
Hardcoded values

        &#8595;

Level 2
appsettings.json

        &#8595;

Level 3
Environment-specific configuration

        &#8595;

Level 4
Strongly typed options

        &#8595;

Level 5
Validation

        &#8595;

Level 6
Centralized configuration

        &#8595;

Level 7
Safe dynamic reload

        &#8595;

Level 8
Versioning, auditing, rollback, and observability</code></code></pre><p>Not every application needs Level 8.</p><p>But as systems become more distributed and operationally important, configuration deserves increasingly deliberate engineering.</p><h3>Common Mistakes to Avoid</h3><p>The most common problems usually come from treating configuration as an afterthought.</p><p>Avoid:</p><ul><li><p>Hardcoded infrastructure addresses</p></li><li><p>Configuration strings scattered throughout application code</p></li><li><p>Missing validation</p></li><li><p>Secrets committed to configuration files</p></li><li><p>Reloading every setting dynamically</p></li><li><p>Changing values halfway through operations</p></li><li><p>Giant global settings classes</p></li><li><p>Unclear units and names</p></li><li><p>No version history</p></li><li><p>No rollback mechanism</p></li><li><p>Logging secret values</p></li><li><p>Assuming every instance reloads simultaneously</p></li></ul><p>Most of these mistakes are easy to avoid when configuration is treated as an explicit architectural concern.</p><h2>How This Fits Our ASP.NET Core Journey</h2><p>The previous article introduced .NET Aspire and showed how distributed applications can be modeled as connected resources.</p><p>That naturally leads to configuration.</p><p>Once applications span multiple services, environments, and infrastructure dependencies, every component needs reliable information about how it should behave.</p><p>The challenge is no longer simply loading that information.</p><p>The challenge becomes controlling its lifecycle.</p><p>We need to know whether configuration is valid, when it can change, how changes reach running services, what happens when the provider fails, and how we recover when a bad value reaches production.</p><p>Those questions become increasingly important as ASP.NET Core systems grow.</p><p>And they lead directly into our next architectural problem.</p><p>What happens when a perfectly configured application receives more work than it can possibly process?</p><p>That is where <strong>backpressure and overload protection</strong> enter the picture.</p><h2>Coming Next</h2><p>In the next article, we&#8217;ll explore <strong>Designing Backpressure in ASP.NET Core: Handling Overload Without Crashing Your System</strong>.</p><p>We&#8217;ll look at what happens when producers generate work faster than consumers can process it, why unlimited queues can quietly destroy application stability, and how bounded channels, concurrency limits, load shedding, cancellation, and graceful degradation help ASP.NET Core applications remain responsive under extreme load.</p><h2>Closing Thoughts</h2><p>Configuration begins as a convenience and eventually becomes part of the operational architecture of an application.</p><p>ASP.NET Core gives us a strong foundation through configuration providers, the Options pattern, validation, startup validation, snapshots, and monitoring. But the framework cannot decide which settings are safe to change while an application is running.</p><p>That responsibility belongs to us.</p><p>The most reliable systems distinguish static configuration from genuinely dynamic settings. They validate important values before accepting traffic, apply runtime changes at safe boundaries, preserve known-good settings when appropriate, record configuration history, and provide a fast rollback path when something goes wrong.</p><p>At scale, configuration should not be a collection of mysterious values scattered across files and environment variables.</p><p>It should be a controlled, validated, observable input into application behavior.</p><p>When we treat it that way, configuration stops being one of the easiest ways to accidentally break production and becomes one of the safest ways to operate a system without constantly redeploying it.</p><h2>Subscribe Now</h2><p>Enjoying the series? <a href="https://www.asptoday.com/">Subscribe to </a><strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, advanced architecture deep dives, and production-focused .NET engineering guides. Join our Substack Chat to discuss configuration strategies, distributed systems, and the challenges of running modern ASP.NET Core applications at scale. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Building Distributed Applications with .NET Aspire: Orchestration, Service Discovery, and Observability]]></title><description><![CDATA[Discover how .NET Aspire simplifies orchestration, service discovery, and observability. #ASPToday #aspnetcore #dotnetaspire #dotnet #cloudnative]]></description><link>https://www.asptoday.com/p/building-distributed-applications</link><guid isPermaLink="false">https://www.asptoday.com/p/building-distributed-applications</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 18 Aug 2026 15:01:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!9xvC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Distributed applications give us flexibility and scalability, but they also introduce a frustrating amount of operational complexity. An <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> application may depend on several APIs, databases, caches, message brokers, and background workers, each with its own endpoint, configuration, startup requirements, and telemetry. <a href="https://aspire.dev/">.NET Aspire</a> helps bring these moving pieces together. It provides a code-first way to describe a distributed application, connect its resources, discover services, coordinate startup, and observe the entire system from one place. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9xvC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9xvC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!9xvC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!9xvC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!9xvC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9xvC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/07025538-2ced-4246-ae8e-8137a6643117_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2907351,&quot;alt&quot;:&quot;Spacecraft modules connect inside a vast hangar, representing orchestration and service discovery. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/211543549?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Spacecraft modules connect inside a vast hangar, representing orchestration and service discovery. " title="Spacecraft modules connect inside a vast hangar, representing orchestration and service discovery. " srcset="https://substackcdn.com/image/fetch/$s_!9xvC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!9xvC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!9xvC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!9xvC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07025538-2ced-4246-ae8e-8137a6643117_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we'll build a practical mental model of .NET Aspire and explore how orchestration, service discovery, health checks, and <a href="https://www.asptoday.com/p/deep-dive-into-distributed-tracing">OpenTelemetry</a> can make distributed ASP.NET Core development much easier. </p><h2>The Problem with Distributed Applications</h2><p>A traditional ASP.NET Core application might be relatively simple.</p><pre><code><code>Browser
   &#8595;
ASP.NET Core
   &#8595;
Database</code></code></pre><p>As the application grows, that picture changes.</p><p>We might introduce:</p><pre><code><code>Web Frontend
     &#8595;
API Gateway
     &#8595;
 &#9484;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
 &#8595;   &#8595;         &#8595;
Orders Products Payments
 &#8595;       &#8595;        &#8595;
SQL    Redis    Message Queue</code></code></pre><p>Soon, running the application locally means starting several projects and infrastructure dependencies.</p><p>Developers need to know which ports services use.</p><p>Configuration files contain connection strings.</p><p>One service must start before another.</p><p>Databases and containers need to be running.</p><p>Telemetry is spread across multiple consoles.</p><p>The architecture may be well designed, but the development experience becomes increasingly difficult.</p><p>This is one of the problems Aspire is designed to solve.</p><h2>What Is .NET Aspire?</h2><p>Aspire provides tooling for building and operating distributed applications, with a strong emphasis on orchestration and observability.</p><p>Rather than treating each application, database, cache, and supporting service as an unrelated component, Aspire lets us describe the entire system as a collection of connected <strong>resources</strong>.</p><p>Microsoft currently describes Aspire as a code-first orchestration and observability layer for distributed applications.</p><p>That distinction matters.</p><p>Aspire is not another web framework replacing ASP.NET Core.</p><p>Your APIs remain ASP.NET Core applications.</p><p>Your background workers remain .NET workers.</p><p>Redis remains Redis.</p><p>PostgreSQL remains PostgreSQL.</p><p>Aspire provides a model that connects these components and makes the distributed application easier to develop and understand.</p><h2>Think of Aspire as the Application Coordinator</h2><p>Imagine an orchestra.</p><p>The musicians already know how to play their instruments.</p><p>The conductor does not replace them.</p><p>Instead, the conductor coordinates when everyone starts, how the pieces fit together, and how the entire performance behaves.</p><p>Aspire plays a similar role.</p><p>Your services already know how to perform their individual jobs.</p><p>Aspire helps coordinate the system around them.</p><h2>The AppHost</h2><p>The center of an Aspire application is the <strong>AppHost</strong>.</p><p>The AppHost describes your distributed application.</p><p>Instead of maintaining a collection of disconnected scripts and manually configured ports, you describe your resources and relationships in code.</p><p>A simplified AppHost might look like this:</p><pre><code><code>var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject&lt;Projects.CatalogApi&gt;("catalog");

builder.AddProject&lt;Projects.WebApp&gt;("web")
       .WithReference(api);

builder.Build().Run();</code></code></pre><p>We now have two resources:</p><pre><code><code>Catalog API
     &#8593;
     &#9474;
   Web App</code></code></pre><p>The important part is the relationship.</p><p>The web application references the API.</p><p>Aspire can use that relationship to provide connection information and service discovery rather than requiring the application to depend on a manually configured port.</p><p>Microsoft&#8217;s current Aspire guidance describes the AppHost as a code-first model for defining application architecture and relationships.</p><h2>Resources Are More Than ASP.NET Core Projects</h2><p>An Aspire resource does not have to be an ASP.NET Core project.</p><p>A distributed application might contain:</p><ul><li><p>ASP.NET Core APIs</p></li><li><p>Worker services</p></li><li><p>Databases</p></li><li><p><a href="https://www.asptoday.com/p/implementing-distributed-caching">Redis</a></p></li><li><p>Message brokers</p></li><li><p>Containers</p></li><li><p>Executables</p></li><li><p>Frontend applications</p></li></ul><p>The AppHost becomes a map of the entire application.</p><p>For example:</p><pre><code><code>             Web
              &#9474;
              &#9660;
          Catalog API
           /       \
          &#9660;         &#9660;
     PostgreSQL    Redis</code></code></pre><p>Instead of expecting every developer to reconstruct this architecture mentally, we can express it directly.</p><h2>Adding Infrastructure</h2><p>Suppose our Catalog API requires Redis.</p><p>Conceptually, our AppHost might define the cache and reference it from the API:</p><pre><code><code>var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");

var api = builder
    .AddProject&lt;Projects.CatalogApi&gt;("catalog")
    .WithReference(cache);

builder.Build().Run();</code></code></pre><p><code>WithReference()</code> tells Aspire that the API depends on the cache.</p><p>Aspire integrations can then provide the consuming application with the connection information it needs.</p><p>This is far cleaner than telling every developer:</p><p>&#8220;Start Redis manually, make sure it uses this port, then copy this connection string into your local configuration.&#8221;</p><h2>Why Code-First Orchestration Matters</h2><p>Infrastructure relationships often exist only in documentation.</p><p>That documentation becomes outdated.</p><p>Someone changes a port.</p><p>A new database is introduced.</p><p>A service gets renamed.</p><p>Another developer joins the team and spends half a day figuring out how everything fits together.</p><p>With Aspire, the application topology lives alongside the application.</p><p>That makes relationships:</p><ul><li><p>Visible</p></li><li><p>Version controlled</p></li><li><p>Repeatable</p></li><li><p>Easier to modify</p></li></ul><p>The architecture becomes something developers can run rather than something they merely read about.</p><h2>Orchestration Does Not Mean Business Orchestration</h2><p>There is an important distinction here.</p><p>Aspire application orchestration is not the same thing as orchestrating business workflows.</p><p>For example, Aspire does not replace the Saga pattern we explored earlier.</p><p>A Saga might coordinate:</p><pre><code><code>Create Order
    &#8595;
Reserve Stock
    &#8595;
Process Payment
    &#8595;
Arrange Shipping</code></code></pre><p>Aspire coordinates the resources required to run the application.</p><pre><code><code>Start Database
      &#8595;
Start API
      &#8595;
Start Frontend</code></code></pre><p>These solve completely different problems.</p><h2>Controlling Startup Dependencies</h2><p>Distributed applications often suffer from startup races.</p><p>Imagine the frontend starts immediately and calls the API.</p><p>The API is still starting.</p><p>The first request fails.</p><p>Or perhaps the API starts before its database is ready.</p><p>Aspire allows resource relationships to express readiness dependencies.</p><p>For example:</p><pre><code><code>var api = builder
    .AddProject&lt;Projects.CatalogApi&gt;("catalog");

builder
    .AddProject&lt;Projects.WebApp&gt;("web")
    .WithReference(api)
    .WaitFor(api);</code></code></pre><p><code>WaitFor()</code> can prevent the dependent resource from starting until the referenced resource is ready according to its lifecycle or health state. Microsoft&#8217;s current Aspire getting-started guidance demonstrates this alongside health checks to prevent startup race conditions.</p><p>This sounds like a small improvement.</p><p>Across a system with many dependencies, it becomes extremely valuable.</p><h2>Service Discovery</h2><p>Now we reach one of the biggest headaches in distributed development.</p><p>How does one service find another?</p><p>Suppose our Catalog API runs locally at:</p><p>https://localhost:7143</p><p>A developer might configure:</p><pre><code><code>{
  "CatalogApi": "https://localhost:7143"
}</code></code></pre><p>But another developer might receive:</p><p>https://localhost:7288</p><p>Production uses a completely different hostname.</p><p>Containers introduce another network environment.</p><p>Hardcoded addresses become fragile quickly.</p><h2>Logical Service Names</h2><p>Service discovery solves this problem.</p><p>Instead of thinking:</p><pre><code><code>Call https://localhost:7143</code></code></pre><p>the application thinks:</p><pre><code><code>Call catalog</code></code></pre><p>Aspire knows where <code>catalog</code> actually lives.</p><p>A client can therefore use a logical service address rather than a hardcoded endpoint.</p><p>For example:</p><pre><code><code>builder.Services.AddHttpClient&lt;CatalogClient&gt;(client =&gt;
{
    client.BaseAddress =
        new Uri("https+http://catalog");
});</code></code></pre><p>Aspire&#8217;s service discovery resolves the logical service name to the appropriate endpoint at runtime. <a href="https://aspire.dev/fundamentals/service-discovery/">Microsoft&#8217;s documentation</a> specifically describes this model as using logical names instead of hardcoded URLs.</p><h2>Why This Changes Local Development</h2><p>Imagine five developers working on the same system.</p><p>Without service discovery, everyone needs matching port configuration.</p><p>With service discovery:</p><pre><code><code>Web
 &#8595;
"catalog"
 &#8595;
Service Discovery
 &#8595;
Actual Catalog Endpoint</code></code></pre><p>The consuming code no longer cares whether Catalog runs:</p><pre><code><code>localhost:5237</code></code></pre><p>or somewhere else entirely.</p><p>The same basic service relationship can work across different environments because endpoint resolution is separated from application logic.</p><h2>Adding a Database</h2><p>Let&#8217;s make the example more realistic.</p><p>Suppose the Catalog API needs PostgreSQL.</p><p>The AppHost can model that dependency too.</p><pre><code><code>var builder = DistributedApplication.CreateBuilder(args);

var postgres = builder.AddPostgres("postgres");

var catalogDb =
    postgres.AddDatabase("catalogdb");

var catalog = builder
    .AddProject&lt;Projects.CatalogApi&gt;("catalog")
    .WithReference(catalogDb);

builder.AddProject&lt;Projects.WebApp&gt;("web")
       .WithReference(catalog)
       .WaitFor(catalog);

builder.Build().Run();</code></code></pre><p>Our topology is now:</p><pre><code><code>Web
 &#8595;
Catalog API
 &#8595;
PostgreSQL
 &#8595;
Catalog Database</code></code></pre><p>The code itself explains the relationship.</p><h2>Adding Redis</h2><p>Now suppose product information is frequently requested.</p><p>We introduce Redis.</p><pre><code><code>var cache = builder.AddRedis("cache");

var catalog = builder
    .AddProject&lt;Projects.CatalogApi&gt;("catalog")
    .WithReference(catalogDb)
    .WithReference(cache);</code></code></pre><p>The architecture becomes:</p><pre><code><code>             Web
              &#9474;
              &#9660;
          Catalog API
           /       \
          &#9660;         &#9660;
   PostgreSQL      Redis</code></code></pre><p>Notice what is happening.</p><p>We aren&#8217;t adding Redis-specific orchestration logic to our web application.</p><p>We&#8217;re describing the dependency at the application-model level.</p><h2>Aspire Integrations</h2><p>Aspire provides integrations for many common infrastructure components and services.</p><p>These integrations help model resources and provide appropriate configuration to applications consuming them.</p><p>The important idea is not memorizing a list of integrations.</p><p>It is understanding the pattern:</p><pre><code><code>Define Resource
      &#8595;
Reference Resource
      &#8595;
Aspire Supplies Configuration
      &#8595;
Application Uses Resource</code></code></pre><p>This creates a much more consistent way to wire distributed applications together.</p><h2>Service Defaults</h2><p>Distributed applications tend to repeat the same infrastructure configuration.</p><p>Every service needs:</p><ul><li><p><a href="https://www.asptoday.com/p/aspnet-core-health-checks-keeping">Health checks</a></p></li><li><p>Telemetry</p></li><li><p>Service discovery</p></li><li><p>Resilience configuration</p></li></ul><p>Copying the same setup into every project creates maintenance problems.</p><p>Aspire addresses this through <strong>Service Defaults</strong>.</p><p>A Service Defaults project centralizes common configuration that can be reused across projects in the solution.</p><p>Microsoft&#8217;s Aspire templates describe Service Defaults as reusable configuration for areas including resilience, service discovery, and telemetry.</p><h2>Why Service Defaults Matter</h2><p>Imagine ten ASP.NET Core services.</p><p>Without shared defaults:</p><pre><code><code>Orders     &#8594; configure telemetry
Payments   &#8594; configure telemetry
Catalog    &#8594; configure telemetry
Shipping   &#8594; configure telemetry
Identity   &#8594; configure telemetry</code></code></pre><p>Someone eventually configures one differently.</p><p>With shared defaults:</p><pre><code><code>          Service Defaults
        /    /    |    \    \
       &#8595;    &#8595;     &#8595;     &#8595;    &#8595;
Orders Payments Catalog Shipping Identity</code></code></pre><p>The teams begin with a consistent baseline.</p><p>Individual applications can still add their own configuration where necessary.</p><p>Observability from the Beginning</p><p>Distributed applications are difficult to debug because a single user action may cross several services.</p><p>Imagine a checkout request:</p><pre><code><code>Browser
   &#8595;
Web
   &#8595;
Orders API
   &#8595;
Inventory API
   &#8595;
Payment API
   &#8595;
Database</code></code></pre><p>The user sees:</p><pre><code><code>Checkout failed.</code></code></pre><p>Where did it fail?</p><p>Without distributed observability, developers may need to inspect several log files and manually correlate timestamps.</p><p>Aspire encourages observability as part of the development experience rather than something added just before production.</p><h2>The Aspire Dashboard</h2><p>One of Aspire&#8217;s most useful features is its dashboard.</p><p>When you run an AppHost, the dashboard provides a centralized view of the resources in your distributed application.</p><p>Instead of switching between terminal windows, developers get a place to inspect the system.</p><p>The dashboard works with OpenTelemetry data and can surface logs, traces, metrics, resource information, and endpoints. Microsoft&#8217;s documentation notes that the dashboard launches automatically in common AppHost development workflows.</p><p>This dramatically improves the local debugging experience.</p><h2>Structured Logs</h2><p>Suppose the Orders API produces:</p><pre><code><code>logger.LogInformation(
    "Processing order {OrderId}",
    order.Id);</code></code></pre><p>Structured logging keeps <code>OrderId</code> as meaningful data rather than burying it inside a text string.</p><p>Across distributed services, this becomes particularly useful because developers can search and correlate events much more effectively.</p><h2>Distributed Traces</h2><p>We explored OpenTelemetry and distributed tracing earlier in this series.</p><p>Aspire makes those concepts immediately useful during development.</p><p>Suppose a request travels:</p><pre><code><code>Web
 &#9474;
 &#9492;&#9472;&#9472; Orders API          35 ms
       &#9474;
       &#9500;&#9472;&#9472; Inventory     18 ms
       &#9474;
       &#9492;&#9472;&#9472; Payments     840 ms</code></code></pre><p>The problem becomes obvious.</p><p>Payments is taking most of the time.</p><p>Instead of guessing which service caused the delay, the trace shows the request&#8217;s journey.</p><h2>Metrics</h2><p>Logs tell us what happened.</p><p>Traces show how a request moved.</p><p>Metrics tell us how the system behaves over time.</p><p>Useful metrics might include:</p><pre><code><code>Requests per second
Request duration
Failed requests
CPU usage
Memory usage
Dependency latency</code></code></pre><p>Aspire&#8217;s Service Defaults can configure OpenTelemetry collection for logging, tracing, and metrics, while OTLP allows telemetry to be exported to compatible monitoring systems.</p><h2>Health Checks</h2><p>Observability tells us what is happening.</p><p>Health checks answer another important question:</p><p><strong>Can this service actually do its job?</strong></p><p>An application process may technically be running while its database is unavailable.</p><p>That service isn&#8217;t truly healthy.</p><p>ASP.NET Core health checks can represent conditions such as:</p><pre><code><code>API running            &#10003;
Database reachable     &#10003;
Redis reachable        &#10003;
Message broker         &#10007;</code></code></pre><p>Aspire can incorporate health information into resource orchestration and visibility.</p><h2>A More Complete Example</h2><p>Let&#8217;s imagine we&#8217;re building an online store.</p><p>Our system contains:</p><pre><code><code>Web Frontend
Catalog API
Orders API
PostgreSQL
Redis</code></code></pre><p>The AppHost could conceptually describe the system like this:</p><pre><code><code>var builder =
    DistributedApplication.CreateBuilder(args);

var postgres =
    builder.AddPostgres("postgres");

var ordersDb =
    postgres.AddDatabase("ordersdb");

var cache =
    builder.AddRedis("cache");

var catalog =
    builder.AddProject&lt;Projects.CatalogApi&gt;("catalog")
           .WithReference(cache);

var orders =
    builder.AddProject&lt;Projects.OrdersApi&gt;("orders")
           .WithReference(ordersDb)
           .WithReference(catalog)
           .WaitFor(ordersDb);

builder.AddProject&lt;Projects.WebApp&gt;("web")
       .WithReference(catalog)
       .WithReference(orders)
       .WaitFor(catalog)
       .WaitFor(orders);

builder.Build().Run();</code></code></pre><p>Now look at what the application model communicates.</p><pre><code><code>                 Web
                /   \
               &#9660;     &#9660;
          Catalog   Orders
             &#9474;       /  \
             &#9660;      &#9660;    &#9660;
           Redis   DB   Catalog</code></code></pre><p>Someone joining the project can understand much of the topology simply by reading the AppHost.</p><h2>Adding a New Developer</h2><p>This is where Aspire can make a noticeable difference.</p><p>Imagine joining a project containing eight services.</p><p>Traditionally, onboarding instructions might say:</p><ol><li><p>Install PostgreSQL.</p></li><li><p>Install Redis.</p></li><li><p>Configure these connection strings.</p></li><li><p>Start Orders first.</p></li><li><p>Start Catalog.</p></li><li><p>Make sure Catalog uses port 7182.</p></li><li><p>Start the frontend.</p></li><li><p>Open three terminals for logs.</p></li><li><p>Configure your tracing environment.</p></li></ol><p>That is a lot of tribal knowledge.</p><p>A well-designed Aspire application can encode much more of that setup in the application model.</p><p>The developer starts the AppHost.</p><p>The system topology comes to life.</p><h2>Aspire and Containers</h2><p>Containers remain useful.</p><p>Aspire does not make Docker irrelevant.</p><p>Instead, it can model containerized dependencies alongside your application projects.</p><p>For local development, that means infrastructure such as Redis or PostgreSQL can participate in the same application model rather than being treated as completely separate setup tasks.</p><p>This complements the <a href="https://www.asptoday.com/p/building-scalable-applications-with">Docker</a> and <a href="https://www.asptoday.com/p/aspnet-core-and-kubernetes-from-development">Kubernetes</a> concepts we&#8217;ve already explored in this series. </p><h2>Aspire and Kubernetes</h2><p>Another important distinction:</p><p>Aspire is not Kubernetes.</p><p>Kubernetes is an orchestration platform for deploying and operating containerized workloads.</p><p>Aspire&#8217;s AppHost provides a code-first application model that can be used during development and can participate in deployment workflows.</p><p>You should not think:</p><pre><code><code>Aspire OR Kubernetes</code></code></pre><p>Think:</p><pre><code><code>Application Model
        &#8595;
Deployment Environment</code></code></pre><p>The deployment environment might eventually involve containers, Kubernetes, Azure services, or another supported target.</p><p>Aspire&#8217;s current documentation explicitly separates application modeling from deployment topology and supports multiple deployment directions rather than tying the model to a single hosting destination.</p><h2>Aspire Is Not Only for New Applications</h2><p>You do not need to rebuild an existing ASP.NET Core system from scratch.</p><p>Aspire can be introduced incrementally.</p><p>For example, you might start by adding an AppHost that models:</p><pre><code><code>Existing API
Existing Worker
Redis
PostgreSQL</code></code></pre><p>Then introduce common telemetry.</p><p>Later, add service discovery.</p><p>Then model more infrastructure.</p><p>Microsoft&#8217;s current guidance specifically supports adding Aspire to existing applications and gradually modeling containers, databases, caches, queues, workers, and other resources.</p><p>That makes adoption much less risky.</p><h2>Where Aspire Helps Most</h2><p>Aspire becomes particularly valuable when an application contains enough moving parts that local development starts becoming painful.</p><p>Imagine:</p><pre><code><code>Frontend
API Gateway
Identity Service
Catalog Service
Orders Service
Payment Service
Worker
Redis
PostgreSQL
RabbitMQ</code></code></pre><p>Running all of that manually is frustrating.</p><p>Understanding it is harder.</p><p>Debugging it is harder still.</p><p>The value of a unified application model grows with the number of dependencies.</p><h2>Where Aspire May Be Unnecessary</h2><p>Not every ASP.NET Core application needs distributed orchestration.</p><p>Suppose you have:</p><pre><code><code>Razor Pages App
      &#8595;
SQL Database</code></code></pre><p>The system is simple.</p><p>Adding additional tooling may provide little value.</p><p>This is an important architectural lesson we&#8217;ve repeated throughout this series:</p><p><strong>Use complexity to solve complexity. Do not introduce complexity merely because a technology is interesting.</strong></p><h2>Common Mistake: Treating Aspire as Magic</h2><p>Aspire does not make distributed systems simple.</p><p>Network failures still happen.</p><p>Databases still become unavailable.</p><p>Services still experience latency.</p><p>Distributed transactions are still difficult.</p><p>Security still matters.</p><p>What Aspire improves is our ability to <strong>model, connect, run, and observe</strong> those systems.</p><p>Good architecture remains essential.</p><h2>Common Mistake: Hiding Architecture</h2><p>Because Aspire makes adding resources convenient, developers may be tempted to keep adding services.</p><p>That would be the wrong lesson.</p><p>If one ASP.NET Core application solves the problem well, splitting it into six services does not automatically improve the architecture.</p><p>The AppHost should describe a sensible architecture.</p><p>It should not justify an unnecessarily complicated one.</p><h2>Common Mistake: Ignoring Production Differences</h2><p>A beautiful local environment does not guarantee a healthy production environment.</p><p>Production introduces:</p><ul><li><p>Real network latency</p></li><li><p><a href="https://www.asptoday.com/p/advanced-authentication-patterns">Authentication</a></p></li><li><p>Secrets</p></li><li><p>Scaling</p></li><li><p>Regional failures</p></li><li><p>Cost</p></li><li><p>Resource limits</p></li></ul><p>Use Aspire to improve development and application modeling, but continue designing explicitly for the environment where the system will actually operate.</p><h2>A Real-World Scenario</h2><p>Imagine a logistics company building a parcel-processing platform.</p><p>Its system contains:</p><pre><code><code>Operations Portal
       &#8595;
Shipment API
       &#8595;
 &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
 &#9660;     &#9660;          &#9660;
Pricing Tracking Notifications
 &#9474;       &#9474;
 &#9660;       &#9660;
Redis PostgreSQL

Background Workers
       &#8595;
Message Broker</code></code></pre><p>Without a unified development model, every engineer must understand how to launch and configure all of these pieces.</p><p>With Aspire, the AppHost describes them as resources and relationships.</p><p>Developers start the distributed application.</p><p>Service discovery connects the services.</p><p>Health checks reveal whether dependencies are ready.</p><p>OpenTelemetry records activity.</p><p>The dashboard exposes the system&#8217;s behavior.</p><p>When a shipment request becomes slow, developers inspect its trace.</p><p>They discover that Pricing is waiting on a downstream dependency.</p><p>Instead of searching through five terminal windows, they can follow the request through the distributed system.</p><p>That is where Aspire becomes much more than a convenient project launcher.</p><p>It becomes a development environment for understanding the architecture itself.</p><h2>How This Fits Our ASP.NET Core Journey</h2><p>Over the course of this series, we&#8217;ve explored many pieces of distributed application architecture individually.</p><p>We&#8217;ve covered:</p><ul><li><p>Microservices</p></li><li><p>Docker</p></li><li><p>Kubernetes</p></li><li><p>Event-driven architecture</p></li><li><p>gRPC</p></li><li><p>Health checks</p></li><li><p>Resilience</p></li><li><p>Distributed caching</p></li><li><p>Distributed tracing</p></li><li><p>API Gateways</p></li><li><p>Service meshes</p></li><li><p>Internal Developer Platforms</p></li></ul><p>Aspire does not replace those concepts.</p><p>It provides a way to bring many of them into a more coherent developer experience.</p><p>Instead of thinking about an application as ten unrelated projects and five infrastructure components, we can model it as one distributed system.</p><p>That is an important shift.</p><h2>What Aspire Gives Us</h2><p>The easiest way to remember Aspire is through four questions.</p><p><strong>What runs?</strong></p><p>The AppHost describes the resources.</p><p><strong>What depends on what?</strong></p><p>References describe relationships.</p><p><strong>How do services find each other?</strong></p><p>Service discovery resolves logical names to endpoints.</p><p><strong>How do we understand what is happening?</strong></p><p>OpenTelemetry and the Aspire dashboard provide observability.</p><p>Together, those capabilities attack some of the most frustrating parts of distributed application development.</p><h2>Closing Thoughts</h2><p>Distributed architecture gives development teams powerful ways to scale applications, isolate responsibilities, and choose the right technology for each workload. But every new service also creates another endpoint, dependency, configuration problem, health state, and source of telemetry.</p><p>Aspire tackles that operational complexity from the developer&#8217;s perspective.</p><p>With a code-first AppHost, we can describe our distributed application as a system rather than a collection of unrelated processes. Service discovery removes the need to hardcode changing endpoints. Shared defaults give services a consistent foundation for resilience, health checks, and telemetry. OpenTelemetry and the Aspire dashboard then help us see how those services behave together.</p><p>The result is not a simpler architecture by magic.</p><p>It is an architecture that is <strong>easier to run, easier to understand, easier to observe, and easier for the next developer to work with</strong>.</p><p>And as our ASP.NET Core systems become increasingly distributed, that developer experience matters just as much as the individual services themselves.</p><h2>Subscribe Now</h2><p>Enjoying the series? <a href="https://www.asptoday.com/">Subscribe to </a><strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, advanced architecture guides, and real-world .NET engineering strategies. Join our Substack Chat to discuss distributed applications, share development experiences, and connect with developers building modern ASP.NET Core systems. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Edge Computing with ASP.NET Core: Running Applications Closer to Users]]></title><description><![CDATA[Discover how edge computing brings ASP.NET Core workloads closer to users and devices for faster responses, local processing, and greater resilience. #ASPToday #aspnetcore #edgecomputing #cloudnative #dotnet]]></description><link>https://www.asptoday.com/p/edge-computing-with-aspnet-core-running</link><guid isPermaLink="false">https://www.asptoday.com/p/edge-computing-with-aspnet-core-running</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 11 Aug 2026 15:01:08 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!rCs1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Cloud computing transformed the way we build applications by giving us centralized infrastructure that can scale almost instantly. But sending every request and every piece of data back to a distant cloud region is not always the best solution. Some applications need extremely fast responses, must continue working when connectivity disappears, or generate so much data that constantly sending everything to the cloud becomes inefficient. Edge computing addresses these challenges by moving part of the application closer to the people, devices, and machines using it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rCs1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rCs1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!rCs1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!rCs1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!rCs1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rCs1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2586682,&quot;alt&quot;:&quot;Remote mountain sensors process data locally at an edge station before syncing with the cloud. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/210442196?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Remote mountain sensors process data locally at an edge station before syncing with the cloud. " title="Remote mountain sensors process data locally at an edge station before syncing with the cloud. " srcset="https://substackcdn.com/image/fetch/$s_!rCs1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!rCs1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!rCs1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!rCs1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73cc23c4-4834-4591-89d7-97e9ec5fc636_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we&#8217;ll explore what edge computing means for <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> developers, where it makes sense, how edge and cloud systems work together, and how to design practical applications that remain fast, secure, and resilient. </p><h2>What Does &#8220;The Edge&#8221; Actually Mean?</h2><p>We&#8217;ve spent years moving applications into the cloud.</p><p>Now we&#8217;re talking about moving some of them back?</p><p>Not exactly.</p><p>Edge computing does not mean abandoning the cloud. It means deciding <strong>where each piece of work should happen</strong>.</p><p>Imagine a factory in Germany sending sensor readings to a cloud data center hundreds of kilometers away. If every machine decision requires a round trip to that cloud, network latency and connectivity become part of the production process.</p><p>Instead, the factory can run some application services locally.</p><p>Those services process important information immediately and send selected data to the cloud afterward.</p><p>That local computing environment is the edge.</p><p>The edge could be a small server in a factory, a computer inside a retail store, an industrial gateway, a regional data center, or another computing system located near the source of the data.</p><p>The exact hardware matters less than the architectural idea.</p><p><strong>Move time-sensitive processing closer to where the data is created or consumed. </strong></p><h2>Cloud Computing vs Edge Computing</h2><p>Traditional cloud architecture often looks like this:</p><pre><code><code>Device
   &#9474;
   &#9474; Internet
   &#9660;
Cloud Application
   &#9474;
   &#9660;
Cloud Database</code></code></pre><p>Every important operation depends on reaching the cloud.</p><p>An edge architecture introduces another layer:</p><pre><code><code>Device
   &#9474;
   &#9660;
Edge Application
   &#9474;
   &#9500;&#9472;&#9472; Local Processing
   &#9500;&#9472;&#9472; Local Storage
   &#9492;&#9472;&#9472; Immediate Decisions
   &#9474;
   &#9474; Internet
   &#9660;
Cloud Platform</code></code></pre><p>The edge handles work that benefits from being local.</p><p>The cloud continues handling workloads that benefit from centralized infrastructure.</p><p>This distinction is important because edge computing and cloud computing are not competitors. In most real applications, they complement each other. </p><h2>Why Put ASP.NET Core at the Edge?</h2><p>ASP.NET Core is well suited to many edge scenarios because .NET runs across Windows and Linux, can be deployed in containers, provides high-performance web APIs, and includes mature support for <a href="https://www.asptoday.com/p/dependency-injection-in-aspnet-core">dependency injection</a>, configuration, background services, security, and observability.</p><p>An ASP.NET Core application running at an edge location might expose APIs to local devices, process incoming information, store temporary data, make immediate decisions, and periodically communicate with cloud services.</p><p>Developers can use many of the same programming patterns they already know.</p><p>The biggest difference is the environment.</p><p>At the edge, you cannot assume the network is always available. </p><h2>The Latency Problem</h2><p>Latency is the delay between sending a request and receiving a response.</p><p>For many business applications, a little network latency is perfectly acceptable.</p><p>If someone requests a monthly sales report, an extra fraction of a second probably does not matter.</p><p>But consider a manufacturing system detecting a dangerous machine condition.</p><p>Waiting for:</p><pre><code><code>Machine
   &#8595;
Internet
   &#8595;
Cloud
   &#8595;
Analysis
   &#8595;
Internet
   &#8595;
Machine</code></code></pre><p>may be unnecessary when the decision could happen locally.</p><p>With edge processing:</p><pre><code><code>Machine
   &#8595;
Local ASP.NET Core Service
   &#8595;
Immediate Response</code></code></pre><p>The cloud can receive the event afterward for analytics, reporting, and long-term storage. </p><h2>Edge Computing Is About More Than Speed</h2><p>Latency receives most of the attention, but it is only one reason to use edge computing.</p><p>Connectivity is another.</p><p>Imagine a delivery warehouse where handheld scanners depend entirely on a cloud API.</p><p>If the internet connection fails, workers may no longer be able to:</p><ul><li><p>Scan packages</p></li><li><p>Locate inventory</p></li><li><p>Confirm collections</p></li><li><p>Record deliveries</p></li></ul><p>An edge service inside the warehouse can keep those operations running locally.</p><p>When connectivity returns, the edge system synchronizes its data with the cloud.</p><p>That is a major architectural advantage. </p><h2>Designing for Intermittent Connectivity</h2><p>Cloud applications are often designed around an assumption:</p><blockquote><p>The network is available.</p></blockquote><p>Edge applications should make the opposite assumption:</p><blockquote><p>The network will eventually fail.</p></blockquote><p>That changes how we design software.</p><p>Instead of immediately sending every operation to the cloud, an edge application may record work locally.</p><p>For example:</p><pre><code><code>public async Task RecordScanAsync(PackageScan scan)
{
    await _localRepository.SaveAsync(scan);

    await _syncQueue.EnqueueAsync(scan.Id);
}</code></code></pre><p>The scan succeeds locally.</p><p>A background process can attempt cloud synchronization separately.</p><p>This prevents temporary connectivity problems from stopping the user&#8217;s work. </p><h2>Background Synchronization</h2><p>ASP.NET Core&#8217;s hosted services are useful for synchronization workloads.</p><p>A simplified worker might look like this:</p><pre><code><code>public class CloudSyncWorker : BackgroundService
{
    private readonly ISyncService _syncService;

    public CloudSyncWorker(ISyncService syncService)
    {
        _syncService = syncService;
    }

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _syncService.PushPendingChangesAsync(
                stoppingToken);

            await Task.Delay(
                TimeSpan.FromSeconds(30),
                stoppingToken);
        }
    }
}</code></code></pre><p>Production synchronization is more complicated, but the pattern is straightforward.</p><p>Perform the user&#8217;s work locally first.</p><p>Synchronize with the central platform when possible.</p><h2>Local Storage at the Edge</h2><p>Once an application can operate offline, it usually needs local storage.</p><p>Depending on the workload, that might include a relational database, embedded database, local files, cache, or message queue.</p><p>The edge service might store:</p><ul><li><p>Pending transactions</p></li><li><p>Device readings</p></li><li><p>Configuration</p></li><li><p>Product information</p></li><li><p>Inventory data</p></li><li><p>Synchronization state</p></li></ul><p>The important question is not simply what database to choose.</p><p>You must decide <strong>which data belongs at the edge and which data remains authoritative in the cloud</strong>.</p><p>That distinction prevents synchronization from becoming chaotic. </p><h2>Data Synchronization Gets Complicated</h2><p>Suppose a retail store loses its internet connection.</p><p>While offline, the store sells the final item in stock.</p><p>At the same time, the online store sells what it believes is the same final item.</p><p>When the connection returns, two valid systems disagree.</p><p>Which one wins?</p><p>This is a synchronization conflict.</p><p>Edge systems need explicit rules for situations like this.</p><p>Possible strategies include timestamps, version numbers, optimistic concurrency, business-specific conflict resolution, or manual review.</p><p>There is no universal answer.</p><p>The correct strategy depends on what the data represents. </p><h2>Idempotency Matters Again</h2><p>Our earlier discussion of idempotency becomes particularly useful at the edge.</p><p>Suppose an edge application sends an order to the cloud.</p><p>The cloud processes it successfully, but the network disconnects before the edge receives the response.</p><p>Did the operation succeed?</p><p>The edge cannot know.</p><p>If it blindly retries, the customer could receive two orders.</p><p>An idempotency key allows the cloud service to recognize the repeated operation.</p><p>For example:</p><pre><code><code>POST /orders
Idempotency-Key: 91d57e2f-2e8a-4b11</code></code></pre><p>If the same request arrives again, the server can return the previous result rather than creating another order.</p><p>Reliable synchronization depends heavily on patterns like this. </p><h2>Practical Use Case: Manufacturing</h2><p>Factories are a natural environment for edge computing.</p><p>Modern equipment can generate enormous streams of sensor data.</p><p>Sending every temperature reading, vibration measurement, and machine event to the cloud may be unnecessary.</p><p>An edge ASP.NET Core application can collect the readings locally.</p><p>It can detect abnormal conditions immediately.</p><p>For example:</p><pre><code><code>if (reading.Temperature &gt; safetyLimit)
{
    await _machineController.StopAsync();
}</code></code></pre><p>The edge application reacts immediately.</p><p>Meanwhile, summarized measurements and important events can still be sent to the cloud for long-term analytics. </p><h2>Practical Use Case: Retail Stores</h2><p>Imagine a supermarket with:</p><ul><li><p>Checkout systems</p></li><li><p>Barcode scanners</p></li><li><p>Electronic price displays</p></li><li><p>Inventory terminals</p></li></ul><p>If every interaction depends on the internet, a network outage could disrupt the entire store.</p><p>A local ASP.NET Core service can maintain essential product and pricing information.</p><p>Checkout operations continue.</p><p>Transactions are queued.</p><p>When connectivity returns, the store synchronizes with central systems.</p><p>The cloud still provides centralized management, but the store is no longer completely dependent on it. </p><h2>Practical Use Case: Logistics</h2><p>Delivery and logistics systems are another excellent example.</p><p>Warehouses may process thousands of package scans every hour.</p><p>A local edge application can validate scans, update warehouse state, coordinate sorting equipment, and maintain operations even during temporary cloud outages.</p><p>The central platform receives synchronized events as connectivity permits.</p><p>The warehouse gets responsiveness.</p><p>Head office gets centralized visibility.</p><p>Both benefit. </p><h2>Practical Use Case: Remote Locations</h2><p>Not every application runs in a city with excellent connectivity.</p><p>Consider:</p><ul><li><p>Ships</p></li><li><p>Mines</p></li><li><p>Farms</p></li><li><p>Construction sites</p></li><li><p>Remote energy facilities</p></li></ul><p>Network connections may be slow, expensive, intermittent, or unavailable.</p><p>Edge computing allows important operations to continue locally.</p><p>The cloud becomes a synchronization and management layer rather than a constant dependency. </p><h2>Edge AI</h2><p>Our previous article explored integrating AI and machine learning with ASP.NET Core.</p><p>Edge computing adds another interesting possibility.</p><p>Some AI inference can happen locally.</p><p>Imagine a manufacturing camera inspecting products on a conveyor belt.</p><p>Sending every image to the cloud introduces bandwidth usage and delay.</p><p>Instead, a local model can inspect the image.</p><p>Only unusual results might be uploaded.</p><p>The workflow becomes:</p><pre><code><code>Camera
   &#8595;
Edge AI Model
   &#8595;
ASP.NET Core Service
   &#8595;
Immediate Decision
   &#8595;
Selected Results
   &#8595;
Cloud</code></code></pre><p>This combination of AI and edge computing can be particularly useful when low latency, privacy, or bandwidth efficiency matters. </p><h2>Containers at the Edge</h2><p>Containers make edge deployment much easier.</p><p>An ASP.NET Core service can be packaged into a container and deployed consistently across many locations.</p><p>The same application image might run in:</p><ul><li><p>50 stores</p></li><li><p>200 warehouses</p></li><li><p>1,000 industrial sites</p></li></ul><p>Containers provide consistency, but deployment at this scale creates another challenge.</p><p>How do you update all those locations safely? </p><h2>Managing Edge Deployments</h2><p>Centralized management is essential.</p><p>You need to know:</p><ul><li><p>Which application version each location runs</p></li><li><p>Whether deployments succeeded</p></li><li><p>Which devices are online</p></li><li><p>Whether an edge node is healthy</p></li><li><p>When updates failed</p></li></ul><p>Updates should also tolerate interruptions.</p><p>If connectivity disappears halfway through an upgrade, the site should not become unusable.</p><p>Versioned deployments and rollback strategies become especially important. </p><h2>Security at the Edge</h2><p>Edge computing changes your security boundary.</p><p>A cloud server usually lives inside a carefully controlled data center.</p><p>An edge server might live:</p><ul><li><p>Behind a checkout counter</p></li><li><p>Inside a warehouse</p></li><li><p>On a factory floor</p></li><li><p>In a remote cabinet</p></li></ul><p>Someone may be able to physically access it.</p><p>That means security must account for both network and physical threats.</p><p>Sensitive information stored locally should be protected appropriately, credentials should not be hardcoded into deployments, and each edge node should have a strong identity. </p><h2>Zero Trust at the Edge</h2><p>Our earlier Zero Trust discussion applies particularly well here.</p><p>The cloud should not automatically trust an edge device simply because it belongs to the company.</p><p>Each device or service should authenticate itself.</p><p>Likewise, edge services should verify cloud endpoints.</p><p>This can involve certificates, managed identities where supported, short-lived credentials, or other workload identity mechanisms.</p><p>The principle remains simple:</p><p><strong>Verify the identity of every participant rather than trusting its network location. </strong></p><h2>Protecting Local Data</h2><p>Edge systems sometimes contain sensitive information.</p><p>That data may include:</p><ul><li><p>Customer details</p></li><li><p>Transactions</p></li><li><p>Operational information</p></li><li><p><a href="https://www.asptoday.com/p/advanced-authentication-patterns">Authentication</a> credentials</p></li></ul><p>Encryption at rest becomes important, especially when hardware could be lost or stolen. </p><p>Data retention should also be considered carefully. </p><p>If an edge node only needs seven days of local information, keeping six months may create unnecessary risk. </p><p>Store only what the edge actually needs.  </p><h2>Observability at the Edge</h2><p>Monitoring a cloud application is relatively straightforward because the infrastructure is continuously connected.</p><p>Edge environments are different.</p><p>A node might disappear for hours.</p><p>Does that mean the application failed?</p><p>Or did the internet connection fail?</p><p>Your observability strategy needs to distinguish between the two.</p><p>Useful signals include:</p><ul><li><p>Application <a href="https://www.asptoday.com/p/aspnet-core-health-checks-keeping">health</a></p></li><li><p>CPU and memory usage</p></li><li><p>Synchronization backlog</p></li><li><p>Last successful cloud connection</p></li><li><p>Local storage capacity</p></li><li><p>Application version</p></li></ul><p>When connectivity returns, telemetry may also need to be uploaded in batches. </p><h2>Distributed Tracing</h2><p><a href="https://www.asptoday.com/p/deep-dive-into-distributed-tracing">Distributed tracing</a> becomes interesting when a request begins at the edge and continues in the cloud.</p><p>A correlation or trace identifier can follow the operation across both environments.</p><p>For example:</p><pre><code><code>Warehouse Scanner
      &#8595;
Edge API
      &#8595;
Local Queue
      &#8595;
Cloud Sync
      &#8595;
Order API
      &#8595;
Inventory Service</code></code></pre><p>A distributed trace helps developers understand the complete journey.</p><p>OpenTelemetry remains valuable here, although telemetry collection needs to account for intermittent connections and buffering. </p><h2>API Gateways and Edge Computing</h2><p>Our recent API Gateway article focused on providing a central entrance to backend services.</p><p>Edge architectures may introduce gateways at multiple levels.</p><p>A local gateway might route requests between devices and edge services.</p><p>A cloud gateway handles requests entering the central platform.</p><p>The architecture could resemble:</p><pre><code><code>Local Devices
      &#8595;
Edge Gateway
      &#8595;
ASP.NET Core Edge Services
      &#8595;
Cloud API Gateway
      &#8595;
Cloud Microservices</code></code></pre><p>The same architectural concepts appear at different physical locations. </p><h2>Service Meshes at the Edge</h2><p>Our previous article explored service meshes.</p><p>In larger edge installations with multiple local services, mesh-style capabilities may also be useful for secure service-to-service communication, traffic management, and observability.</p><p>However, complexity matters.</p><p>A small store running two services probably does not need the same infrastructure as a <a href="https://www.asptoday.com/p/aspnet-core-and-kubernetes-from-development">Kubernetes cluster</a> running hundreds of microservices.</p><p>Edge architecture rewards simplicity.</p><p>Every extra component must operate when connectivity is poor and remote troubleshooting is difficult. </p><h2>What Should Stay in the Cloud?</h2><p>Edge computing does not mean moving everything locally.</p><p>Centralized workloads often belong in the cloud.</p><p>Examples include:</p><ul><li><p>Organization-wide analytics</p></li><li><p>Long-term storage</p></li><li><p>Central reporting</p></li><li><p>Model training</p></li><li><p>Cross-location coordination</p></li><li><p>Large-scale batch processing</p></li></ul><p>The edge should handle workloads that genuinely benefit from proximity.</p><p>A useful question is:</p><blockquote><p>What breaks if the connection to the cloud disappears for an hour?</p></blockquote><p>The answer often reveals which capabilities belong locally.  </p><h2>Avoiding the &#8220;Mini Cloud Everywhere&#8221; Trap</h2><p>A common mistake is recreating the entire cloud platform at every edge location.</p><p>That can become extremely expensive and difficult to operate.</p><p>Imagine maintaining dozens of databases, message brokers, monitoring systems, gateways, and orchestration platforms across hundreds of locations.</p><p>The operational burden grows quickly.</p><p>Instead, keep edge environments focused.</p><p>Run only the services necessary for local operation.</p><p>Centralize everything else where practical. </p><h2>A Complete Edge Architecture</h2><p>Imagine a chain of smart manufacturing facilities.</p><p>Each factory runs an ASP.NET Core edge service.</p><p>Locally it:</p><ul><li><p>Receives machine telemetry.</p></li><li><p>Stores recent readings.</p></li><li><p>Detects safety conditions.</p></li><li><p>Controls local workflows.</p></li><li><p>Queues cloud synchronization.</p></li></ul><p>The cloud platform:</p><ul><li><p>Stores long-term data.</p></li><li><p>Compares performance across factories.</p></li><li><p>Trains predictive models.</p></li><li><p>Distributes configuration.</p></li><li><p>Provides central dashboards.</p></li></ul><p>The two layers work together.</p><p>If the cloud disappears temporarily, the factory keeps operating.</p><p>When connectivity returns, synchronization resumes.</p><p>That is the real strength of edge architecture. </p><h2>When You Probably Don&#8217;t Need Edge Computing</h2><p>Not every application needs an edge layer.</p><p>A traditional business website serving users over the internet may work perfectly well from cloud infrastructure.</p><p>Adding edge services introduces:</p><ul><li><p>Deployment complexity</p></li><li><p>Synchronization challenges</p></li><li><p>Additional security concerns</p></li><li><p>Remote monitoring requirements</p></li><li><p>More infrastructure to maintain</p></li></ul><p>Use edge computing when there is a clear reason.</p><p>Low latency, intermittent connectivity, local autonomy, privacy, or bandwidth constraints are good reasons.</p><p>&#8220;Edge is popular&#8221; is not. </p><h2>How This Fits Your ASP.NET Core Journey</h2><p>Our recent articles have gradually expanded the boundaries of an ASP.NET Core application.</p><p>We started with services and distributed architecture.</p><p>Then we explored API Gateways for managing incoming traffic.</p><p>Service meshes helped manage communication inside the platform.</p><p>AI and machine learning introduced intelligent processing.</p><p>Now edge computing asks a different architectural question:</p><p><strong>Where should that processing actually happen?</strong></p><p>Sometimes the answer is the cloud.</p><p>Sometimes it is a server sitting meters away from the user or machine.</p><p>Increasingly, the answer is both. </p><h2>Closing Thoughts</h2><p>Edge computing is less about a particular technology and more about making intelligent decisions about where software should run.</p><p>ASP.NET Core gives .NET developers a strong foundation for building edge services because the same APIs, dependency injection patterns, background workers, security practices, and observability tools used in cloud applications can also be applied closer to users and devices. </p><p>The architectural challenge is learning to design for a world where connectivity is not guaranteed. Applications need local autonomy, reliable synchronization, secure device identities, careful data management, and strong observability.</p><p>When those pieces come together, edge computing allows cloud and local infrastructure to complement each other rather than compete.</p><p>The cloud provides scale and centralized intelligence.</p><p>The edge provides proximity and independence.</p><p>Together, they can create ASP.NET Core applications that keep working exactly where they&#8217;re needed most. </p><h2>Subscribe Now</h2><p>Enjoying the series? Subscribe to <strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, cloud-native architecture guides, and real-world development strategies. Join our Substack Chat to discuss modern .NET development, share ideas, and connect with developers building the next generation of applications. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p> </p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Integrating AI and Machine Learning in ASP.NET Core Applications: Practical Use Cases]]></title><description><![CDATA[Discover practical ways to integrate AI and machine learning into ASP.NET Core applications using ML.NET, Azure AI, OpenAI APIs, and real-world automation. #ASPToday #aspnetcore #artificialintelligence #machinelearning #dotnet]]></description><link>https://www.asptoday.com/p/integrating-ai-and-machine-learning</link><guid isPermaLink="false">https://www.asptoday.com/p/integrating-ai-and-machine-learning</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 04 Aug 2026 15:02:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!UxDz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Artificial Intelligence has rapidly moved from research labs into everyday business software. From intelligent search and chatbots to fraud detection and document analysis, AI is becoming an essential capability rather than a luxury feature.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UxDz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UxDz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!UxDz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!UxDz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!UxDz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UxDz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2088716,&quot;alt&quot;:&quot;Engineers integrate AI services into an ASP.NET Core application using intelligent automation. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/209463430?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Engineers integrate AI services into an ASP.NET Core application using intelligent automation. " title="Engineers integrate AI services into an ASP.NET Core application using intelligent automation. " srcset="https://substackcdn.com/image/fetch/$s_!UxDz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!UxDz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!UxDz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!UxDz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2f13a1a-4074-4749-bc63-b52067de0001_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Fortunately, <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> provides an excellent foundation for integrating modern AI services and machine learning models into existing applications. Whether you're calling cloud-hosted large language models, running local ML.NET predictions, or combining multiple AI services into a single workflow, today's tools make it easier than ever to build intelligent applications. </p><p>In this guide, we'll explore practical ways to add AI to ASP.NET Core applications, understand where machine learning fits into modern software architecture, and learn how to build AI features that solve real business problems. </p><h2>Why AI Matters Today</h2><p>Almost every modern application now contains some form of intelligence.</p><p>Examples include:</p><ul><li><p>Product recommendations</p></li><li><p>Smart search</p></li><li><p>Image recognition</p></li><li><p>Voice assistants</p></li><li><p>Email categorization</p></li><li><p>Fraud detection</p></li><li><p>Predictive maintenance</p></li><li><p>Customer support chatbots</p></li></ul><p>Users increasingly expect applications to understand context instead of simply responding to button clicks.</p><p>The good news is that developers no longer need a PhD in machine learning to build these capabilities.</p><p>Many AI services are available through straightforward APIs that integrate naturally with ASP.NET Core. </p><h2>Artificial Intelligence vs Machine Learning</h2><p>These terms are often used interchangeably, but they describe different concepts.</p><p>Artificial Intelligence is the broader goal of building systems that perform tasks requiring human-like intelligence.</p><p>Machine Learning is one approach to achieving that goal.</p><p>Instead of programming every rule manually, machine learning models learn patterns from data.</p><p>Think of it this way.</p><p>Traditional software follows instructions.</p><p>Machine learning discovers instructions from examples.</p><h2>A Simple Example</h2><p>Suppose you&#8217;re building an email application.</p><p>Traditional code might look like this:</p><pre><code><code>If subject contains "Invoice"
Move to Finance folder.</code></code></pre><p>Machine learning instead examines thousands of previous emails and learns which messages belong together, even when the word &#8220;invoice&#8221; never appears. </p><h2>Where ASP.NET Core Fits</h2><p>ASP.NET Core rarely performs heavy AI training itself.</p><p>Instead, it acts as the application layer.</p><p>It:</p><ul><li><p>Receives requests </p></li><li><p>Sends prompts or data to AI services </p></li><li><p>Validates responses </p></li><li><p>Stores results </p></li><li><p>Serves users </p></li></ul><p>This separation keeps applications maintainable while allowing AI models to evolve independently. </p><h2>Common AI Integration Patterns</h2><p>Most ASP.NET Core applications use one of three approaches.</p><h2>Cloud AI APIs</h2><p>The application calls hosted AI services.</p><p>Examples include:</p><ul><li><p>Azure AI Foundry </p></li><li><p>Azure AI Vision </p></li><li><p>Azure AI Document Intelligence </p></li><li><p>Azure AI Speech </p></li><li><p>OpenAI API<br></p></li></ul><p>This is often the quickest way to introduce AI features.</p><h2>ML.NET Models</h2><p>Sometimes organizations prefer running prediction models locally.</p><p>ML.NET enables developers to train and execute machine learning models directly within .NET applications.</p><p><a href="https://learn.microsoft.com/dotnet/machine-learning/">Official documentation</a>:</p><p>This approach works well when:</p><ul><li><p>Internet connectivity is limited. </p></li><li><p>Sensitive information cannot leave the organization. </p></li><li><p>Predictions must happen with very low latency. <br></p></li></ul><h2>Hybrid Solutions</h2><p>Many enterprise applications combine both approaches.</p><p>For example:</p><ul><li><p>ML.NET performs fraud scoring. </p></li><li><p>Azure AI extracts document text. </p></li><li><p>OpenAI summarizes the results. </p></li></ul><p>Each tool performs the task it handles best. </p><h2>Using AI Through Dependency Injection</h2><p>AI clients fit naturally into ASP.NET Core.</p><p>Example:</p><pre><code><code>builder.Services.AddSingleton&lt;IAIService, AIService&gt;();</code></code></pre><p>Controllers remain focused on business logic while dedicated services manage AI interactions.</p><h2>Calling an AI Service</h2><p>Example:</p><pre><code><code>public class ProductController : ControllerBase
{
    private readonly IAIService _ai;

    public ProductController(IAIService ai)
    {
        _ai = ai;
    }

    [HttpPost("summarize")]
    public async Task&lt;string&gt; Summarize(string text)
    {
        return await _ai.SummarizeAsync(text);
    }
}</code></code></pre><p>Notice how the controller doesn&#8217;t know which AI provider is being used.</p><p>This abstraction makes future changes much easier.</p><h2>Practical Use Case: Intelligent Customer Support</h2><p>Suppose customers submit support requests.</p><p>Instead of manually categorizing tickets, AI can automatically determine:</p><ul><li><p>Billing issue </p></li><li><p>Technical problem </p></li><li><p>Shipping question </p></li><li><p>Feature request </p></li></ul><p>Support teams spend less time sorting requests and more time solving problems.</p><h2>Practical Use Case: Smart Search</h2><p>Traditional keyword searches often disappoint users.</p><p>AI-powered semantic search understands intent.</p><p>Searching for:</p><blockquote><p>&#8220;Laptop won&#8217;t charge&#8221;</p></blockquote><p>can return documents discussing:</p><ul><li><p>Power adapter failures </p></li><li><p>Battery problems </p></li><li><p>Charging ports </p></li></ul><p>even when those exact words never appear.</p><h2>Practical Use Case: Document Processing</h2><p>Many businesses process:</p><ul><li><p>Contracts </p></li><li><p>Invoices </p></li><li><p>Insurance claims </p></li><li><p>Medical forms </p></li></ul><p>Azure AI Document Intelligence can extract structured information automatically.</p><p>ASP.NET Core then validates, stores, and presents the results.</p><p>Manual data entry is dramatically reduced.</p><p><a href="https://learn.microsoft.com/azure/ai-services/document-intelligence/">Official documentation</a>:</p><h2>Practical Use Case: Chatbots</h2><p>Modern chatbots go far beyond scripted conversations.</p><p>Using large language models, chatbots can:</p><ul><li><p>Answer product questions </p></li><li><p>Explain documentation </p></li><li><p>Guide customers through troubleshooting </p></li><li><p>Escalate complex issues </p></li></ul><p>ASP.NET Core becomes the orchestration layer connecting users with AI services.</p><h2>Recommendation Engines</h2><p>Streaming platforms and online stores rely heavily on recommendations.</p><p>Machine learning analyzes:</p><ul><li><p>Purchase history </p></li><li><p>Viewing habits </p></li><li><p>User preferences<br></p></li></ul><p>Applications can then recommend relevant products automatically.</p><h2>Fraud Detection</h2><p>Financial systems constantly evaluate transactions.</p><p>Machine learning models examine:</p><ul><li><p>Transaction amounts </p></li><li><p>Purchase frequency </p></li><li><p>Device information </p></li><li><p>Geographic location </p></li></ul><p>Suspicious activity receives higher fraud scores.</p><p>Human investigators review only the highest-risk transactions.</p><h2>Image Recognition</h2><p>Applications can analyze uploaded images.</p><p>Examples include:</p><ul><li><p>Detecting damaged vehicles </p></li><li><p>Classifying products </p></li><li><p>Identifying plant diseases </p></li><li><p>Reading handwritten forms </p></li></ul><p>ASP.NET Core uploads the image while specialized AI services perform analysis.</p><h2>Voice Applications</h2><p>Speech services allow applications to:</p><ul><li><p>Convert speech into text. </p></li><li><p>Convert text into speech. </p></li><li><p>Translate conversations.<br></p></li></ul><p>Customer service systems increasingly combine speech recognition with language models.</p><h2>Responsible AI</h2><p>Adding AI introduces new responsibilities.</p><p>Developers should consider:</p><ul><li><p>Privacy </p></li><li><p>Security  </p></li><li><p>Bias </p></li><li><p>Transparency<br></p></li></ul><p>Users should understand when AI generates content or makes recommendations.</p><p>Human review remains important for high-impact decisions.</p><h2>Protecting Sensitive Data</h2><p>Never send unnecessary confidential information to AI services.</p><p>Before submitting requests:</p><ul><li><p>Remove sensitive identifiers. </p></li><li><p>Minimize personal data. </p></li><li><p>Encrypt communication. </p></li><li><p>Follow organizational compliance requirements.<br></p></li></ul><p>This connects directly with our earlier articles on Data Protection and Zero Trust Architecture.</p><h2>Observability</h2><p>AI systems should be monitored like every other service.</p><p>Track:</p><ul><li><p>Response time </p></li><li><p>Token usage </p></li><li><p>Error rates </p></li><li><p>Model latency </p></li><li><p>Request volume<br></p></li></ul><p>OpenTelemetry integrates well with AI-powered applications.</p><p>Observability helps teams understand cost as well as performance.</p><h2>Prompt Engineering</h2><p>Large language models respond according to the instructions they receive.</p><p>Well-designed prompts produce more reliable results.</p><p>Rather than asking:</p><blockquote><p>&#8220;Summarize this.&#8221;</p></blockquote><p>Consider:</p><blockquote><p>&#8220;Summarize this customer complaint in three bullet points highlighting the primary issue, urgency level, and recommended next action.&#8221;</p></blockquote><p>Clear prompts often improve output quality dramatically.</p><h2>Cost Management</h2><p>AI services usually charge per request or per token.</p><p>Applications should:</p><ul><li><p>Cache repeated responses. </p></li><li><p>Limit unnecessary requests. </p></li><li><p>Select appropriate model sizes. </p></li><li><p>Monitor usage.<br></p></li></ul><p>Good architecture reduces operational costs significantly.</p><h2>Real-World Example</h2><p>Imagine a global logistics company.</p><p>Customers upload shipping documents.</p><p>The workflow becomes:</p><ol><li><p>ASP.NET Core receives the document. </p></li><li><p><a href="https://www.asptoday.com/p/deploying-aspnet-core-applications">Azure AI</a> extracts structured information. </p></li><li><p>ML.NET predicts shipping delays. </p></li><li><p>A language model summarizes unusual issues. </p></li><li><p>Results appear inside the operations dashboard. </p></li></ol><p>No single AI model performs everything.</p><p>Each specializes in one task.</p><p>ASP.NET Core coordinates the entire workflow.</p><h2>How This Fits Your ASP.NET Core Journey</h2><p>Throughout this series we&#8217;ve explored:</p><ul><li><p>Microservices </p></li><li><p>API Gateways </p></li><li><p>Service Meshes </p></li><li><p>Distributed Tracing </p></li><li><p>Zero Trust Security </p></li><li><p>Internal Developer Platforms </p></li></ul><p>Artificial Intelligence becomes another service within this architecture.</p><p>Rather than replacing traditional software design, AI enhances existing systems by making them more intelligent, adaptive, and helpful.</p><h2>Closing Thoughts</h2><p>Artificial Intelligence is changing how users interact with software, but successful AI applications still rely on strong software engineering principles.</p><p>ASP.NET Core provides an excellent foundation for integrating AI services, managing workflows, securing sensitive data, and exposing intelligent capabilities through clean APIs.</p><p>Whether you&#8217;re building chatbots, recommendation engines, document processing systems, or predictive analytics platforms, treating AI as one component of a well-designed architecture leads to applications that are easier to maintain, easier to scale, and far more valuable to users.</p><p>The future of software isn&#8217;t simply AI-powered. It&#8217;s thoughtfully engineered systems where traditional software and intelligent services work together to solve real business problems.</p><h2>Subscribe Now</h2><p>Enjoying this series? Subscribe to <strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, cloud-native architecture guides, AI integration strategies, and enterprise development best practices. Join our Substack Chat to discuss modern .NET development with developers from around the world. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Service Mesh Concepts for ASP.NET Core Developers: Traffic, Security, and Observability]]></title><description><![CDATA[Discover how service meshes simplify secure communication, traffic management, and observability for ASP.NET Core microservices. #ASPToday #aspnetcore #servicemesh #microservices #cloudnative]]></description><link>https://www.asptoday.com/p/service-mesh-concepts-for-aspnet</link><guid isPermaLink="false">https://www.asptoday.com/p/service-mesh-concepts-for-aspnet</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 28 Jul 2026 15:02:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Y83A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In our previous article, we explored how <a href="https://www.asptoday.com/p/implementing-api-gateways-in-aspnet">API Gateways</a> provide a single entry point for clients accessing your microservices. But what happens after a request enters your system? Once dozens or even hundreds of services begin communicating with one another, managing traffic, securing every connection, and understanding what is happening inside the network becomes a major challenge.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Y83A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Y83A!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!Y83A!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!Y83A!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!Y83A!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Y83A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2172579,&quot;alt&quot;:&quot;Engineers monitor a holographic service mesh controlling secure traffic between microservices. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/208443421?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Engineers monitor a holographic service mesh controlling secure traffic between microservices. " title="Engineers monitor a holographic service mesh controlling secure traffic between microservices. " srcset="https://substackcdn.com/image/fetch/$s_!Y83A!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!Y83A!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!Y83A!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!Y83A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f247eba-6a9a-4072-9d30-4e4a53a3a782_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is where a service mesh comes in. Rather than adding networking logic to every <a href="https://www.asptoday.com/">ASP.NET Core</a> application, a service mesh transparently manages communication between services, providing traffic control, encryption, security policies, and deep observability without requiring changes to your application code. </p><h2>Life Before a Service Mesh</h2><p>Imagine an online retail platform built with ASP.NET Core.</p><p>It contains separate services for:</p><ul><li><p>Products</p></li><li><p>Orders</p></li><li><p>Inventory</p></li><li><p>Payments</p></li><li><p>Shipping</p></li><li><p>Notifications</p></li><li><p>Customer Accounts</p></li></ul><p>The API Gateway successfully routes incoming requests.</p><p>However, after the request enters your platform, the services begin talking to one another.</p><p>An order service might contact inventory, payment, shipping, and notification services during a single customer purchase.</p><p>As the system grows, each service must handle:</p><ul><li><p>Secure communication</p></li><li><p><a href="https://www.asptoday.com/p/authentication-and-authorization">Authentication</a></p></li><li><p>Retries</p></li><li><p>Timeouts</p></li><li><p>Load balancing</p></li><li><p>Logging</p></li><li><p><a href="https://www.asptoday.com/p/deep-dive-into-distributed-tracing">Distributed tracing</a></p></li><li><p>Encryption</p></li><li><p>Circuit breakers</p></li></ul><p>Every development team ends up writing nearly identical networking code.</p><p>Maintaining that code across dozens of services quickly becomes difficult.</p><h2>What Is a Service Mesh?</h2><p>A service mesh is dedicated infrastructure that manages communication between your microservices.</p><p>Instead of placing networking logic inside every ASP.NET Core application, the mesh handles those responsibilities automatically.</p><p>Your application continues focusing on business logic while the service mesh manages communication behind the scenes.</p><p>Unlike an API Gateway, which handles traffic entering your platform, a service mesh focuses on traffic moving inside your platform.</p><h2>Understanding Sidecar Proxies</h2><p>The heart of a service mesh is the sidecar proxy.</p><p>Each ASP.NET Core service runs alongside its own lightweight proxy.</p><pre><code><code>Client
   &#9474;
   &#9660;
API Gateway
   &#9474;
   &#9660;
Order Service &#8592;&#8594; Sidecar Proxy
                     &#9474;
                     &#9660;
            Inventory Sidecar
                     &#9474;
                     &#9660;
           Inventory Service</code></code></pre><p>Every request passes through these proxies.</p><p>The application never communicates directly with another service.</p><p>Instead, the sidecars handle networking responsibilities.</p><h2>Why Sidecars Matter</h2><p>Because every request flows through a proxy, the service mesh can automatically provide:</p><ul><li><p>Mutual TLS encryption </p></li><li><p>Traffic routing </p></li><li><p>Load balancing </p></li><li><p>Retries </p></li><li><p>Timeouts </p></li><li><p>Observability </p></li><li><p><a href="https://www.asptoday.com/p/authentication-and-authorization">Authentication</a> </p></li><li><p>Authorization </p></li><li><p>Policy enforcement </p></li></ul><p>Your ASP.NET Core applications remain clean and focused.</p><h2>Traffic Management</h2><p>Suppose version two of your Product Service has just been deployed.</p><p>Instead of sending every request to the new version immediately, the mesh can gradually shift traffic.</p><p>For example:</p><ul><li><p>95% of requests continue using Version 1 </p></li><li><p>5% use Version 2</p></li></ul><p>If everything performs well, the percentage gradually increases.</p><p>This approach enables safer deployments.</p><h2>Canary Releases</h2><p>Canary deployments become extremely simple.</p><p>Rather than modifying application code, the service mesh controls how traffic flows between service versions.</p><p>If problems appear, traffic can immediately return to the previous version.</p><p>No code changes are required.</p><h2>Load Balancing</h2><p>Suppose your Inventory Service now runs on six different containers.</p><p>The service mesh distributes traffic automatically across healthy instances.</p><p>Applications simply call Inventory Service.</p><p>The mesh decides which instance should receive the request.</p><h2>Automatic Retries</h2><p>Temporary failures happen frequently.</p><p>A container may restart.</p><p>A network connection might briefly fail.</p><p>Instead of every ASP.NET Core application implementing retry logic independently, the mesh retries failed requests automatically according to configurable policies.</p><h2>Timeouts</h2><p>Long-running requests can consume valuable resources.</p><p>The mesh applies timeout policies before failures spread throughout the system.</p><p>Applications don&#8217;t need to implement timeout logic repeatedly.</p><h2>Circuit Breaking</h2><p>We previously discussed circuit breakers.</p><p>Service meshes implement them automatically.</p><p>When a downstream service becomes unhealthy, the mesh temporarily stops forwarding requests.</p><p>This prevents cascading failures across your platform.</p><h2>Security Between Services</h2><p>One of the biggest advantages of a service mesh is security.</p><p>Without a mesh, developers often need to configure secure communication manually.</p><p>With a mesh, secure communication becomes the default.</p><h2>Mutual TLS</h2><p>Most developers are familiar with HTTPS protecting communication between browsers and servers.</p><p>Inside a microservices platform, every service should also verify the identity of every other service.</p><p>Service meshes accomplish this using Mutual TLS (mTLS).</p><p>Every connection becomes:</p><ul><li><p>Authenticated </p></li><li><p>Encrypted </p></li><li><p>Verified </p></li></ul><p>No application code changes are required.</p><h2>Identity Instead of IP Addresses</h2><p>Traditional systems often trust network locations.</p><p>Modern service meshes trust service identities instead.</p><p>Even if containers move between servers, identities remain consistent.</p><p>This aligns closely with the Zero Trust principles discussed in earlier articles.</p><h2>Policy Enforcement</h2><p>Administrators can define rules such as:</p><ul><li><p>Payment Service can call Inventory Service. </p></li><li><p>Notification Service cannot call Payment Service. </p></li><li><p>Reporting Service has read-only access. </p></li></ul><p>Policies are managed centrally rather than inside dozens of applications.</p><h2>Observability</h2><p>Understanding distributed systems is difficult.</p><p>One customer request may travel through fifteen services.</p><p>Finding performance problems without visibility becomes almost impossible.</p><p>Service meshes automatically collect <a href="https://www.asptoday.com/p/deep-dive-into-distributed-tracing">telemetry</a>.</p><h2>Metrics</h2><p>Without changing your code, the mesh records:</p><ul><li><p>Request count </p></li><li><p>Latency </p></li><li><p>Success rate </p></li><li><p>Error rate </p></li><li><p>Response times </p></li></ul><p>These metrics help teams understand application health.</p><h2>Distributed Tracing</h2><p>Each request receives a trace identifier.</p><p>As requests move between services, the mesh records every step.</p><p>Developers can visualize exactly where delays occur.</p><p>This works beautifully alongside OpenTelemetry, which we explored in our previous observability article.</p><h2>Logging</h2><p>Because every request passes through sidecars, the mesh automatically records communication between services.</p><p>This simplifies troubleshooting without requiring custom logging logic in every application.</p><h2>Popular Service Mesh Technologies</h2><p>Several mature service mesh platforms are widely used.</p><p>The most common include:</p><ul><li><p>Istio </p></li><li><p>Linkerd </p></li><li><p>Kuma </p></li><li><p>Consul Connect </p></li></ul><p>Most integrate naturally with <a href="https://www.asptoday.com/p/aspnet-core-and-kubernetes-from-development">Kubernetes</a>, where ASP.NET Core applications are commonly deployed. </p><h2>Does ASP.NET Core Need Special Code?</h2><p>One of the biggest advantages is that your application usually requires very little modification. </p><p>Your ASP.NET Core services continue communicating using standard HTTP or gRPC.</p><p>The mesh intercepts traffic transparently.</p><p>Business logic remains completely separate from networking infrastructure.</p><h2>API Gateway vs Service Mesh</h2><p>These technologies complement one another rather than compete.</p><p>API Gateway responsibilities include:</p><ul><li><p>Client authentication </p></li><li><p>External routing </p></li><li><p>Rate limiting </p></li><li><p>Public APIs </p></li></ul><p>Service Mesh responsibilities include:</p><ul><li><p>Internal <a href="https://www.asptoday.com/p/routing-in-aspnet-core-beginners-guide">routing</a> </p></li><li><p>Service authentication </p></li><li><p>Mutual TLS </p></li><li><p>Retries </p></li><li><p>Traffic shaping </p></li><li><p>Observability </p></li><li><p>Policy enforcement </p></li></ul><p>Many modern cloud-native systems use both together.</p><h2>Common Mistakes</h2><p>Avoid expecting the service mesh to replace good application design.</p><p>A mesh improves networking, but it does not replace:</p><ul><li><p>Clean architecture </p></li><li><p>Domain boundaries </p></li><li><p>Reliable code </p></li><li><p>Proper error handling </p></li></ul><p>It should enhance your platform, not compensate for poor software design.</p><h2>Real-World Example</h2><p>Imagine a global airline booking platform.</p><p>A customer books a flight.</p><p>The API Gateway accepts the request.</p><p>Behind the scenes:</p><ul><li><p>Booking Service contacts Inventory. </p></li><li><p>Inventory contacts Pricing. </p></li><li><p>Pricing contacts Loyalty. </p></li><li><p>Booking contacts Payments. </p></li><li><p>Payments contacts Notifications. </p></li></ul><p>The customer sees a single confirmation screen.</p><p>Meanwhile, the service mesh securely manages dozens of encrypted service-to-service conversations, balances traffic, retries temporary failures, collects telemetry, and records distributed traces without requiring each application to implement those capabilities individually.</p><h2>How This Fits Your ASP.NET Core Journey</h2><p>So far, we&#8217;ve explored:</p><ul><li><p>API Gateways </p></li><li><p>Zero Trust Security </p></li><li><p>Distributed Tracing </p></li><li><p><a href="https://www.asptoday.com/p/chaos-engineering-for-aspnet-core">Chaos Engineering</a> </p></li><li><p>Internal Developer Platforms </p></li><li><p>Advanced Authorization </p></li></ul><p>Service meshes tie these concepts together by providing a secure, observable communication layer for modern microservices.</p><h2>Closing Thoughts</h2><p>As ASP.NET Core applications continue evolving toward cloud-native architectures, reliable communication between services becomes just as important as the services themselves.</p><p>A service mesh removes much of the complexity traditionally embedded inside application code. By managing traffic, encryption, retries, authentication, and observability at the infrastructure layer, it allows development teams to focus on building business features instead of repeatedly solving networking challenges.</p><p>Whether you&#8217;re deploying a handful of services or operating hundreds across Kubernetes clusters, understanding service mesh concepts will help you build systems that are more secure, more resilient, and significantly easier to operate.</p><h2>Subscribe Now</h2><p>If you&#8217;re enjoying this cloud-native architecture series, subscribe to <strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, real-world architecture guides, and enterprise development best practices. Join our Substack Chat to discuss modern .NET development with developers building scalable applications around the world. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Implementing API Gateways in ASP.NET Core: Routing, Aggregation, and Security]]></title><description><![CDATA[Discover how API Gateways simplify routing, strengthen security, and improve performance in ASP.NET Core microservices. #ASPToday #aspnetcore #microservices #apigateway #dotnet]]></description><link>https://www.asptoday.com/p/implementing-api-gateways-in-aspnet</link><guid isPermaLink="false">https://www.asptoday.com/p/implementing-api-gateways-in-aspnet</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 21 Jul 2026 15:01:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!UJ-K!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As applications evolve into dozens of independent microservices, managing communication between clients and services becomes increasingly complex. Without a central entry point, clients often need to know where every service lives, how each service authenticates requests, and which endpoints to call. API Gateways solve this problem by acting as a single front door for your applications.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UJ-K!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UJ-K!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!UJ-K!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!UJ-K!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!UJ-K!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UJ-K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2191359,&quot;alt&quot;:&quot;Shoppers gather at a central mall information desk guiding visitors to stores across the complex. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/207617722?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Shoppers gather at a central mall information desk guiding visitors to stores across the complex. " title="Shoppers gather at a central mall information desk guiding visitors to stores across the complex. " srcset="https://substackcdn.com/image/fetch/$s_!UJ-K!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!UJ-K!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!UJ-K!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!UJ-K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc91857-e5af-4787-8d99-2a2f8e2538f9_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we&#8217;ll explore how API Gateways work, how to implement them using <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a>, and how they improve routing, security, scalability, and developer experience in modern distributed systems. </p><h2>Why Microservices Need a Front Door</h2><p>Imagine an online shopping application built using microservices.</p><p>Instead of one application, you now have:</p><ul><li><p>Product Service</p></li><li><p>Orders Service</p></li><li><p>Inventory Service</p></li><li><p>Payment Service</p></li><li><p>Shipping Service</p></li><li><p>Notification Service</p></li></ul><p>If a mobile application communicates directly with every service, several problems appear.</p><p>The client must know:</p><ul><li><p>Where each service lives</p></li><li><p>Which URLs to call</p></li><li><p>Which authentication tokens are required</p></li><li><p>Which service owns which data</p></li></ul><p>Every new microservice increases complexity.</p><p>This quickly becomes difficult to maintain. </p><h2>What Is an API Gateway?</h2><p>An API Gateway sits between clients and your microservices.</p><p>Instead of calling services directly, every request goes through the gateway.</p><pre><code><code>Mobile App
Web App
Desktop App
        &#9474;
        &#9660;
   API Gateway
        &#9474;
 &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
 &#9660;      &#9660;        &#9660;
Orders Products Payments</code></code></pre><p>The gateway becomes the single public entry point.</p><p>Clients only need to know one address. </p><h2>Why API Gateways Matter</h2><p>API Gateways simplify communication.</p><p>Instead of exposing every internal service, the gateway handles:</p><ul><li><p>Routing</p></li><li><p>Authentication</p></li><li><p>Authorization</p></li><li><p>Rate limiting</p></li><li><p>Request aggregation</p></li><li><p>Logging</p></li><li><p>Monitoring</p></li><li><p>Response transformation</p></li></ul><p>Microservices remain focused on business logic.</p><p>The gateway handles cross-cutting concerns. </p><h2>Understanding Request Routing</h2><p>The primary responsibility of an API Gateway is routing.</p><p>For example:</p><pre><code><code>/api/products
        &#9474;
        &#9660;
Product Service

/api/orders
        &#9474;
        &#9660;
Orders Service

/api/payments
        &#9474;
        &#9660;
Payment Service</code></code></pre><p>Clients don&#8217;t need to know where services are hosted.</p><p>The gateway forwards requests automatically. </p><h2>Building an API Gateway with ASP.NET Core</h2><p>ASP.NET Core provides several options.</p><p>One of the most popular is <strong>YARP (Yet Another Reverse Proxy)</strong>, developed by Microsoft.</p><p><a href="https://microsoft.github.io/reverse-proxy/">YARP documentation</a></p><p>Install:</p><pre><code><code>dotnet add package Yarp.ReverseProxy</code></code></pre><h2>Configuring YARP</h2><p>Register the reverse proxy.</p><pre><code><code>builder.Services
    .AddReverseProxy()
    .LoadFromConfig(
        builder.Configuration
    .GetSection("ReverseProxy"));</code></code></pre><p>Then enable it.</p><pre><code><code>app.MapReverseProxy();</code></code></pre><p>Your gateway is now ready to forward requests. </p><h2>Configuring Routes</h2><p>Example configuration:</p><pre><code><code>{
  "ReverseProxy": {
    "Routes": {
      "products": {
        "ClusterId": "products",
        "Match": {
          "Path": "/products/{**catch-all}"
        }
      }
    }
  }
}</code></code></pre><p>Clients continue calling one API while YARP forwards traffic to the correct service. </p><h2>Request Aggregation</h2><p>Sometimes a client needs data from multiple services.</p><p>Imagine loading a product page.</p><p>The application needs:</p><ul><li><p>Product details</p></li><li><p>Inventory</p></li><li><p>Reviews</p></li><li><p>Pricing</p></li></ul><p>Without a gateway:</p><p>The browser makes four separate requests.</p><p>With aggregation:</p><p>The browser makes one request.</p><p>The gateway gathers information from multiple services before returning a single response.</p><p>This reduces network traffic and improves performance. </p><h2>Example Aggregation Flow</h2><pre><code><code>Client
   &#9474;
   &#9660;
Gateway
 &#9500;&#9472;&#9472; Product Service
 &#9500;&#9472;&#9472; Inventory Service
 &#9500;&#9472;&#9472; Review Service
 &#9492;&#9472;&#9472; Pricing Service
        &#9474;
        &#9660;
 Combined Response</code></code></pre><p>The client receives one complete response. </p><h2>Authentication at the Gateway</h2><p>One of the biggest advantages of an API Gateway is centralized authentication.</p><p>Instead of every service validating JWT tokens independently, the gateway performs the initial authentication.</p><p>Example:</p><pre><code><code>builder.Services
.AddAuthentication()
.AddJwtBearer();</code></code></pre><p>Only authenticated requests reach internal services.</p><p>This simplifies service development.</p><h2>Authorization</h2><p>Authentication identifies users.</p><p>Authorization determines what they can access.</p><p>Policy-based authorization works well inside gateways.</p><p>Example:</p><pre><code><code>[Authorize(Policy = "Orders.Read")]</code></code></pre><p>Unauthorized requests are rejected before reaching downstream services. </p><h2>Rate Limiting</h2><p>Gateways are an ideal location for rate limiting.</p><p>ASP.NET Core includes built-in support.</p><p>Example:</p><pre><code><code>builder.Services.AddRateLimiter(options =&gt;
{
    options.AddFixedWindowLimiter(
        "api",
        limiter =&gt;
        {
            limiter.PermitLimit = 100;
            limiter.Window =
                TimeSpan.FromMinutes(1);
        });
});</code></code></pre><p>Rate limiting protects services from abuse. </p><h2>Response Caching</h2><p>Frequently requested data can be cached.</p><p>Instead of contacting the Product Service repeatedly, the gateway serves cached responses.</p><p>Benefits include:</p><ul><li><p>Faster responses</p></li><li><p>Reduced backend load</p></li><li><p>Lower infrastructure costs</p></li></ul><p>Caching should be applied carefully to avoid serving stale data. </p><h2>Load Balancing</h2><p>A Product Service may run multiple instances.</p><pre><code><code>Gateway
   &#9474;
 &#9484;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
 &#9660; &#9660; &#9660;
Instance A
Instance B
Instance C</code></code></pre><p>The gateway distributes traffic across available instances.</p><p>Users experience higher availability without knowing multiple servers exist. </p><h2>Service Discovery</h2><p>Microservices often change location.</p><p>Containers restart.</p><p>Pods move.</p><p>New instances appear.</p><p>Instead of hardcoding addresses, the gateway integrates with service discovery solutions.</p><p>Examples include:</p><ul><li><p>Kubernetes</p></li><li><p>Consul</p></li><li><p>Azure Container Apps</p></li></ul><p>Requests always reach healthy instances. </p><h2>Circuit Breakers</h2><p>If a downstream service fails, the gateway should avoid repeatedly sending requests.</p><p>Circuit breakers temporarily stop traffic to unhealthy services.</p><p>This improves overall system stability.</p><p>It also complements the resilience strategies we explored in previous articles. </p><h2>Logging Requests</h2><p>Every request passes through the gateway.</p><p>This makes it an excellent place to capture:</p><ul><li><p>Request paths</p></li><li><p>Response codes</p></li><li><p>Processing times</p></li><li><p>Client IP addresses</p></li></ul><p>These logs become valuable for troubleshooting. </p><h2>Distributed Tracing</h2><p>The gateway also plays an important role in observability.</p><p>Using OpenTelemetry, it can:</p><ul><li><p>Start trace spans</p></li><li><p>Propagate trace context</p></li><li><p>Record latency</p></li><li><p>Capture downstream service performance</p></li></ul><p>This connects directly with our earlier article on distributed tracing. </p><h2>API Versioning</h2><p>Gateways simplify API evolution.</p><p>Clients may continue using:</p><pre><code><code>/api/v1/orders</code></code></pre><p>While newer clients use:</p><pre><code><code>/api/v2/orders</code></code></pre><p>The gateway routes requests appropriately without exposing internal service complexity. </p><h2>Security Benefits</h2><p>The gateway strengthens security by:</p><ul><li><p>Hiding internal services</p></li><li><p>Centralizing authentication</p></li><li><p>Applying authorization policies</p></li><li><p>Enforcing HTTPS</p></li><li><p>Performing request validation</p></li><li><p>Blocking malicious traffic</p></li></ul><p>Internal services remain inaccessible from the public internet. </p><h2>Common Mistakes</h2><p>Avoid making the gateway responsible for business logic.</p><p>The gateway should coordinate requests.</p><p>Business decisions belong inside services.</p><p>Also avoid:</p><ul><li><p>Creating a single massive gateway</p></li><li><p>Hardcoding routes</p></li><li><p>Ignoring monitoring</p></li><li><p>Forgetting rate limiting</p></li><li><p>Bypassing authentication</p></li></ul><p>A lightweight gateway is easier to maintain. </p><h2>Real-World Example</h2><p>Imagine an airline booking platform.</p><p>Customers use one mobile app.</p><p>Behind the scenes the gateway communicates with:</p><ul><li><p>Flight Service</p></li><li><p>Booking Service</p></li><li><p>Payment Service</p></li><li><p>Loyalty Service</p></li><li><p>Notification Service</p></li></ul><p>The mobile app makes a single request.</p><p>The gateway handles routing, authentication, aggregation, and response formatting.</p><p>The customer experiences one seamless interaction despite multiple backend services working together. </p><h2>How This Fits Your ASP.NET Core Journey</h2><p>Our recent articles explored:</p><ul><li><p>Microservices</p></li><li><p>Zero Trust Architecture</p></li><li><p>Data Protection</p></li><li><p>Advanced Authorization</p></li><li><p>Internal Developer Platforms</p></li></ul><p>API Gateways bring these ideas together.</p><p>They become the secure front door through which every request enters your distributed system.</p><p>They enforce security, simplify communication, improve observability, and create a better experience for both developers and users. </p><h2>Closing Thoughts</h2><p>As applications continue moving toward microservices, API Gateways become an essential architectural component.</p><p>They reduce client complexity, centralize security, simplify routing, and provide a single location for cross-cutting concerns such as authentication, logging, caching, and monitoring.</p><p>ASP.NET Core, together with Microsoft&#8217;s YARP project, provides an excellent foundation for building lightweight, high-performance API Gateways that scale with modern cloud-native applications.</p><p>Rather than exposing dozens of services directly, let your gateway become the intelligent front door that protects, organizes, and simplifies your entire platform. </p><h2>Join The Community</h2><p>Enjoyed this article? <a href="https://www.asptoday.com/">Subscribe to </a><strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core tutorials, cloud-native architecture guides, and enterprise development best practices. Join the Substack Chat to discuss modern ASP.NET Core development with developers building scalable applications around the world. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Building Internal Developer Platforms with ASP.NET Core: Tools, APIs, and Automation]]></title><description><![CDATA[Discover how Internal Developer Platforms help teams ship software faster using ASP.NET Core, self-service APIs, automation, and reusable developer tools. #ASPToday #aspnetcore #platformengineering #devops #dotnet]]></description><link>https://www.asptoday.com/p/building-internal-developer-platforms</link><guid isPermaLink="false">https://www.asptoday.com/p/building-internal-developer-platforms</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 14 Jul 2026 15:02:02 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!kRwv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As organizations grow, software development often becomes slower rather than faster. Teams spend increasing amounts of time requesting infrastructure, configuring environments, creating new projects, managing deployments, and solving repetitive operational problems. Internal Developer Platforms (IDPs) address these challenges by providing developers with self-service tools, standardized APIs, reusable templates, and automated workflows that reduce friction and accelerate delivery. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kRwv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kRwv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!kRwv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!kRwv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!kRwv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kRwv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2506893,&quot;alt&quot;:&quot;Developers use a self-service LEGO workshop to instantly build new software projects. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/206658434?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Developers use a self-service LEGO workshop to instantly build new software projects. " title="Developers use a self-service LEGO workshop to instantly build new software projects. " srcset="https://substackcdn.com/image/fetch/$s_!kRwv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!kRwv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!kRwv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!kRwv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F773fce55-8ac3-493a-8c84-3cdf60faee11_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we&#8217;ll explore how to build an Internal Developer Platform using <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> and create a foundation that allows development teams to focus on writing business features instead of managing infrastructure. </p><h2>Why Development Slows as Teams Grow</h2><p>A small startup might have:</p><ul><li><p>Five developers </p></li><li><p>One application </p></li><li><p>One deployment pipeline</p></li></ul><p>Everyone knows where everything is.</p><p>Need a database?</p><p>Someone creates one.</p><p>Need a new API?</p><p>Someone builds it.</p><p>Need production access?</p><p>Just ask.</p><p>Everything works.</p><p>Until it doesn&#8217;t.</p><p>As organizations grow, so do requests.</p><p>Developers begin asking questions like:</p><ul><li><p>Can someone create a Kubernetes namespace?</p></li><li><p>Can I get a SQL database?</p></li><li><p>Who creates <a href="https://www.asptoday.com/p/aspnet-core-and-azure-service-bus">Azure</a> resources?</p></li><li><p>Where do I request secrets?</p></li><li><p>How do I deploy to staging?</p></li><li><p>Which logging standard should I follow?</p></li></ul><p>Eventually engineers spend more time waiting than building. </p><h2>What Is an Internal Developer Platform?</h2><p>An Internal Developer Platform is software built for developers inside an organization.</p><p>Instead of manually requesting resources, developers use a platform to provision them themselves.</p><p>Think of it as an internal app store.</p><p>Instead of installing games, developers request:</p><ul><li><p>New projects</p></li><li><p>Databases</p></li><li><p>Storage accounts</p></li><li><p>API keys</p></li><li><p><a href="https://www.asptoday.com/p/aspnet-core-and-kubernetes-from-development">Kubernetes</a> environments</p></li><li><p>Deployment pipelines</p></li></ul><p>Everything follows company standards automatically. </p><h2>Why Organizations Build Internal Platforms</h2><p>The biggest goal is reducing developer friction.</p><p>Without an IDP:</p><p>Developer writes ticket.</p><p>Operations creates infrastructure.</p><p>Security reviews access.</p><p>Platform team configures deployment.</p><p>Developer waits.</p><p>With an IDP:</p><p>Developer clicks a button.</p><p>Platform performs everything automatically.</p><p>The developer continues working. </p><h2>Platform Engineering vs DevOps</h2><p>These terms are closely related.</p><p>DevOps focuses on collaboration between development and operations.</p><p>Platform Engineering creates products that help developers succeed.</p><p>Instead of manually helping every team, platform engineers build reusable tools.</p><p>Those tools become the Internal Developer Platform. </p><h2>Understanding Self-Service Development</h2><p>Imagine buying groceries.</p><p>Years ago, every item required asking a shop assistant.</p><p>Today we simply use self-checkout.</p><p>Internal platforms work the same way.</p><p>Instead of requesting resources manually, developers provision them independently.</p><p>The platform performs all the complicated work behind the scenes.</p><h2>What Can an Internal Developer Platform Provide?</h2><p>A mature platform might offer:</p><ul><li><p>Project templates</p></li><li><p>API creation</p></li><li><p>Infrastructure provisioning</p></li><li><p>Secret management</p></li><li><p>Logging configuration</p></li><li><p>Monitoring dashboards</p></li><li><p>CI/CD pipelines</p></li><li><p>Deployment approvals</p></li><li><p>Security scanning</p></li><li><p>Documentation</p></li></ul><p>Developers interact with one consistent interface. </p><h2>Why ASP.NET Core Is a Great Choice</h2><p>ASP.NET Core excels at building internal tools.</p><p>It offers:</p><ul><li><p>High performance</p></li><li><p>Cross-platform support</p></li><li><p>Excellent security</p></li><li><p>REST API development</p></li><li><p>Background services</p></li><li><p>Identity integration</p></li><li><p>Cloud-native capabilities</p></li></ul><p>These features make it ideal for building platform APIs. </p><h2>Designing the Platform</h2><p>Think of the platform as multiple services working together.</p><pre><code><code>Developer Portal
        &#9474;
        &#9660;
Platform API
        &#9474;
 &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
 &#9660;      &#9660;        &#9660;
Templates Deployments Infrastructure
        &#9474;
        &#9660;
Cloud Resources</code></code></pre><p>Each component has a specific responsibility.</p><p>This separation keeps the platform maintainable. </p><h2>Building the Platform API</h2><p>Everything begins with an API.</p><p>Developers request services through HTTP endpoints.</p><p>Example:</p><pre><code><code>POST /projects</code></code></pre><p>Payload:</p><pre><code><code>{
  "name": "InventoryService",
  "template": "webapi"
}</code></code></pre><p>The platform performs the remaining work automatically. </p><h2>Creating the API</h2><p>Example:</p><pre><code><code>app.MapPost("/projects",
(ProjectRequest request) =&gt;
{
    return Results.Ok(
        $"Creating {request.Name}");
});</code></code></pre><p>Simple APIs often orchestrate much larger workflows. </p><h2>Automating Project Creation</h2><p>Suppose every microservice requires:</p><ul><li><p>Standard folder structure</p></li><li><p>Logging</p></li><li><p>Health checks</p></li><li><p>Docker support</p></li><li><p>OpenTelemetry</p></li><li><p>Authentication</p></li></ul><p>Instead of manually configuring each project, developers choose a template.</p><p>The platform generates everything consistently. </p><h2>Project Templates</h2><p>Templates eliminate repetitive work.</p><p>A template might include:</p><ul><li><p>Authentication configured</p></li><li><p>Swagger enabled</p></li><li><p>Logging configured</p></li><li><p>Dockerfile included</p></li><li><p>CI/CD pipeline ready</p></li><li><p>Unit tests added</p></li></ul><p>Every new service begins with the same high-quality foundation.</p><h2>Managing Templates</h2><p>Templates evolve over time.</p><p>For example:</p><p>Version 1:</p><ul><li><p>.NET 8</p></li><li><p>Basic logging</p></li></ul><p>Version 2:</p><ul><li><p>OpenTelemetry</p></li><li><p>Health checks</p></li><li><p>Distributed tracing</p></li><li><p>Rate limiting</p></li></ul><p>Developers automatically benefit from platform improvements. </p><h2>Infrastructure as Code</h2><p>Infrastructure should never be created manually.</p><p>Instead use:</p><ul><li><p>Bicep</p></li><li><p>Terraform</p></li><li><p>ARM Templates</p></li></ul><p>The platform invokes these tools automatically.</p><p>Developers simply request resources. </p><h2>Provisioning Cloud Resources</h2><p>Imagine a developer needs:</p><ul><li><p>Azure SQL</p></li><li><p>Azure Storage</p></li><li><p>Service Bus</p></li><li><p>Key Vault</p></li></ul><p>Instead of opening multiple Azure portals, they submit one request.</p><p>The platform provisions everything. </p><h2>Background Processing</h2><p>Some operations take several minutes.</p><p>Examples:</p><ul><li><p>Creating Kubernetes clusters</p></li><li><p>Deploying infrastructure</p></li><li><p>Provisioning databases</p></li></ul><p>ASP.NET Core Background Services handle these long-running jobs efficiently.</p><p>Example:</p><pre><code><code>public class ProvisioningWorker
    : BackgroundService
{
    protected override async Task ExecuteAsync(
        CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            await Task.Delay(5000, token);
        }
    }
}</code></code></pre><p>Background workers keep APIs responsive. </p><h2>Building a Developer Portal</h2><p>The portal becomes the front door of the platform.</p><p>Developers can:</p><ul><li><p>Create projects</p></li><li><p>Request environments</p></li><li><p>View deployments</p></li><li><p>Browse documentation</p></li><li><p>Monitor applications</p></li></ul><p>Everything is available from one location. </p><h2>Authentication</h2><p>Only authorized developers should access platform resources.</p><p><a href="https://www.asptoday.com/p/understanding-aspnet-core-identity">ASP.NET Core Identity</a> integrates easily with:</p><ul><li><p>Microsoft Entra ID</p></li><li><p>Azure AD</p></li><li><p>OAuth</p></li><li><p>OpenID Connect</p></li></ul><p>Authentication ensures platform requests remain secure. </p><h2>Authorization</h2><p>Different developers require different permissions.</p><p>Examples:</p><p>Developers</p><ul><li><p>Create projects</p></li></ul><p>Team Leads</p><ul><li><p>Approve production deployments</p></li></ul><p>Platform Engineers</p><ul><li><p>Manage templates</p></li></ul><p>Administrators</p><ul><li><p>Configure infrastructure</p></li></ul><p>Policy-based authorization keeps permissions manageable.</p><p>This builds directly on concepts from our previous article on advanced authorization. </p><h2>API Versioning</h2><p>Internal platforms evolve continuously.</p><p><a href="https://www.asptoday.com/p/exploring-web-api-versioning-in-aspnet">Versioning</a> prevents breaking existing developer workflows.</p><p>Example:</p><pre><code><code>/api/v1/projects

/api/v2/projects</code></code></pre><p>Existing automation continues working while new features are introduced. </p><h2>Logging Platform Activity</h2><p>Everything the platform does should be logged.</p><p>Examples:</p><ul><li><p>Project created</p></li><li><p>Deployment started</p></li><li><p>Database provisioned</p></li><li><p>Secret generated</p></li></ul><p><a href="https://www.asptoday.com/p/building-audit-logging-systems-in">Audit logging</a> becomes especially valuable for compliance and troubleshooting.</p><p>As discussed in our previous article, audit logs provide accountability across complex systems. </p><h2>Monitoring Platform Health</h2><p>An Internal Developer Platform quickly becomes business-critical.</p><p>Monitor:</p><ul><li><p>API availability</p></li><li><p>Provisioning success rate</p></li><li><p>Deployment duration</p></li><li><p>Queue lengths</p></li><li><p>Failed workflows</p></li></ul><p><a href="https://www.asptoday.com/p/aspnet-core-health-checks-keeping">Health checks</a> and OpenTelemetry provide excellent visibility.</p><p>This connects naturally with our recent observability articles. </p><h2>Building for Reuse</h2><p>One of the biggest goals of an IDP is eliminating duplicated effort.</p><p>Instead of every team solving the same problems independently, the platform provides reusable solutions that everyone benefits from.</p><p>This creates consistency across the entire organization. </p><h2>Coming in Part 2</h2><p>Our Internal Developer Platform can now provide developers with self-service tools and standardized project creation, but there&#8217;s still more work to do before it becomes a truly enterprise-ready platform.</p><p>In Part 2, we&#8217;ll automate <a href="https://www.asptoday.com/p/utilizing-azure-devops-for-cicd-in">CI/CD pipelines</a>, provision development and production environments on demand, manage secrets securely, implement deployment approvals and governance, and explore how to scale the platform across multiple teams. We&#8217;ll finish by building a complete real-world Internal Developer Platform that helps developers deliver software faster while maintaining security, consistency, and operational excellence. </p><h2>Closing Thoughts</h2><p>An Internal Developer Platform is much more than a collection of internal tools. It becomes the foundation that enables development teams to work faster, deliver more consistently, and spend less time on repetitive operational tasks. By providing self-service capabilities, standardized templates, automated infrastructure provisioning, and reusable APIs, organizations can reduce bottlenecks while improving security, reliability, and developer experience. As your platform evolves, you can expand it with automated deployment pipelines, environment provisioning, secret management, governance controls, and observability, transforming it into a central hub that supports every stage of the software development lifecycle.</p><h2>Subscribe Now</h2><p>Enjoyed this article? <a href="https://www.asptoday.com/">Subscribe to ASP Today</a> for practical ASP.NET Core tutorials, architecture deep dives, and real-world engineering guides. Join our Substack Chat to connect with fellow developers, ask questions, and stay up to date with the latest ASP.NET Core best practices.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Building Audit Logging Systems in ASP.NET Core: Tracking Changes and Compliance]]></title><description><![CDATA[Learn how to build reliable audit logging in ASP.NET Core to track user actions, protect sensitive data, and support compliance. #ASPToday #aspnetcore #auditlogging #security #dotnet]]></description><link>https://www.asptoday.com/p/building-audit-logging-systems-in</link><guid isPermaLink="false">https://www.asptoday.com/p/building-audit-logging-systems-in</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 07 Jul 2026 15:02:45 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!fsVb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Protecting an application is only part of the security story. Organizations also need to know who accessed data, what changed, when it changed, and why it changed. Audit logging provides that visibility by creating a trustworthy history of important actions throughout an application.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fsVb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fsVb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png 424w, https://substackcdn.com/image/fetch/$s_!fsVb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png 848w, https://substackcdn.com/image/fetch/$s_!fsVb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!fsVb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fsVb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png" width="1456" height="813" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:813,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9148522,&quot;alt&quot;:&quot;A luxury hotel lobby where guests' actions are logged in large books as glowing data and code. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/205141211?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A luxury hotel lobby where guests' actions are logged in large books as glowing data and code. " title="A luxury hotel lobby where guests' actions are logged in large books as glowing data and code. " srcset="https://substackcdn.com/image/fetch/$s_!fsVb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png 424w, https://substackcdn.com/image/fetch/$s_!fsVb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png 848w, https://substackcdn.com/image/fetch/$s_!fsVb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!fsVb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64a45198-49ad-4075-b78c-aa171b597074_2752x1536.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, you&#8217;ll learn how to build audit logging into <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> applications, automatically capture meaningful events, support regulatory compliance, and create audit trails that remain useful as your applications grow. </p><h2>Why Audit Logging Matters</h2><p>Imagine a customer calls support and says:</p><blockquote><p>&#8220;I never changed my shipping address.&#8221;</p></blockquote><p>A week later another customer reports:</p><blockquote><p>&#8220;My account was deleted.&#8221;</p></blockquote><p>An administrator notices that sensitive information has disappeared from the database.</p><p>Without an audit log, you only know that something happened.</p><p>You don&#8217;t know:</p><ul><li><p>Who made the change</p></li><li><p>When it happened</p></li><li><p>What the previous value was</p></li><li><p>What the new value became</p></li><li><p>Which application made the change</p></li></ul><p>Audit logging fills in these missing details.</p><p>It creates a permanent history of important events that helps developers troubleshoot problems, helps administrators investigate incidents, and helps organizations satisfy compliance requirements. </p><h2>Application Logs vs Audit Logs</h2><p>Many developers assume ordinary application logs are enough.</p><p>They are not.</p><p>Application logs record how the application behaves.</p><p>Examples include:</p><ul><li><p>Exceptions</p></li><li><p>Startup events</p></li><li><p>Performance metrics</p></li><li><p>Debug information</p></li></ul><p>Audit logs answer business questions instead.</p><p>Examples include:</p><ul><li><p>Who updated this customer?</p></li><li><p>Who approved this payment?</p></li><li><p>Who downloaded this report?</p></li><li><p>Who deleted this document?</p></li></ul><p>The two logging systems complement each other.</p><p>They should not replace one another. </p><h2>What Should Be Audited?</h2><p>Not every action deserves an audit record.</p><p>Focus on events that affect business operations or security.</p><p>Examples include:</p><ul><li><p>User logins</p></li><li><p>Failed login attempts</p></li><li><p>Password changes</p></li><li><p>Profile updates</p></li><li><p>Record creation</p></li><li><p>Record modification</p></li><li><p>Record deletion</p></li><li><p>Permission changes</p></li><li><p>Role assignments</p></li><li><p>Administrative actions</p></li><li><p>Data exports</p></li><li><p>Financial approvals</p></li></ul><p>Auditing every tiny event often creates unnecessary noise.</p><p>Choose events that would matter during an investigation. </p><h2>Designing an Audit Record</h2><p>Every audit entry should answer five simple questions.</p><p>Who? </p><p>What? </p><p>When? </p><p>Where? </p><p>Why? </p><p>A typical audit record might contain:</p><ul><li><p>User ID</p></li><li><p>Username</p></li><li><p>Action performed</p></li><li><p>Entity affected</p></li><li><p>Previous values</p></li><li><p>New values</p></li><li><p>Timestamp</p></li><li><p>IP address</p></li><li><p>Browser or device</p></li><li><p>Correlation ID</p></li></ul><p>The more context captured, the more useful the audit log becomes. </p><h2>A Simple Audit Model</h2><p>Example:</p><pre><code><code>public class AuditLog
{
    public int Id { get; set; }

    public string UserId { get; set; }

    public string Action { get; set; }

    public string Entity { get; set; }

    public string OldValues { get; set; }

    public string NewValues { get; set; }

    public DateTime Timestamp { get; set; }

    public string IpAddress { get; set; }
}</code></code></pre><p>This model provides a solid starting point.</p><p>Real-world systems often add more metadata. </p><h2>Capturing User Identity</h2><p>Audit logs become much more valuable when every action is linked to a specific user.</p><p>ASP.NET Core makes this easy.</p><p>Example:</p><pre><code><code>var username = User.Identity?.Name;</code></code></pre><p>For applications using JWT authentication:</p><pre><code><code>var userId =
    User.FindFirst("sub")?.Value;</code></code></pre><p>Capturing user identity should be standard practice. </p><h2>Recording Request Information</h2><p>Sometimes the request itself provides useful evidence.</p><p>Consider recording:</p><ul><li><p>IP address</p></li><li><p>HTTP method</p></li><li><p>URL</p></li><li><p>User agent</p></li><li><p>Correlation ID</p></li></ul><p>Example:</p><pre><code><code>var ip =
    HttpContext.Connection
        .RemoteIpAddress?
        .ToString();</code></code></pre><p>These details help reconstruct what happened during investigations. </p><h2>Automatically Auditing Entity Framework Core</h2><p>One of the easiest ways to implement audit logging is by intercepting Entity Framework Core changes.</p><p>Every tracked entity already knows whether it has been:</p><ul><li><p>Added</p></li><li><p>Modified</p></li><li><p>Deleted</p></li></ul><p>This makes EF Core an excellent source of audit events. </p><h2>Detecting Changes</h2><p>Example:</p><pre><code><code>foreach (var entry in
    ChangeTracker.Entries())
{
    if (entry.State ==
        EntityState.Modified)
    {
        // Create audit record
    }
}</code></code></pre><p>Instead of manually writing audit code throughout the application, you centralize it in one place.</p><p>This greatly simplifies maintenance. </p><h2>Overriding SaveChanges</h2><p>A common pattern is overriding SaveChangesAsync.</p><p>Example:</p><pre><code><code>public override async Task&lt;int&gt;
SaveChangesAsync(
CancellationToken cancellationToken)
{
    CreateAuditEntries();

    return await
        base.SaveChangesAsync(
            cancellationToken);
}</code></code></pre><p>Every database change automatically produces an audit record.</p><p>Developers no longer need to remember to create one manually. </p><h2>Capturing Old and New Values</h2><p>Suppose a customer changes their email address.</p><p>Instead of recording only:</p><blockquote><p>Customer updated</p></blockquote><p>Capture:</p><pre><code><code>Old:
john@example.com

New:
john.smith@example.com</code></code></pre><p>This information becomes invaluable during troubleshooting. </p><h2>Recording Authentication Events</h2><p>Authentication activity should also be audited.</p><p>Examples include:</p><ul><li><p>Successful logins</p></li><li><p>Failed logins</p></li><li><p>Account lockouts</p></li><li><p>Password resets</p></li><li><p>Multi-factor authentication failures</p></li></ul><p>Security teams frequently investigate these events. </p><h2>Recording Authorization Events</h2><p>Authorization changes often have significant business impact.</p><p>Examples include:</p><ul><li><p>Administrator granted</p></li><li><p>Permission revoked</p></li><li><p>Role changed</p></li><li><p>Access denied</p></li></ul><p>These records help identify accidental or unauthorized permission changes. </p><h2>Soft Deletes vs Hard Deletes</h2><p>Deleting data permanently removes valuable evidence.</p><p>Many organizations prefer soft deletes.</p><p>Instead of removing a record:</p><pre><code><code>IsDeleted = true</code></code></pre><p>The audit log records:</p><ul><li><p>Who deleted it </p></li><li><p>When </p></li><li><p>Why </p></li></ul><p>The record remains recoverable if needed. </p><h2>Tamper Resistance</h2><p>Audit logs should not be easy to modify.</p><p>Otherwise attackers can simply erase their tracks.</p><p>Common approaches include:</p><ul><li><p>Write-once storage</p></li><li><p>Append-only databases</p></li><li><p>Restricted permissions</p></li><li><p>Digital signatures</p></li></ul><p>An audit log should be treated as evidence. </p><h2>Separating Audit Logs from Application Logs</h2><p>Do not mix audit logs with application diagnostics.</p><p>Instead:</p><ul><li><p>Application logs &#8594; troubleshooting</p></li><li><p>Audit logs &#8594; accountability</p></li></ul><p>Keeping them separate improves security and simplifies compliance reporting. </p><h2>Correlation IDs</h2><p>Modern applications often span multiple services.</p><p>A single customer request might involve:</p><ul><li><p>API Gateway</p></li><li><p>Orders Service</p></li><li><p>Inventory Service</p></li><li><p>Payment Service</p></li></ul><p>Using a Correlation ID allows every audit record to be connected to the same business operation. </p><p>Example:</p><pre><code><code>var correlationId =
    HttpContext.TraceIdentifier;</code></code></pre><p>This becomes especially useful in distributed systems.</p><h2>Audit Logging in Microservices</h2><p>Microservices introduce new challenges.</p><p>Each service produces its own audit events.</p><p>Questions include:</p><ul><li><p>Should every service keep its own audit log?</p></li><li><p>Should logs be centralized?</p></li><li><p>How are timestamps synchronized?</p></li></ul><p>Many organizations collect audit events into a centralized platform for easier analysis. </p><h2>Asynchronous Audit Logging</h2><p>Writing audit records synchronously can slow applications.</p><p>Instead consider:</p><ul><li><p>Background queues</p></li><li><p>Azure Service Bus</p></li><li><p>RabbitMQ</p></li></ul><p>Applications remain responsive while audit records are written in the background.</p><p>This connects nicely with our earlier article on messaging systems. </p><h2>Protecting Audit Data</h2><p>Audit logs themselves often contain sensitive information.</p><p>Protect them using:</p><ul><li><p>Encryption</p></li><li><p>Restricted access</p></li><li><p>Backup policies</p></li><li><p>Data retention rules</p></li></ul><p>Never assume audit data is harmless. </p><h2>Compliance Requirements</h2><p>Many regulations require organizations to maintain audit trails.</p><p>Examples include:</p><h3>GDPR</h3><p><a href="https://gdpr.eu/">Requires accountability for personal data access.</a></p><h3>HIPAA</h3><p><a href="https://www.hhs.gov/hipaa">Requires auditing of healthcare information.</a></p><h3>PCI DSS</h3><p><a href="https://www.pcisecuritystandards.org/">Requires tracking access to payment information.</a></p><h3>SOC 2</h3><p><a href="https://www.aicpa.org/">Emphasizes monitoring and accountability.</a></p><p>Audit logging helps organizations demonstrate compliance.</p><h2>What Audit Logs Should Not Contain</h2><p>Avoid recording:</p><ul><li><p>Passwords</p></li><li><p>Encryption keys</p></li><li><p>Credit card numbers</p></li><li><p>Authentication tokens</p></li><li><p>Sensitive secrets</p></li></ul><p>Instead record enough information to investigate activity without exposing confidential data. </p><h2>Common Mistakes</h2><p>Several mistakes appear repeatedly.</p><h3>Logging Too Much</h3><p>Huge audit tables become difficult to search.</p><h3>Logging Too Little</h3><p>Critical investigations become impossible.</p><h3>Missing User Information</h3><p>An action without a user has little value.</p><h3>Ignoring Failed Attempts</h3><p>Unauthorized access attempts often matter as much as successful ones. </p><h2>Real-World Example</h2><p>Imagine a banking application.</p><p>A customer transfers money.</p><p>The audit log records:</p><ul><li><p>User identity</p></li><li><p>Source account</p></li><li><p>Destination account</p></li><li><p>Amount</p></li><li><p>Timestamp</p></li><li><p>IP address</p></li><li><p>Approval status</p></li><li><p>Correlation ID</p></li></ul><p>Months later an investigation occurs.</p><p>The organization can reconstruct the entire transaction history within seconds. </p><h2>How Audit Logging Supports Earlier Topics</h2><p>This article connects naturally with several recent posts.</p><p>Zero Trust verifies identities.</p><p>Authorization controls permissions.</p><p>Data Protection secures information.</p><p>Audit Logging records every important action.</p><p>Distributed Tracing follows requests across services.</p><p>Together these practices create applications that are secure, observable, and accountable. </p><h2>Best Practices Checklist</h2><p>When designing an audit logging system:</p><ul><li><p>Audit important business events rather than every operation.</p></li><li><p>Capture user identity and request context.</p></li><li><p>Record previous and new values where appropriate.</p></li><li><p>Keep audit logs separate from application logs.</p></li><li><p>Protect audit records from unauthorized modification.</p></li><li><p>Encrypt sensitive audit data.</p></li><li><p>Use asynchronous processing for high-volume systems.</p></li><li><p>Define retention policies that satisfy compliance requirements.</p></li><li><p>Regularly review audit logs for suspicious activity.</p></li></ul><p>Following these practices helps build audit systems that remain valuable as applications grow. </p><h2>Closing Thoughts</h2><p>Building secure applications involves much more than preventing unauthorized access.</p><p>Organizations also need visibility into what happens after users sign in.</p><p>Audit logging provides that visibility by recording important events in a consistent, reliable, and searchable way.</p><p>ASP.NET Core offers all the building blocks needed to create robust audit logging systems, especially when combined with Entity Framework Core, authentication, authorization, and distributed tracing.</p><p>As your applications grow in complexity, audit logging becomes one of the most valuable tools for troubleshooting, security investigations, and regulatory compliance.</p><p>Knowing what happened yesterday often determines how quickly you can solve tomorrow&#8217;s problem. </p><h2>Join The Community</h2><p>Enjoyed this article? <a href="https://www.asptoday.com/">Subscribe to </a><strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core architecture, security, and enterprise development guides. Join the Substack Chat to discuss modern ASP.NET Core development with developers building secure, scalable applications. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Advanced Authorization in ASP.NET Core: Policy-Based and Attribute-Based Access Control]]></title><description><![CDATA[Go beyond simple roles with policy-based and attribute-based authorization in ASP.NET Core. Learn how to build secure, flexible access control for modern applications. #ASPToday #aspnetcore #authorization #dotnet #security]]></description><link>https://www.asptoday.com/p/advanced-authorization-in-aspnet</link><guid isPermaLink="false">https://www.asptoday.com/p/advanced-authorization-in-aspnet</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 30 Jun 2026 15:02:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4Cr2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Knowing who a user is is only half of the security story. Modern applications must also decide what each user is allowed to do. As systems grow in size and complexity, simple role-based authorization often becomes difficult to maintain. <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> provides powerful authorization features that allow developers to build flexible, secure access control using policies, custom requirements, and authorization attributes. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4Cr2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4Cr2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!4Cr2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!4Cr2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!4Cr2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4Cr2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2553333,&quot;alt&quot;:&quot;Theme park guests use different wristbands to access rides and VIP areas. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/203939378?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Theme park guests use different wristbands to access rides and VIP areas. " title="Theme park guests use different wristbands to access rides and VIP areas. " srcset="https://substackcdn.com/image/fetch/$s_!4Cr2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!4Cr2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!4Cr2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!4Cr2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe92f7896-514e-4ae8-a31a-d3ce67da551f_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, you&#8217;ll learn how advanced authorization works, when to use policy-based and attribute-based approaches, and how to build scalable authorization rules for real-world applications. </p><h2>Authentication vs Authorization</h2><p>These two concepts are closely related but solve different problems.</p><p>Authentication answers:</p><blockquote><p>Who are you?</p></blockquote><p>Authorization answers:</p><blockquote><p>What are you allowed to do?</p></blockquote><p>Imagine entering an airport.</p><p>Showing your passport proves your identity.</p><p>That is authentication.</p><p>Your boarding pass determines which gate you may enter and whether you can access the airline lounge.</p><p>That is authorization.</p><p>ASP.NET Core separates these responsibilities, making applications easier to secure and maintain.</p><h2>Why Authorization Becomes Difficult</h2><p>Small applications often begin with simple roles.</p><p>Examples include:</p><ul><li><p>Administrator</p></li><li><p>Manager</p></li><li><p>Employee</p></li><li><p>Customer</p></li></ul><p>Initially this works well.</p><p>As applications grow, new questions appear.</p><p>Can managers approve expenses over $10,000?</p><p>Can support engineers edit customer information but not delete it?</p><p>Can finance users export reports only during business hours?</p><p>Can users manage only projects belonging to their own department?</p><p>Roles alone cannot answer these questions cleanly.</p><h2>Understanding Role-Based Authorization</h2><p>ASP.NET Core supports role-based authorization out of the box.</p><p>Example:</p><pre><code><code>[Authorize(Roles = "Administrator")]
public IActionResult DeleteUser()
{
    return Ok();
}</code></code></pre><p>Only users belonging to the Administrator role can access this action.</p><p>Simple.</p><p>Easy to understand.</p><p>Perfect for straightforward scenarios.</p><p>However, real businesses usually require more flexibility.</p><h2>The Limitations of Roles</h2><p>Imagine an organization with:</p><ul><li><p>Sales Manager </p></li><li><p>Regional Sales Manager </p></li><li><p>Global Sales Manager </p></li><li><p>Finance Manager </p></li><li><p>HR Manager</p></li><li><p>Support Manager </p></li><li><p>Operations Manager<br></p></li></ul><p>Soon the number of roles begins to grow rapidly.</p><p>Eventually you encounter:</p><ul><li><p>Duplicate permissions </p></li><li><p>Conflicting responsibilities </p></li><li><p>Difficult maintenance</p></li><li><p>Authorization bugs <br></p></li></ul><p>This is often called <strong>role explosion</strong>.</p><p>Policies solve this problem.</p><h2>What Is Policy-Based Authorization?</h2><p>A policy defines rules instead of roles.</p><p>Instead of asking:</p><p>&#8220;Is this user an Administrator?&#8221;</p><p>You ask:</p><p>&#8220;Does this user satisfy these requirements?&#8221;</p><p>A policy can evaluate:</p><ul><li><p>Claims </p></li><li><p>Roles </p></li><li><p>Permissions </p></li><li><p>Custom business rules </p></li><li><p>Time of day </p></li><li><p>Department </p></li><li><p>Resource ownership<br></p></li></ul><p>Policies provide much greater flexibility.</p><h2>Creating a Policy</h2><p>Example:</p><pre><code><code>builder.Services.AddAuthorization(options =&gt;
{
    options.AddPolicy(
        "CanApproveOrders",
        policy =&gt;
        {
            policy.RequireClaim(
                "permission",
                "orders.approve");
        });
});</code></code></pre><p>The policy now checks whether the user has the required permission claim.</p><h2>Applying a Policy</h2><pre><code><code>[Authorize(Policy = "CanApproveOrders")]
public IActionResult Approve()
{
    return Ok();
}</code></code></pre><p>Notice how the controller contains no business logic.</p><p>The authorization system handles everything.</p><h2>Understanding Claims</h2><p>Claims describe information about a user.</p><p>Examples include:</p><ul><li><p>Name </p></li><li><p>Department </p></li><li><p>Country </p></li><li><p>Permission </p></li><li><p>Subscription Level </p></li><li><p>Employee Number <br></p></li></ul><p>Unlike roles, claims provide detailed information that policies can evaluate.</p><h2>Claims-Based Authorization</h2><p>Suppose users receive:</p><pre><code><code>Department = Finance

Permission = ApproveInvoices</code></code></pre><p>Your policy can require either or both.</p><p>This creates much more granular authorization.</p><h2>Combining Multiple Requirements</h2><p>Policies may require multiple conditions.</p><p>Example:</p><pre><code><code>options.AddPolicy(
    "SeniorFinance",
    policy =&gt;
    {
        policy.RequireRole("Manager");

        policy.RequireClaim(
            "Department",
            "Finance");
    });</code></code></pre><p>Only finance managers satisfy the policy.</p><h2>Custom Authorization Requirements</h2><p>Sometimes business rules become very specific.</p><p>Example:</p><p>A project manager may edit only projects belonging to their own region.</p><p>This requires custom authorization.</p><p>Create a requirement:</p><pre><code><code>public class RegionRequirement
    : IAuthorizationRequirement
{
}</code></code></pre><p>Then implement an authorization handler.</p><p>The handler contains your business logic.</p><p>This keeps controllers clean and reusable.</p><h2>Resource-Based Authorization</h2><p>Sometimes authorization depends on the resource itself.</p><p>Example:</p><p>A customer may edit only their own profile.</p><p>Not someone else&#8217;s.</p><p>Instead of checking only user claims, ASP.NET Core evaluates:</p><ul><li><p>User </p></li><li><p>Resource </p></li><li><p>Policy <br></p></li></ul><p>Together.</p><p>This is called resource-based authorization.</p><h2>Attribute-Based Access Control (ABAC)</h2><p>Attribute-Based Access Control goes even further.</p><p>Instead of checking only roles or permissions, it evaluates attributes.</p><p>Examples include:</p><p>User attributes:</p><ul><li><p>Department </p></li><li><p>Clearance level </p></li><li><p>Country<br></p></li></ul><p>Resource attributes:</p><ul><li><p>Owner </p></li><li><p>Classification </p></li><li><p>Status<br></p></li></ul><p>Environment attributes:</p><ul><li><p>Time </p></li><li><p>Device </p></li><li><p>Network location<br></p></li></ul><p>Actions:</p><ul><li><p>Read </p></li><li><p>Update </p></li><li><p>Delete<br></p></li></ul><p>All these factors influence authorization decisions.</p><h2>Example Scenario</h2><p>Suppose an employee wants to download payroll information.</p><p>Rules:</p><ul><li><p>Must belong to HR </p></li><li><p>Must use a company device </p></li><li><p>Must be inside the corporate network </p></li><li><p>Must access during working hours<br></p></li></ul><p>A single role cannot express this.</p><p>Attribute-based authorization can.</p><h2>Authorization Handlers</h2><p>Authorization handlers evaluate custom requirements.</p><p>Example:</p><pre><code><code>public class MinimumAgeHandler
    : AuthorizationHandler&lt;
        MinimumAgeRequirement&gt;
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        // Validation logic

        return Task.CompletedTask;
    }
}</code></code></pre><p>Handlers keep authorization logic centralized.</p><h2>Dynamic Policies</h2><p>Large enterprise systems often load permissions from databases.</p><p>Instead of hardcoding every policy, applications generate them dynamically.</p><p>Benefits include:</p><ul><li><p>Easier administration </p></li><li><p>Less code duplication </p></li><li><p>Business-managed permissions<br></p></li></ul><p>This approach is common in SaaS platforms.</p><h2>Authorization in Minimal APIs</h2><p>Policies also work with Minimal APIs.</p><p>Example:</p><pre><code><code>app.MapGet("/reports", () =&gt;
{
    return Results.Ok();
})
.RequireAuthorization("CanViewReports");</code></code></pre><p>Security remains consistent across application styles.</p><h2>Authorization and Microservices</h2><p>In a microservices architecture, authorization becomes distributed.</p><p>Each service should verify:</p><ul><li><p>Identity</p></li><li><p>Permissions </p></li><li><p>Claims<br></p></li></ul><p>Never assume another service has already performed sufficient authorization.</p><p>This aligns with the Zero Trust principles discussed in our previous article.</p><h2>Combining Authentication and Authorization</h2><p>Authentication produces identity.</p><p>Authorization evaluates identity.</p><p>Neither replaces the other.</p><p>Together they provide layered security.</p><h2>Common Authorization Mistakes</h2><p>Avoid:</p><ul><li><p>Hardcoding permissions </p></li><li><p>Mixing authorization into controllers </p></li><li><p>Relying only on roles </p></li><li><p>Trusting client-side validation </p></li><li><p>Forgetting API authorization <br></p></li></ul><p>Centralized authorization policies improve consistency.</p><h2>Testing Authorization</h2><p>Authorization should be tested just like business logic.</p><p>Verify:</p><ul><li><p>Authorized users succeed. </p></li><li><p>Unauthorized users receive <strong>403 Forbidden</strong>. </p></li><li><p>Anonymous users receive <strong>401 Unauthorized</strong> when appropriate.<br></p></li></ul><p>Automated tests help prevent accidental permission changes.</p><h2>Real-World Example</h2><p>Imagine a project management platform.</p><p>Users include:</p><ul><li><p>Developers </p></li><li><p>Team Leads </p></li><li><p>Project Managers </p></li><li><p>Clients<br></p></li></ul><p>Developers may edit only assigned tasks.</p><p>Project Managers approve budgets.</p><p>Clients view only their own projects.</p><p>Finance exports reports.</p><p>Instead of creating dozens of overlapping roles, policies evaluate permissions, ownership, and department.</p><p>The authorization model remains clean even as the application grows.</p><h2>Authorization and Compliance</h2><p>Strong authorization also supports compliance requirements.</p><p>Frameworks such as:</p><ul><li><p>GDPR </p></li><li><p>HIPAA </p></li><li><p>SOC 2 </p></li><li><p>PCI DSS<br></p></li></ul><p>Require organizations to restrict access to sensitive information.</p><p>Policy-based authorization helps enforce these restrictions consistently.</p><h2>How This Fits Your ASP.NET Core Journey</h2><p>Over the past several articles we&#8217;ve explored:</p><ul><li><p>Zero Trust Architecture </p></li><li><p>Data Protection </p></li><li><p>Encryption </p></li><li><p>Key Management </p></li><li><p>Secure Microservices<br></p></li></ul><p>Advanced authorization builds directly on those concepts.</p><p>Authentication proves identity.</p><p>Encryption protects data.</p><p>Authorization determines who can use that data.</p><p>Together they form the foundation of secure ASP.NET Core applications.</p><h2>Closing Thoughts</h2><p>Authorization is much more than checking whether a user is an administrator.</p><p>Modern applications require flexible, maintainable access control that adapts to changing business requirements.</p><p>ASP.NET Core&#8217;s policy-based and attribute-based authorization features provide a scalable way to secure APIs, web applications, and distributed systems without filling controllers with complex permission logic.</p><p>As your applications grow, investing in a strong authorization strategy pays dividends in security, maintainability, and compliance.</p><h2>Join The Community</h2><p>Enjoyed this article? <a href="https://www.asptoday.com/">Subscribe to </a><strong><a href="https://www.asptoday.com/">ASP Today</a></strong> for practical ASP.NET Core architecture, security, and cloud-native development guides. Join the Substack Chat to discuss modern ASP.NET Core development with developers from around the world. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Data Protection in ASP.NET Core: Encryption, Key Management, and Compliance]]></title><description><![CDATA[Learn how to secure sensitive data with encryption, key management, ASP.NET Core Data Protection, and compliance best practices. #ASPToday #aspnetcore #cybersecurity #dataprotection #dotnet]]></description><link>https://www.asptoday.com/p/data-protection-in-aspnet-core-encryption</link><guid isPermaLink="false">https://www.asptoday.com/p/data-protection-in-aspnet-core-encryption</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 23 Jun 2026 15:02:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!JSda!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Data is one of the most valuable assets any application manages. Whether you&#8217;re storing customer information, authentication tokens, financial records, or business data, protecting that information is essential. Modern <a href="https://www.asptoday.com/">ASP.NET Core</a> applications must not only secure data against attackers, but also satisfy regulatory and compliance requirements.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JSda!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JSda!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!JSda!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!JSda!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!JSda!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JSda!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2255240,&quot;alt&quot;:&quot;Child securing a piggy bank with locks, keys, and shields symbolizing data protection. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/202950612?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Child securing a piggy bank with locks, keys, and shields symbolizing data protection. " title="Child securing a piggy bank with locks, keys, and shields symbolizing data protection. " srcset="https://substackcdn.com/image/fetch/$s_!JSda!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!JSda!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!JSda!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!JSda!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F939cf221-63d8-4209-8af8-578af69036d2_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we&#8217;ll explore how ASP.NET Core supports data protection through encryption, secure key management, and practical compliance strategies that help keep sensitive information safe.</p><h2>Why Data Protection Matters</h2><p>Every application stores data.</p><p>Examples include:</p><ul><li><p>Customer profiles</p></li><li><p>Login credentials</p></li><li><p>Payment information</p></li><li><p>Business records</p></li><li><p>Application secrets</p></li><li><p>Session tokens</p></li></ul><p>If this information becomes exposed, organizations can face:</p><ul><li><p>Financial losses</p></li><li><p>Reputational damage</p></li><li><p>Regulatory penalties</p></li><li><p>Customer distrust</p></li></ul><p>Data protection is no longer optional.</p><p>It is a fundamental requirement of modern software development.</p><h2>The Expanding Risk Landscape</h2><p>Modern applications rarely exist on a single server.</p><p>Today&#8217;s ASP.NET Core applications often interact with:</p><ul><li><p><a href="https://www.asptoday.com/p/the-role-of-aspnet-core-in-cloud">Cloud databases</a></p></li><li><p><a href="https://www.asptoday.com/p/microservices-architecture-with-aspnet">Microservices</a></p></li><li><p>Message queues</p></li><li><p>Third-party APIs</p></li><li><p>Mobile applications</p></li><li><p>Browser clients</p></li></ul><p>Each connection introduces new opportunities for data exposure.</p><p>As systems become more distributed, protecting data becomes more challenging.</p><p>This builds naturally on concepts we explored in:</p><ul><li><p><a href="https://www.asptoday.com/p/securing-microservices-in-aspnet">Securing Microservices in ASP.NET Core</a></p></li><li><p><a href="https://www.asptoday.com/p/deep-dive-into-distributed-tracing">Distributed Tracing with OpenTelemetry</a></p></li><li><p><a href="https://www.asptoday.com/p/designing-fault-tolerant-systems">Fault-Tolerant Systems in ASP.NET Core</a></p></li></ul><p>Security must exist throughout the entire application lifecycle.</p><h2>Understanding Data Protection</h2><p>Data protection focuses on ensuring information remains:</p><ul><li><p>Confidential</p></li><li><p>Accurate</p></li><li><p>Available</p></li></ul><p>These goals align closely with the CIA Triad:</p><h3>Confidentiality</h3><p>Only authorized users can access information.</p><h3>Integrity</h3><p>Data cannot be altered without authorization.</p><h3>Availability</h3><p>Authorized users can access data when needed.</p><p>Strong protection strategies balance all three requirements.</p><h2>Encryption: The Foundation of Data Protection</h2><p>Encryption converts readable information into an unreadable format.</p><p>Without the correct key, encrypted data becomes useless to attackers.</p><p>For example:</p><pre><code><code>Original:
CustomerPassword123

Encrypted:
Qm1kT3dQVnNQaW5GcXhaZw==</code></code></pre><p>While this example is simplified, the principle remains the same.</p><p>Encryption protects sensitive information even if storage systems become compromised.</p><h2>Data at Rest vs Data in Transit</h2><p>When discussing encryption, two scenarios matter most.</p><h3>Data at Rest</h3><p>Data stored in:</p><ul><li><p>Databases </p></li><li><p>Files </p></li><li><p>Backups </p></li><li><p>Cloud storage<br></p></li></ul><p>Examples:</p><ul><li><p>SQL Server records </p></li><li><p>Azure Blob Storage </p></li><li><p>Backup archives<br></p></li></ul><h3>Data in Transit</h3><p>Data moving between systems.</p><p>Examples:</p><ul><li><p>Browser to API </p></li><li><p>API to database </p></li><li><p>Service to service communication </p></li><li><p>Message queue traffic<br></p></li></ul><p>Both require protection.</p><p>Securing one without the other leaves gaps.</p><h2>Encrypting Data in Transit</h2><p>HTTPS should be mandatory for every ASP.NET Core application.</p><p>HTTPS uses TLS (Transport Layer Security) to encrypt communication.</p><p>Benefits include:</p><ul><li><p>Preventing eavesdropping </p></li><li><p>Preventing data tampering </p></li><li><p>Verifying server identity<br></p></li></ul><p>ASP.NET Core makes HTTPS straightforward.</p><pre><code><code>app.UseHttpsRedirection();</code></code></pre><p>Microsoft documentation:</p><p>https://learn.microsoft.com/aspnet/core/security/enforcing-ssl</p><h2>HTTP Strict Transport Security (HSTS)</h2><p>HSTS tells browsers to always use HTTPS.</p><pre><code><code>if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}</code></code></pre><p>This prevents downgrade attacks where attackers attempt to force insecure connections.</p><h2>Encrypting Data at Rest</h2><p>Data at rest remains vulnerable if storage systems are compromised.</p><p>Common approaches include:</p><ul><li><p>Database encryption </p></li><li><p>Disk encryption </p></li><li><p>File encryption </p></li><li><p>Application-level encryption<br></p></li></ul><p>Each protects different layers of the system.</p><h2>SQL Server Encryption</h2><p>SQL Server supports:</p><ul><li><p>Transparent Data Encryption (TDE) </p></li><li><p>Column encryption </p></li><li><p>Always Encrypted<br></p></li></ul><p><a href="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/sql-server-encryption?view=sql-server-ver17">Official documentation</a>:</p><p>TDE encrypts database files automatically.</p><p>Applications require minimal changes.</p><h2>When Application-Level Encryption Is Necessary</h2><p>Sometimes specific fields require stronger protection.</p><p>Examples include:</p><ul><li><p>National identification numbers </p></li><li><p>Payment data </p></li><li><p>Medical information </p></li><li><p>API credentials<br></p></li></ul><p>In these cases, encrypting sensitive values before storage provides additional security.</p><h4>Example: AES Encryption</h4><pre><code><code>using System.Security.Cryptography;
using System.Text;

public static string Encrypt(
    string plaintext,
    byte[] key,
    byte[] iv)
{
    using var aes = Aes.Create();

    aes.Key = key;
    aes.IV = iv;

    using var encryptor =
        aes.CreateEncryptor();

    using var ms = new MemoryStream();

    using var cs = new CryptoStream(
        ms,
        encryptor,
        CryptoStreamMode.Write);

    using var sw = new StreamWriter(cs);

    sw.Write(plaintext);

    return Convert.ToBase64String(
        ms.ToArray());
}</code></code></pre><p>This demonstrates the concept of application-level encryption.</p><p>Production implementations require additional safeguards and validation.</p><h2>Introducing ASP.NET Core Data Protection</h2><p>ASP.NET Core includes a built-in Data Protection system.</p><p>The framework uses it internally for:</p><ul><li><p>Authentication cookies </p></li><li><p>Session data </p></li><li><p>CSRF protection </p></li><li><p>Temporary tokens<br></p></li></ul><p><a href="https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-10.0">Official documentation</a>:</p><p>This system simplifies secure encryption and key management.</p><h2>Configuring Data Protection</h2><p>Basic setup:</p><pre><code><code>builder.Services
    .AddDataProtection();</code></code></pre><p>The framework automatically generates and manages encryption keys.</p><h2>Protecting Sensitive Values</h2><p>Example:</p><pre><code><code>public class SecretService
{
    private readonly IDataProtector _protector;

    public SecretService(
        IDataProtectionProvider provider)
    {
        _protector =
            provider.CreateProtector(
                "CustomerData");
    }

    public string Protect(string value)
    {
        return _protector.Protect(value);
    }

    public string Unprotect(string value)
    {
        return _protector.Unprotect(value);
    }
}</code></code></pre><p>This approach avoids many common encryption implementation mistakes.</p><h2>Why Key Management Matters</h2><p>Encryption is only as strong as its keys.</p><p>Many breaches occur because keys are poorly managed.</p><p>Common mistakes include:</p><ul><li><p>Storing keys in source code</p></li><li><p>Sharing keys between environments </p></li><li><p>Never rotating keys </p></li><li><p>Exposing keys in configuration files<br></p></li></ul><p>Protecting encryption keys is often more important than protecting the encrypted data itself.</p><h2>Understanding Key Rotation</h2><p>Keys should not remain active forever.</p><p>Key rotation involves:</p><ul><li><p>Creating new keys </p></li><li><p>Retiring old keys </p></li><li><p>Re-encrypting data when appropriate<br></p></li></ul><p>Benefits include:</p><ul><li><p>Reduced exposure </p></li><li><p>Limited attack windows </p></li><li><p>Improved compliance <br></p></li></ul><p>ASP.NET Core Data Protection supports automatic key rotation.</p><h2>Storing Keys Securely</h2><p>Development environments often store keys locally.</p><p>Production environments require stronger solutions.</p><p>Options include:</p><ul><li><p>Azure Key Vault </p></li><li><p>AWS KMS </p></li><li><p>Hardware Security Modules (HSMs) </p></li><li><p>Dedicated secret stores<br></p></li></ul><h2>Using Azure Key Vault</h2><p>Azure Key Vault provides:</p><ul><li><p>Secure secret storage </p></li><li><p>Key management </p></li><li><p>Certificate management </p></li><li><p>Access control<br></p></li></ul><p><a href="https://learn.microsoft.com/en-us/azure/key-vault/">Official documentation</a>:</p><p>Key Vault integrates well with ASP.NET Core applications.</p><h2>Configuring Azure Key Vault</h2><p>Example:</p><pre><code><code>builder.Configuration
    .AddAzureKeyVault(
        new Uri(vaultUrl),
        new DefaultAzureCredential());</code></code></pre><p>This removes sensitive secrets from application configuration files.</p><h2>Protecting Configuration Data</h2><p>Configuration often contains:</p><ul><li><p>API keys </p></li><li><p>Database connection strings </p></li><li><p>Service credentials<br></p></li></ul><p>Never commit these values to source control.</p><p>Instead use:</p><ul><li><p>Secret managers </p></li><li><p>Environment variables </p></li><li><p>Key Vault solutions<br></p></li></ul><p><a href="https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-10.0&amp;tabs=windows%2Cpowershell">ASP.NET Core Secret Manager</a></p><h2>Data Protection in Distributed Systems</h2><p>Modern applications frequently operate across multiple services.</p><p>Examples include:</p><ul><li><p>Microservices </p></li><li><p>Background workers </p></li><li><p>Message processors<br></p></li></ul><p>These systems often need access to shared encryption keys.</p><p>Careful planning becomes essential.</p><p>Improper key distribution can create significant security risks.</p><h2>Compliance Requirements</h2><p>Many organizations must satisfy regulatory requirements.</p><p>Common examples include:</p><h3>GDPR</h3><p><a href="https://gdpr.eu/">Protects personal data for EU residents</a>.</p><h3>HIPAA</h3><p><a href="https://www.hhs.gov/hipaa/index.html">Protects healthcare information in the United States</a>.</p><h3>PCI DSS</h3><p><a href="https://www.pcisecuritystandards.org/">Protects payment card information</a>.</p><h3>SOC 2</h3><p><a href="https://www.aicpa-cima.com/">Focuses on security and operational controls</a>.</p><p>Compliance requirements often influence data protection strategies.</p><h2>Encryption Alone Does Not Guarantee Compliance</h2><p>A common misconception is:</p><blockquote><p>&#8220;We encrypted the data, so we&#8217;re compliant.&#8221;</p></blockquote><p>Compliance involves much more.</p><p>Organizations must also consider:</p><ul><li><p>Access controls </p></li><li><p>Audit logging </p></li><li><p>Data retention </p></li><li><p>Incident response </p></li><li><p>Monitoring<br></p></li></ul><p>Encryption is only one piece of the puzzle.</p><h2>Auditing and Monitoring</h2><p>Data access should be traceable.</p><p>Important events include:</p><ul><li><p>Login attempts </p></li><li><p>Data modifications </p></li><li><p>Key access </p></li><li><p>Permission changes<br></p></li></ul><p>Audit trails support:</p><ul><li><p>Security investigations </p></li><li><p>Compliance reporting </p></li><li><p>Incident response<br></p></li></ul><h2>Least Privilege Access</h2><p>Not every user should access every piece of data.</p><p>Apply least privilege principles:</p><ul><li><p>Grant only required permissions </p></li><li><p>Remove unnecessary access </p></li><li><p>Review permissions regularly<br></p></li></ul><p>This aligns closely with the Zero Trust concepts discussed in our previous article.</p><h2>Protecting Backups</h2><p>Backups often contain the same sensitive data as production systems.</p><p>Organizations sometimes secure production databases while neglecting backups.</p><p>Backups should be:</p><ul><li><p>Encrypted </p></li><li><p>Access controlled </p></li><li><p>Monitored </p></li><li><p>Retained appropriately<br></p></li></ul><p>A compromised backup can be just as damaging as a compromised database.</p><h2>Data Retention and Deletion</h2><p>Keeping data forever creates unnecessary risk.</p><p>Questions to consider:</p><ul><li><p>How long should data be retained? </p></li><li><p>When should records be deleted? </p></li><li><p>Are retention requirements documented?<br></p></li></ul><p>Compliance frameworks frequently require documented retention policies.</p><h2>Real-World Example: Customer Management Platform</h2><p>Imagine a SaaS application storing:</p><ul><li><p>Customer profiles </p></li><li><p>Billing information </p></li><li><p>Authentication data<br></p></li></ul><p>A strong protection strategy might include:</p><ul><li><p>HTTPS everywhere </p></li><li><p>Database encryption </p></li><li><p>Azure Key Vault </p></li><li><p>ASP.NET Core Data Protection </p></li><li><p>Audit logging </p></li><li><p>Automated key rotation<br></p></li></ul><p>If a database backup were stolen, encryption would help prevent attackers from accessing sensitive information.</p><h2>Common Data Protection Mistakes</h2><p>Several mistakes appear repeatedly.</p><h3>Hardcoded Secrets</h3><p>Secrets should never exist in source code.</p><h3>Weak Key Management</h3><p>Protecting data while exposing keys defeats the purpose.</p><h3>Lack of Rotation</h3><p>Long-lived keys increase risk.</p><h3>Unencrypted Backups</h3><p>Backup systems require the same protections as production.</p><h3>Excessive Access</h3><p>Too many users often have unnecessary permissions.</p><p>Avoiding these mistakes significantly improves security.</p><h2>How This Fits Your ASP.NET Core Journey</h2><p>So far, we&#8217;ve explored:</p><ul><li><p>Microservice security </p></li><li><p>Zero Trust Architecture </p></li><li><p>Distributed tracing </p></li><li><p>Chaos engineering </p></li><li><p>Fault tolerance<br></p></li></ul><p>Data protection builds on all of these topics.</p><p>Security focuses on who can access systems.</p><p>Data protection focuses on safeguarding the information those systems manage.</p><p>Together they form a critical foundation for modern cloud-native applications.</p><h2>Closing Thoughts</h2><p>Protecting data is one of the most important responsibilities developers have.</p><p>ASP.NET Core provides powerful tools for:</p><ul><li><p>Encryption </p></li><li><p>Key management </p></li><li><p>Secret storage </p></li><li><p>Data protection<br></p></li></ul><p>Combined with proper compliance practices, monitoring, and access controls, these capabilities help organizations secure sensitive information and reduce risk.</p><p>Security threats will continue to evolve.</p><p>Strong data protection practices ensure your applications remain prepared.</p><h2>Join The Community</h2><p>Enjoyed this article? Subscribe to <a href="https://www.asptoday.com/">ASP Today</a> for practical ASP.NET Core architecture guides, security best practices, and real-world development strategies. Join the Substack Chat and connect with developers building secure and reliable modern applications. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Securing Microservices in ASP.NET Core: Zero Trust Architecture and Best Practices]]></title><description><![CDATA[Discover Zero Trust Architecture, service-to-service authentication, JWT security, API protection, and best practices for securing ASP.NET Core microservices. #ASPToday #aspnetcore #microservices #zerotrust #cybersecurity]]></description><link>https://www.asptoday.com/p/securing-microservices-in-aspnet</link><guid isPermaLink="false">https://www.asptoday.com/p/securing-microservices-in-aspnet</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 16 Jun 2026 15:02:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!gch0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As applications evolve into distributed microservices architectures, security becomes significantly more complex. Instead of protecting a single application, developers must secure dozens of APIs, services, databases, message queues, and communication channels. Traditional security models often assume that systems inside a network can be trusted. Zero Trust Architecture challenges this assumption by requiring every request, user, service, and device to continuously prove its identity. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gch0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gch0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!gch0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!gch0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!gch0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gch0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2460739,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/201933000?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!gch0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!gch0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!gch0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!gch0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96157193-27da-46d9-878d-86d9eff9de1a_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we&#8217;ll explore how to secure <a href="https://www.asptoday.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> microservices using Zero Trust principles and practical security best practices. </p><h2>Why Microservices Change the Security Landscape </h2><p>In a traditional monolithic application, security boundaries are relatively simple.</p><p>Users interact with a single application. That application communicates with a single database. Most communication happens internally.</p><p>Microservices introduce a very different environment.</p><p>A single user request may pass through:</p><ul><li><p>API Gateway</p></li><li><p>Authentication Service</p></li><li><p>Product Service</p></li><li><p>Inventory Service</p></li><li><p>Payment Service</p></li><li><p>Notification Service</p></li><li><p>Message Queue</p></li></ul><p>Each service creates a potential attack surface. Every API endpoint becomes a possible entry point. Every service-to-service call becomes a security concern.</p><p>This is why securing microservices requires a different mindset. </p><h2>The Problem with Traditional Network Trust</h2><p>Historically, many organizations relied on perimeter-based security.</p><p>The model was simple:</p><ul><li><p>Protect the network boundary</p></li><li><p>Trust everything inside</p></li></ul><p>This approach worked reasonably well when systems lived inside a single corporate network.</p><p>Modern cloud-native systems rarely operate this way.</p><p>Today we have:</p><ul><li><p>Cloud infrastructure</p></li><li><p>Hybrid environments</p></li><li><p>Remote workforces</p></li><li><p>Third-party integrations</p></li><li><p>Distributed services</p></li></ul><p>The network perimeter has effectively disappeared. </p><p>Trusting everything inside the network is no longer sufficient. </p><h2>What Is Zero Trust Architecture?</h2><p>Zero Trust follows a simple principle:</p><blockquote><p>Never trust. Always verify.</p></blockquote><p>Every request must be validated regardless of where it originates.</p><p>This applies to:</p><ul><li><p>Users</p></li><li><p>Services</p></li><li><p>Devices</p></li><li><p>Applications</p></li><li><p>APIs</p></li></ul><p>Being inside the network does not automatically grant trust.</p><p>Every interaction must continuously prove its legitimacy.</p><p><a href="https://learn.microsoft.com/security/zero-trust/">Microsoft&#8217;s Zero Trust guidance</a></p><h2>Core Principles of Zero Trust</h2><p>Zero Trust is built around three core ideas.</p><h3>Verify Explicitly</h3><p>Always authenticate and authorize every request.</p><p>Do not rely on network location.</p><p>Do not assume trust. </p><h3>Use Least Privilege Access</h3><p>Grant only the permissions required.</p><p>Nothing more.</p><p>This limits the damage caused by compromised accounts or services. </p><h3>Assume Breach</h3><p>Design systems as though attackers are already inside.</p><p>This encourages stronger monitoring, segmentation, and verification. </p><h2>Why ASP.NET Core Fits Zero Trust Well</h2><p>ASP.NET Core provides several built-in security capabilities:</p><ul><li><p>Authentication middleware</p></li><li><p>Authorization policies</p></li><li><p>Identity integration</p></li><li><p>JWT support</p></li><li><p>OAuth integration</p></li><li><p>OpenID Connect support</p></li></ul><p>These features make ASP.NET Core an excellent platform for implementing Zero Trust principles.</p><p><a href="https://learn.microsoft.com/aspnet/core/security/">Official documentation</a></p><h2>Authentication vs Authorization</h2><p>These concepts are often confused.</p><p>Authentication answers:</p><blockquote><p>Who are you?</p></blockquote><p>Authorization answers:</p><blockquote><p>What are you allowed to do?</p></blockquote><p>Both are essential.</p><p>A user may be authenticated successfully but still lack permission to perform specific actions. </p><h2>JWT Authentication for Microservices</h2><p>JSON Web Tokens (JWTs) are commonly used in microservices environments.</p><p>A user authenticates once.</p><p>The identity provider issues a token. The token accompanies subsequent requests.</p><p>Example:</p><pre><code><code>Authorization: Bearer eyJhbGciOi...</code></code></pre><p>Services validate the token before processing requests. This eliminates the need for centralized session storage. </p><h2>Configuring JWT Authentication</h2><p>ASP.NET Core provides built-in support.</p><pre><code><code>builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer(options =&gt;
    {
        options.Authority =
            "https://identity.example.com";

        options.Audience = "orders-api";
    });</code></code></pre><p>This validates incoming access tokens automatically.</p><h2>Securing APIs with Authorization Policies</h2><p>Role-based authorization works well for many scenarios.</p><p>Example:</p><pre><code><code>[Authorize(Roles = "Administrator")]
public IActionResult DeleteOrder()
{
    return Ok();
}</code></code></pre><p>However, microservices often require more granular controls.</p><p>Policy-based authorization provides greater flexibility.</p><h2>Policy-Based Authorization</h2><p>Example:</p><pre><code><code>builder.Services.AddAuthorization(options =&gt;
{
    options.AddPolicy(
        "CanProcessPayments",
        policy =&gt;
        {
            policy.RequireClaim(
                "permission",
                "payments.process");
        });
});</code></code></pre><p>Controller:</p><pre><code><code>[Authorize(Policy = "CanProcessPayments")]
public IActionResult ProcessPayment()
{
    return Ok();
}</code></code></pre><p>This approach scales well across complex systems.</p><h2>Service-to-Service Authentication</h2><p>Zero Trust applies to services too.</p><p>One common mistake is assuming internal services can trust each other automatically.</p><p>Example:</p><pre><code><code>API Gateway
     &#8595;
Orders Service
     &#8595;
Inventory Service
     &#8595;
Payment Service</code></code></pre><p>Every service should verify the identity of the caller.</p><p>This prevents attackers from moving laterally through compromised systems.</p><h2>Machine-to-Machine Authentication</h2><p>Service communication often uses OAuth 2.0 client credentials flow.</p><p>Instead of users authenticating:</p><ul><li><p>Services authenticate themselves<br></p></li></ul><p>Example workflow:</p><pre><code><code>Inventory Service
     &#8595;
Identity Provider
     &#8595;
Access Token
     &#8595;
Payment Service</code></code></pre><p>This creates secure service-to-service communication.</p><p><a href="https://oauth.net/2/">OAuth documentation</a></p><h2>Mutual TLS (mTLS)</h2><p>Mutual TLS strengthens service authentication.</p><p>Normally:</p><ul><li><p>Client verifies server <br></p></li></ul><p>With mTLS:</p><ul><li><p>Client verifies server </p></li><li><p>Server verifies client </p></li></ul><p>Both parties prove their identities.</p><p>This significantly improves security for internal service communication.</p><h2>API Gateways and Security</h2><p>Most microservices architectures include an API gateway.</p><p>The gateway becomes a central security layer.</p><p>Responsibilities often include:</p><ul><li><p>Authentication </p></li><li><p>Authorization </p></li><li><p>Rate limiting </p></li><li><p>Request validation </p></li><li><p>Threat detection </p></li></ul><p>Popular gateways include:</p><ul><li><p>YARP </p></li><li><p>Kong </p></li><li><p>Azure API Management </p></li></ul><p><a href="https://microsoft.github.io/reverse-proxy">YARP documentation</a></p><h2>Rate Limiting for Protection</h2><p>Rate limiting protects APIs from abuse.</p><p>ASP.NET Core includes built-in rate limiting.</p><p>Example:</p><pre><code><code>builder.Services.AddRateLimiter(options =&gt;
{
    options.AddFixedWindowLimiter(
        "api",
        opt =&gt;
        {
            opt.PermitLimit = 100;
            opt.Window = TimeSpan.FromMinutes(1);
        });
});</code></code></pre><p>This reduces:</p><ul><li><p>Brute-force attacks </p></li><li><p>Abuse </p></li><li><p>Resource exhaustion<br></p></li></ul><h2>Protecting Secrets</h2><p>Secrets are often one of the weakest links.</p><p>Common mistakes include:</p><ul><li><p>Hardcoded passwords </p></li><li><p>Secrets in source control </p></li><li><p>Shared credentials </p></li></ul><p>Instead use:</p><ul><li><p>Azure Key Vault </p></li><li><p>AWS Secrets Manager </p></li><li><p>Environment variables<br></p></li></ul><p><a href="https://learn.microsoft.com/azure/key-vault/">Azure Key Vault</a></p><h2>Secret Rotation</h2><p>Secrets should not live forever.</p><p>Implement regular rotation policies for:</p><ul><li><p>API keys </p></li><li><p>Database credentials </p></li><li><p>Certificates </p></li><li><p>Service accounts<br></p></li></ul><p>Automated rotation reduces risk significantly.</p><h2>Securing Message-Based Systems</h2><p>Many ASP.NET Core applications use:</p><ul><li><p>Azure Service Bus </p></li><li><p>RabbitMQ </p></li><li><p>Kafka </p></li></ul><p>Security must extend to messaging systems.</p><p>Consider:</p><ul><li><p>Authentication </p></li><li><p>Authorization </p></li><li><p>Encryption </p></li><li><p>Auditing </p></li></ul><p>Message queues should never be treated as trusted by default.</p><p>This connects directly with our earlier article on Azure Service Bus.</p><h2>Encrypt Data in Transit</h2><p>All service communication should use TLS.</p><p>Without encryption:</p><ul><li><p>Credentials can be intercepted </p></li><li><p>Data can be modified </p></li><li><p>Sensitive information becomes exposed </p></li></ul><p>HTTPS should be mandatory for every service endpoint.</p><h2>Encrypt Sensitive Data at Rest</h2><p>Data stored in:</p><ul><li><p>Databases </p></li><li><p>Message queues </p></li><li><p>Object storage </p></li></ul><p>Should be encrypted.</p><p>This limits exposure if storage systems become compromised.</p><h2>Logging Security Events</h2><p>Security incidents rarely announce themselves.</p><p>Logs should capture:</p><ul><li><p>Authentication failures </p></li><li><p>Authorization failures </p></li><li><p>Suspicious activity </p></li><li><p>Policy violations<br></p></li></ul><p>These events become critical during investigations.</p><h2>Distributed Security Monitoring</h2><p>As discussed in our article on OpenTelemetry and distributed tracing:</p><p>https://www.asptoday.com/p/distributed-tracing-opentelemetry-aspnet-core</p><p>Observability plays a major role in security.</p><p>Tracing can reveal:</p><ul><li><p>Unexpected service calls </p></li><li><p>Unauthorized access attempts </p></li><li><p>Abnormal traffic patterns<br></p></li></ul><p>Security and observability increasingly overlap.</p><h2>The Principle of Least Privilege</h2><p>Least privilege remains one of the most effective security controls.</p><p>Avoid:</p><ul><li><p>Administrator everywhere </p></li><li><p>Shared service accounts </p></li><li><p>Broad permissions<br></p></li></ul><p>Instead grant only the permissions required for a specific task.</p><p>This limits the impact of compromised credentials.</p><h2>Network Segmentation</h2><p>Not every service should communicate with every other service.</p><p>Segmenting networks reduces attack paths.</p><p>Example:</p><pre><code><code>Public APIs
     &#8595;
Business Services
     &#8595;
Database Layer</code></code></pre><p>Restricted communication paths improve security.</p><h2>Common Security Mistakes</h2><p>Several mistakes appear repeatedly in microservices environments.</p><h3>Blind Trust Between Services</h3><p>Just because a request originates internally does not mean it is safe.</p><h3>Shared Credentials</h3><p>Shared service accounts make auditing and access control difficult.</p><h3>Excessive Permissions</h3><p>Services often receive more access than they need.</p><h3>Missing Input Validation</h3><p>Never trust incoming data.</p><p>Validate everything.</p><h3>Hardcoded Secrets</h3><p>Secrets should never be stored in source code.</p><h2>Real-World Example: E-Commerce Platform</h2><p>Imagine an online store.</p><p>Components include:</p><ul><li><p>API Gateway </p></li><li><p>Orders Service </p></li><li><p>Inventory Service </p></li><li><p>Payments Service </p></li><li><p>Notification Service </p></li></ul><p>Zero Trust implementation would require:</p><ul><li><p>JWT authentication </p></li><li><p>Service-to-service identity </p></li><li><p>Authorization policies </p></li><li><p>TLS encryption </p></li><li><p>Secret management </p></li><li><p>Distributed monitoring </p></li></ul><p>Each interaction must prove legitimacy.</p><p>No component receives automatic trust.</p><h2>Security and Resilience Work Together</h2><p>This article connects closely with several recent topics:</p><ul><li><p>Fault-Tolerant Systems </p></li><li><p>Advanced Retry Strategies </p></li><li><p>Chaos Engineering </p></li><li><p>Distributed Tracing </p></li></ul><p>A resilient system that is insecure remains vulnerable.</p><p>A secure system that cannot survive failures remains unreliable.</p><p>Modern architecture requires both.</p><h2>Preparing for Compliance Requirements</h2><p>Many organizations must satisfy regulations such as:</p><ul><li><p>GDPR </p></li><li><p>HIPAA </p></li><li><p>PCI DSS </p></li><li><p>SOC 2 </p></li></ul><p>Zero Trust principles help support compliance efforts by enforcing stronger access controls and auditability.</p><h2>How This Fits Your ASP.NET Core Journey</h2><p>So far, we&#8217;ve explored:</p><ul><li><p>Distributed messaging </p></li><li><p>Sagas </p></li><li><p>Retry strategies </p></li><li><p>Fault tolerance </p></li><li><p>Observability </p></li><li><p>Chaos engineering </p></li></ul><p>Security is the next critical layer.</p><p>Once systems become distributed, visibility alone is not enough.</p><p>Every service must verify identities, enforce permissions, and protect sensitive data continuously.</p><h2>Closing Thoughts</h2><p>Microservices create tremendous flexibility and scalability.</p><p>They also introduce significant security challenges.</p><p>Zero Trust Architecture provides a practical framework for addressing those challenges by eliminating assumptions of trust and requiring continuous verification.</p><p>By combining:</p><ul><li><p>Authentication </p></li><li><p>Authorization </p></li><li><p>Least privilege </p></li><li><p>Service identity </p></li><li><p>Encryption </p></li><li><p>Monitoring </p></li></ul><p>You can build ASP.NET Core microservices that remain secure even as systems grow increasingly distributed and complex.</p><p>In modern architecture, trust is not granted.</p><p>Trust is earned continuously.</p><h2>Join The Community</h2><p>Enjoyed this article? Subscribe to <a href="https://www.asptoday.com/">ASP Today</a> for practical ASP.NET Core architecture guides, security strategies, and real-world engineering practices. Join the Substack Chat and connect with developers building secure and resilient cloud-native applications. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Chaos Engineering for ASP.NET Core Applications: Testing Failure Scenarios]]></title><description><![CDATA[Discover how chaos engineering helps ASP.NET Core teams uncover weaknesses before production incidents happen. Learn practical failure-testing strategies for resilient systems. #ASPToday #aspnetcore #chaosengineering #resilience #softwarearchitecture]]></description><link>https://www.asptoday.com/p/chaos-engineering-for-aspnet-core</link><guid isPermaLink="false">https://www.asptoday.com/p/chaos-engineering-for-aspnet-core</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 09 Jun 2026 15:03:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!DECD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most teams spend their time trying to prevent failures. Chaos engineering takes a different approach. Instead of avoiding failure, it deliberately introduces controlled failures into systems to discover weaknesses before real incidents occur.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DECD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DECD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!DECD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!DECD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!DECD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DECD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2688796,&quot;alt&quot;:&quot;Tiny engineers test failures in a miniature theme park while keeping attractions running safely. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/200982536?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Tiny engineers test failures in a miniature theme park while keeping attractions running safely. " title="Tiny engineers test failures in a miniature theme park while keeping attractions running safely. " srcset="https://substackcdn.com/image/fetch/$s_!DECD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!DECD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!DECD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!DECD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0f79e17-0b2b-4e4d-8227-2687a10b9bf1_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, we&#8217;ll explore chaos engineering in <a href="https://asptoday.substack.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a>, how it works, when to use it, and how to safely test failure scenarios that improve reliability and resilience. </p><h2>Why Testing Success Is Not Enough</h2><p>Most application testing focuses on successful outcomes.</p><p>We verify that:</p><ul><li><p>APIs return expected responses</p></li><li><p>Database operations complete successfully</p></li><li><p>Authentication works correctly</p></li><li><p>Business workflows function as expected</p></li></ul><p>These tests are important.</p><p>But production environments rarely behave perfectly.</p><p>Real systems experience:</p><ul><li><p>Network interruptions</p></li><li><p>Database outages</p></li><li><p>Message queue failures</p></li><li><p>Cloud service disruptions</p></li><li><p>Slow dependencies</p></li><li><p>Infrastructure failures</p></li></ul><p>The challenge is that many of these situations are difficult to reproduce during normal testing.</p><p>Chaos engineering helps solve this problem.</p><h2>What Is Chaos Engineering?</h2><p>Chaos engineering is the practice of intentionally introducing failures into a system to observe how it responds.</p><p>The goal is not destruction.</p><p>The goal is learning.</p><p>By safely creating controlled failures, teams can identify weaknesses before customers experience them.</p><p>The concept was popularized by Netflix, which developed Chaos Monkey to randomly terminate production instances and verify that services remained available.</p><p><a href="https://netflix.github.io/chaosmonkey/">Official Chaos Monkey project</a>:</p><h2>Why Chaos Engineering Matters</h2><p>Imagine your application depends on:</p><ul><li><p>A SQL database</p></li><li><p>Redis cache</p></li><li><p>Payment provider</p></li><li><p>Email service</p></li><li><p>Azure Service Bus</p></li></ul><p>Everything works perfectly during development.</p><p>Then one day:</p><ul><li><p>Redis becomes unavailable</p></li><li><p>Payment API starts timing out</p></li><li><p>Network latency increases dramatically</p></li></ul><p>What happens?</p><p>Many teams discover the answer only after customers start reporting issues.</p><p>Chaos engineering helps uncover these weaknesses before they become incidents.</p><h2>Chaos Engineering Is Not Random Destruction</h2><p>A common misconception is that chaos engineering means breaking things randomly.</p><p>Effective chaos engineering is controlled and scientific.</p><p>Every experiment begins with a hypothesis.</p><p>For example:</p><blockquote><p>If Redis becomes unavailable, product pages should still load using database fallbacks.</p></blockquote><p>Then you test the hypothesis.</p><p>If reality differs from expectations, you&#8217;ve found an improvement opportunity.</p><h2>The Scientific Method for Reliability</h2><p>Chaos engineering follows a structured process:</p><ol><li><p>Define steady-state behavior</p></li><li><p>Create a hypothesis</p></li><li><p>Introduce controlled failure</p></li><li><p>Observe results</p></li><li><p>Improve the system</p></li></ol><p>This makes chaos engineering an engineering discipline rather than a guessing exercise.</p><h2>Understanding Steady State</h2><p>Before introducing failures, you need to understand normal behavior.</p><p>Examples include:</p><ul><li><p>Average response times</p></li><li><p>Error rates</p></li><li><p>Throughput</p></li><li><p>Queue depth</p></li><li><p>Resource utilization</p></li></ul><p>Without a baseline, it&#8217;s impossible to evaluate the impact of failure scenarios.</p><p>This is one reason observability is so important.</p><p>As discussed in our previous article on distributed tracing, visibility is critical when investigating system behavior.</p><h2>Chaos Engineering and Distributed Tracing</h2><p>Distributed tracing and chaos engineering work exceptionally well together.</p><p>Tracing helps answer:</p><ul><li><p>Which services were affected?</p></li><li><p>Where did failures originate?</p></li><li><p>How far did failures spread?</p></li><li><p>Which dependencies became bottlenecks?</p></li></ul><p>Using OpenTelemetry, engineers can visualize the impact of chaos experiments across an entire distributed system.</p><h2>Common Failure Scenarios</h2><p>Chaos engineering experiments often focus on realistic production failures.</p><p>Examples include:</p><ul><li><p>Service outages</p></li><li><p>Network latency</p></li><li><p>Packet loss</p></li><li><p>Dependency failures</p></li><li><p>Database connection exhaustion</p></li><li><p>High CPU usage</p></li><li><p>Memory pressure</p></li><li><p>Message queue delays</p></li></ul><p>These are failures that eventually happen in real systems.</p><p>The question is whether your application handles them gracefully.</p><h2>Simulating API Failures</h2><p>Suppose your application calls a payment provider.</p><p>Normally:</p><pre><code><code>var response = await _paymentClient.ProcessAsync(payment);</code></code></pre><p>What happens if:</p><ul><li><p>The API returns HTTP 500? </p></li><li><p>Requests timeout? </p></li><li><p>The service becomes unavailable?<br></p></li></ul><p>Chaos testing allows you to simulate these scenarios safely.</p><h2>Testing Timeouts</h2><p>Timeouts are one of the most common production issues.</p><p>A dependency may not fail completely.</p><p>Instead, it becomes extremely slow.</p><p>Example:</p><pre><code><code>await Task.Delay(TimeSpan.FromSeconds(30));</code></code></pre><p>How does your application react?</p><p>Do users receive helpful feedback?</p><p>Or does everything become stuck waiting indefinitely?</p><h2>Validating Retry Policies</h2><p>Our previous article explored:</p><ul><li><p>Exponential backoff </p></li><li><p>Jitter </p></li><li><p>Idempotency <br></p></li></ul><p>Chaos engineering helps verify those patterns actually work.</p><p>For example:</p><ul><li><p>Simulate API failures </p></li><li><p>Observe retries </p></li><li><p>Verify recovery<br></p></li></ul><p>Many teams discover retry configurations are too aggressive or too conservative.</p><p>Testing reveals these weaknesses.</p><h2>Circuit Breakers Under Stress</h2><p>Circuit breakers are designed to prevent failing dependencies from overwhelming a system.</p><p>Example using Polly:</p><pre><code><code>var circuitBreaker = Policy
    .Handle&lt;HttpRequestException&gt;()
    .CircuitBreakerAsync(
        5,
        TimeSpan.FromSeconds(30));</code></code></pre><p>Chaos testing verifies:</p><ul><li><p>Does the breaker open correctly? </p></li><li><p>Does traffic stop flowing? </p></li><li><p>Does recovery happen automatically?<br></p></li></ul><p>Without testing, assumptions remain unverified.</p><h2>Database Failure Experiments</h2><p>Databases are among the most critical dependencies.</p><p>Experiments may include:</p><ul><li><p>Connection failures </p></li><li><p>High latency </p></li><li><p>Deadlocks </p></li><li><p>Resource exhaustion<br></p></li></ul><p>Questions to ask:</p><ul><li><p>Does the application fail gracefully? </p></li><li><p>Are users informed properly? </p></li><li><p>Do background processes recover? <br></p></li></ul><p>These are valuable insights before production incidents occur.</p><h2>Testing Distributed Caching Failures</h2><p>Many ASP.NET Core systems rely on Redis.</p><p>What happens if Redis becomes unavailable?</p><p>A well-designed system should:</p><ul><li><p>Continue operating </p></li><li><p>Fall back to database queries </p></li><li><p>Maintain acceptable performance<br></p></li></ul><p>Chaos experiments validate these assumptions.</p><h2>Message Queue Failures</h2><p>Applications using Azure Service Bus or RabbitMQ should test scenarios such as:</p><ul><li><p>Delayed message delivery </p></li><li><p>Queue unavailability </p></li><li><p>Poison messages </p></li><li><p>High backlog conditions<br></p></li></ul><p>Questions include:</p><ul><li><p>Are messages retried correctly? </p></li><li><p>Are dead-letter queues used properly? </p></li><li><p>Does the system recover automatically? </p><p></p></li></ul><h2>Chaos Engineering and Saga Patterns</h2><p>Sagas coordinate distributed transactions.</p><p>Failures can occur during:</p><ul><li><p>Inventory reservation </p></li><li><p>Payment processing </p></li><li><p>Shipment creation<br></p></li></ul><p>Chaos testing helps verify:</p><ul><li><p>Compensation actions execute correctly </p></li><li><p>Eventual consistency is maintained </p></li><li><p>Workflows recover safely<br></p></li></ul><p>This is especially valuable in complex business processes.</p><h2>Infrastructure-Level Experiments</h2><p>Not all chaos experiments target application code.</p><p>Infrastructure testing can include:</p><ul><li><p>Container restarts </p></li><li><p>VM shutdowns </p></li><li><p>Kubernetes pod failures </p></li><li><p>DNS issues </p></li><li><p>Network partitions<br></p></li></ul><p>Modern cloud-native systems should tolerate these conditions.</p><h2>Latency Injection</h2><p>Sometimes dependencies do not fail.</p><p>They simply become slow.</p><p>Latency injection simulates this behavior.</p><p>Example:</p><pre><code><code>app.Use(async (context, next) =&gt;
{
    await Task.Delay(2000);
    await next();
});</code></code></pre><p>This helps reveal:</p><ul><li><p>Timeout issues </p></li><li><p>User experience problems </p></li><li><p>Resource bottlenecks </p></li></ul><h2>Fault Injection Middleware</h2><p>ASP.NET Core makes it easy to inject failures.</p><p>Example:</p><pre><code><code>app.Use(async (context, next) =&gt;
{
    if (Random.Shared.Next(100) &lt; 10)
    {
        context.Response.StatusCode = 500;
        return;
    }

    await next();
});</code></code></pre><p>This introduces controlled failures into requests.</p><p>Such experiments should only be used in non-production environments unless carefully managed.</p><h2>Monitoring During Chaos Experiments</h2><p>Observability is essential.</p><p>Monitor:</p><ul><li><p>Error rates </p></li><li><p>Response times </p></li><li><p>Queue depth </p></li><li><p>Memory consumption </p></li><li><p>CPU utilization </p></li><li><p>Retry activity<br></p></li></ul><p>Without visibility, chaos experiments provide little value.</p><h2>Defining Blast Radius</h2><p>One of the most important concepts in chaos engineering is blast radius.</p><p>Blast radius refers to the scope of impact.</p><p>Start small.</p><p>Instead of testing the entire platform:</p><ul><li><p>Test one service </p></li><li><p>Test one dependency </p></li><li><p>Test one workflow </p></li></ul><p>Expand gradually as confidence increases.</p><h2>Running Experiments Safely</h2><p>Every chaos experiment should include:</p><ul><li><p>Clear objectives </p></li><li><p>Success criteria </p></li><li><p>Monitoring </p></li><li><p>Rollback plans </p></li></ul><p>Safety must always come first.</p><p>The goal is learning, not causing outages.</p><h2>Common Mistakes</h2><p>One mistake is introducing failures without clear hypotheses.</p><p>Another is performing experiments without sufficient observability.</p><p>Also avoid:</p><ul><li><p>Testing too much at once </p></li><li><p>Running experiments without rollback procedures </p></li><li><p>Ignoring lessons learned<br></p></li></ul><p>The experiment is only valuable if it produces actionable insights.</p><h2>Real-World Example: E-Commerce Platform</h2><p>Imagine an online store.</p><p>Chaos experiments might simulate:</p><ul><li><p>Redis outage </p></li><li><p>Payment provider latency </p></li><li><p>Inventory service failure </p><p></p></li></ul><p>Expected behavior:</p><ul><li><p>Cached data falls back to database </p></li><li><p>Payments retry automatically </p></li><li><p>Inventory failures trigger compensating actions<br></p></li></ul><p>If the platform remains operational, confidence increases significantly.</p><h2>The Relationship Between Chaos and Reliability</h2><p>Chaos engineering is not about proving systems are perfect.</p><p>It is about discovering where they are fragile.</p><p>Every weakness uncovered is an opportunity to improve resilience.</p><p>Over time, systems become stronger because failures are explored proactively rather than reactively.</p><h2>When NOT to Use Chaos Engineering</h2><p>Small internal applications may not need extensive chaos testing.</p><p>Likewise, teams lacking:</p><ul><li><p>Monitoring </p></li><li><p>Alerting </p></li><li><p>Operational maturity<br></p></li></ul><p>Should establish those foundations first.</p><p>Chaos engineering works best when observability already exists.</p><h2>How This Fits Your ASP.NET Core Journey</h2><p>So far, we&#8217;ve explored:</p><ul><li><p>Distributed messaging </p></li><li><p>Saga patterns </p></li><li><p>Retry strategies </p></li><li><p>Fault-tolerant systems </p></li><li><p>OpenTelemetry and distributed tracing<br></p></li></ul><p>Chaos engineering brings these concepts together.</p><p>It validates whether resilience patterns actually work under realistic failure conditions.</p><p>This is where architecture moves from theory into real-world operational confidence.</p><h2>Closing Thoughts</h2><p>Failures are inevitable.</p><p>The most resilient systems are not those that avoid failure entirely.</p><p>They are the systems that have already practiced failure.</p><p>Chaos engineering provides a structured way to uncover weaknesses, validate assumptions, and strengthen ASP.NET Core applications before production incidents occur.</p><p>By combining:</p><ul><li><p>Observability </p><p>Distributed tracing </p></li><li><p>Retries </p></li><li><p>Circuit breakers </p></li><li><p>Sagas </p></li><li><p>Fault injection<br></p></li></ul><p>Teams can build systems that remain reliable even when the unexpected happens.</p><h2>Join The Community</h2><p>Enjoyed this article? Subscribe to ASP Today for practical <a href="https://www.asptoday.com/">ASP.NET Core</a> architecture guides, resilience strategies, and real-world engineering practices. Join the Substack Chat and connect with developers building modern cloud-native applications. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Deep Dive into Distributed Tracing in ASP.NET Core: OpenTelemetry in Practice]]></title><description><![CDATA[Learn how OpenTelemetry helps ASP.NET Core developers trace requests across APIs, databases, queues, and microservices. #ASPToday #aspnetcore #opentelemetry #observability #distributedsystems]]></description><link>https://www.asptoday.com/p/deep-dive-into-distributed-tracing</link><guid isPermaLink="false">https://www.asptoday.com/p/deep-dive-into-distributed-tracing</guid><dc:creator><![CDATA[Jenny Muralidharan]]></dc:creator><pubDate>Tue, 02 Jun 2026 15:04:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!JpCM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As applications become more distributed, understanding what happens during a single user request becomes increasingly difficult. A request may travel through multiple APIs, databases, caches, message queues, and background services before completing. Distributed tracing solves this challenge by providing end-to-end visibility into request flows. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JpCM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JpCM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png 424w, https://substackcdn.com/image/fetch/$s_!JpCM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png 848w, https://substackcdn.com/image/fetch/$s_!JpCM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png 1272w, https://substackcdn.com/image/fetch/$s_!JpCM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JpCM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png" width="1246" height="690" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:690,&quot;width&quot;:1246,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1599383,&quot;alt&quot;:&quot;Miniature figures in a vast, dark library follow a winding trail of glowing books. &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.asptoday.com/i/200228658?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Miniature figures in a vast, dark library follow a winding trail of glowing books. " title="Miniature figures in a vast, dark library follow a winding trail of glowing books. " srcset="https://substackcdn.com/image/fetch/$s_!JpCM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png 424w, https://substackcdn.com/image/fetch/$s_!JpCM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png 848w, https://substackcdn.com/image/fetch/$s_!JpCM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png 1272w, https://substackcdn.com/image/fetch/$s_!JpCM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fb4f5e9-7428-483f-be95-d4dda983a044_1246x690.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this guide, you&#8217;ll learn how distributed tracing works, why OpenTelemetry has become the industry standard, and how to implement practical tracing solutions in <a href="https://asptoday.substack.com/p/introduction-to-aspnet-core-whats-new-and-why-it-matters">ASP.NET Core</a> applications. </p><h2>Why Traditional Logging Is No Longer Enough</h2><p>Years ago, many applications were relatively simple.</p><p>A user made a request.</p><p>The request hit a web server.</p><p>The application queried a database.</p><p>A response was returned.</p><p>If something went wrong, logs were usually enough.</p><p>Modern systems are very different.</p><p>A single request may now travel through:</p><ul><li><p>API Gateway</p></li><li><p>ASP.NET Core Web API</p></li><li><p>Authentication Service</p></li><li><p>Product Service</p></li><li><p>Redis Cache</p></li><li><p>SQL Database</p></li><li><p>Message Queue</p></li><li><p>Background Worker</p></li></ul><p>Each component may have its own logs.</p><p>When failures occur, finding the root cause becomes difficult.</p><p>This is where distributed tracing becomes essential. </p><h2>What Is Distributed Tracing?</h2><p>Distributed tracing allows developers to follow a request as it moves through multiple systems.</p><p>Think of it as attaching a GPS tracker to every request.</p><p>Instead of viewing isolated logs, you can see:</p><ul><li><p>Where the request started</p></li><li><p>Which services it visited</p></li><li><p>How long each step took</p></li><li><p>Where failures occurred</p></li><li><p>Which dependencies caused delays</p></li></ul><p>Distributed tracing provides a complete picture of request execution.  </p><h2>Understanding Traces, Spans, and Context</h2><p>Distributed tracing revolves around three key concepts.</p><h2>Trace</h2><p>A trace represents the complete journey of a request.</p><p>For example:</p><pre><code><code>User Request
 &#9500;&#9472; API Gateway
 &#9500;&#9472; Product Service
 &#9500;&#9472; SQL Database
 &#9500;&#9472; Redis Cache
 &#9492;&#9472; Response</code></code></pre><p>Everything belongs to a single trace.</p><h2>Span</h2><p>A span represents an individual operation.</p><p>Examples include:</p><ul><li><p>Calling a database </p></li><li><p>Querying Redis </p></li><li><p>Sending a message </p></li><li><p>Invoking another API </p></li></ul><p>Each trace contains multiple spans.</p><h2>Context</h2><p>Context links spans together.</p><p>Without context propagation, traces break apart and become useless.</p><p>OpenTelemetry automatically handles much of this process.</p><h2>Why OpenTelemetry Has Become the Standard</h2><p>Before OpenTelemetry, observability tools often used proprietary approaches.</p><p>Organizations frequently became locked into specific vendors.</p><p>OpenTelemetry was created to solve this problem.</p><p>It provides:</p><ul><li><p>Vendor-neutral instrumentation </p></li><li><p>Standardized telemetry collection </p></li><li><p>Cross-platform support </p></li><li><p>Broad ecosystem integration </p></li></ul><p>OpenTelemetry is now supported by major platforms including:</p><ul><li><p>Microsoft Azure </p></li><li><p>AWS </p></li><li><p>Google Cloud </p></li><li><p>Grafana </p></li><li><p>Datadog </p></li><li><p>New Relic </p></li></ul><p>Official project:</p><p>https://opentelemetry.io</p><h2>Observability vs Monitoring</h2><p>These terms are often confused.</p><p>Monitoring focuses on answering known questions.</p><p>Examples:</p><ul><li><p>Is the service running? </p></li><li><p>How many requests failed? </p></li><li><p>Is CPU usage high? </p></li></ul><p>Observability helps answer unknown questions.</p><p>Examples:</p><ul><li><p>Why is checkout suddenly slow? </p></li><li><p>Which dependency caused latency spikes? </p></li><li><p>Which service introduced failures? </p></li></ul><p>Distributed tracing is a core pillar of observability.</p><h2>The Three Pillars of Observability</h2><p>Modern observability typically includes:</p><ul><li><p>Logs </p></li><li><p>Metrics </p></li><li><p>Traces<br></p></li></ul><p>Logs explain what happened.</p><p>Metrics show how often something happens.</p><p>Traces reveal where it happened.</p><p>Together they provide a complete operational picture.</p><h2>Why Tracing Matters in ASP.NET Core</h2><p>Consider an e-commerce application.</p><p>A customer clicks &#8220;Place Order.&#8221;</p><p>The request triggers:</p><ol><li><p>Authentication </p></li><li><p>Inventory validation </p></li><li><p>Payment processing </p></li><li><p>Shipping creation </p></li><li><p>Notification service <br></p></li></ol><p>A failure occurs.</p><p>Which step caused the issue?</p><p>Without tracing:</p><ul><li><p>Search logs manually </p></li><li><p>Correlate timestamps </p></li><li><p>Guess relationships<br></p></li></ul><p>With tracing:</p><ul><li><p>View the entire workflow instantly<br></p></li></ul><p>This becomes especially valuable in architectures we&#8217;ve discussed previously:</p><ul><li><p>ASP.NET Core and Azure Service Bus </p></li><li><p>Saga Patterns </p></li><li><p>Fault-Tolerant Systems </p></li><li><p>Advanced Retry Strategies </p><p></p></li></ul><p>Distributed tracing ties these systems together.</p><h2>Installing OpenTelemetry</h2><p>Start by adding required packages.</p><pre><code><code>dotnet add package OpenTelemetry.Extensions.Hosting

dotnet add package OpenTelemetry.Instrumentation.AspNetCore

dotnet add package OpenTelemetry.Instrumentation.Http

dotnet add package OpenTelemetry.Instrumentation.SqlClient</code></code></pre><p>These packages provide automatic instrumentation.</p><h2>Configuring OpenTelemetry</h2><p>In Program.cs:</p><pre><code><code>builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =&gt;
    {
        tracing
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddSqlClientInstrumentation();
    });</code></code></pre><p>This immediately starts collecting trace information.</p><h2>Automatic Instrumentation</h2><p>One of OpenTelemetry&#8217;s greatest strengths is automatic instrumentation.</p><p>Without changing business logic, you can collect traces from:</p><ul><li><p>Incoming HTTP requests </p></li><li><p>Outgoing HTTP calls </p></li><li><p>SQL queries </p></li><li><p>Entity Framework operations </p></li><li><p>gRPC services<br></p></li></ul><p>This dramatically reduces implementation effort.</p><h2>Viewing Traces Locally</h2><p>During development, traces can be exported to the console.</p><pre><code><code>builder.Services.AddOpenTelemetry()
    .WithTracing(builder =&gt;
    {
        builder
            .AddAspNetCoreInstrumentation()
            .AddConsoleExporter();
    });</code></code></pre><p>This helps developers understand how traces are structured.</p><h2>Understanding Trace Hierarchies</h2><p>Imagine this workflow:</p><pre><code><code>Order Request
 &#9500;&#9472; Validate User
 &#9500;&#9472; Check Inventory
 &#9474;   &#9500;&#9472; Redis Cache
 &#9474;   &#9492;&#9472; SQL Query
 &#9500;&#9472; Process Payment
 &#9492;&#9472; Send Confirmation</code></code></pre><p>Each operation becomes a span.</p><p>The complete tree becomes a trace.</p><p>This hierarchy makes bottlenecks easy to identify.</p><div><hr></div><h2>Adding Custom Spans</h2><p>Automatic instrumentation is powerful.</p><p>But custom spans provide additional business insight.</p><p>Example:</p><pre><code><code>private static readonly ActivitySource ActivitySource =
    new("OrderService");

public async Task ProcessOrderAsync()
{
    using var activity =
        ActivitySource.StartActivity("ProcessOrder");

    await Task.Delay(100);
}</code></code></pre><p>This creates a custom span visible in trace visualizations.</p><h2>Capturing Business Operations</h2><p>Not everything revolves around technical dependencies.</p><p>Sometimes you want visibility into business workflows.</p><p>Examples:</p><ul><li><p>Calculate Discount </p><p>Validate Coupon </p></li><li><p>Generate Invoice </p></li><li><p>Process Refund <br></p></li></ul><p>Custom spans allow these operations to appear in traces.</p><h2>Context Propagation</h2><p>Tracing only works if context moves between services.</p><p>Consider:</p><pre><code><code>API A
 &#8595;
API B
 &#8595;
API C</code></code></pre><p>If trace context is lost between services:</p><ul><li><p>Visibility breaks </p></li><li><p>Relationships disappear<br></p></li></ul><p>OpenTelemetry automatically propagates context through standard HTTP headers.</p><p>This allows traces to remain connected.</p><h2>Tracing HTTP Requests</h2><p>Outgoing HTTP requests are automatically captured.</p><p>Example:</p><pre><code><code>var response =
    await _httpClient.GetAsync("/products");</code></code></pre><p>OpenTelemetry records:</p><ul><li><p>URL </p></li><li><p>Duration </p></li><li><p>Status code </p></li><li><p>Parent trace </p></li></ul><p>This creates a complete dependency map.</p><h2>Tracing Database Calls</h2><p>Database performance often becomes a bottleneck.</p><p>OpenTelemetry automatically traces:</p><pre><code><code>await _dbContext.Products
    .Where(p =&gt; p.IsActive)
    .ToListAsync();</code></code></pre><p>Useful information includes:</p><ul><li><p>Query duration </p></li><li><p>Database dependency </p></li><li><p>Success or failure </p></li></ul><p>This greatly simplifies performance investigations.</p><h2>Tracing Message-Based Architectures</h2><p>Many modern ASP.NET Core systems rely on messaging.</p><p>Examples include:</p><ul><li><p>Azure Service Bus </p></li><li><p>RabbitMQ </p></li><li><p>Kafka </p></li></ul><p>Distributed tracing becomes even more important because workflows become asynchronous.</p><p>Tracing helps developers understand:</p><ul><li><p>Message publication </p></li><li><p>Message consumption </p></li><li><p>Processing delays </p></li><li><p>Queue bottlenecks<br></p></li></ul><h2>OpenTelemetry and ASP.NET Core APIs</h2><p>Minimal APIs work seamlessly.</p><pre><code><code>app.MapGet("/products", async () =&gt;
{
    return Results.Ok();
});</code></code></pre><p>Each request automatically generates trace information.</p><p>No additional code required.</p><h2>Sampling Strategies</h2><p>Not every trace needs to be collected.</p><p>High-volume systems can generate millions of traces daily.</p><p>Sampling helps control costs.</p><p>Common approaches:</p><ul><li><p>Always sample </p></li><li><p>Never sample </p></li><li><p>Percentage-based sampling </p></li><li><p>Adaptive sampling<br></p></li></ul><p>Example:</p><pre><code><code>.SetSampler(new TraceIdRatioBasedSampler(0.1))</code></code></pre><p>This collects approximately 10% of traces.</p><h2>Exporting Trace Data</h2><p>OpenTelemetry separates collection from storage.</p><p>Popular destinations include:</p><ul><li><p>Azure Monitor </p></li><li><p>Jaeger </p></li><li><p>Zipkin </p></li><li><p>Grafana Tempo </p></li><li><p>Datadog<br></p></li></ul><p>This flexibility prevents vendor lock-in.</p><h2>Visualizing Traces with Jaeger</h2><p>Jaeger is one of the most popular tracing platforms.</p><p><a href="https://www.jaegertracing.io/">Official website</a></p><p>It provides:</p><ul><li><p>Trace timelines </p></li><li><p>Dependency graphs </p></li><li><p>Span details </p></li><li><p>Performance analysis<br></p></li></ul><p>Developers can visually inspect entire request journeys.</p><h2>Diagnosing Performance Problems</h2><p>Suppose a page suddenly becomes slow.</p><p>Tracing reveals:</p><pre><code><code>Request Duration: 3.8s

Authentication: 40ms
Product Service: 120ms
Redis Cache: 5ms
SQL Query: 3.4s</code></code></pre><p>The bottleneck becomes obvious immediately.</p><p>Without tracing, finding this issue could take hours.</p><div><hr></div><h2>Tracing and Distributed Caching</h2><p>Distributed caching often improves performance dramatically.</p><p>But cache misses can still cause problems.</p><p>Tracing helps identify:</p><ul><li><p>Cache hit rates </p></li><li><p>Cache miss paths </p></li><li><p>Database fallbacks<br></p></li></ul><p>This pairs naturally with distributed caching architectures.</p><h2>Tracing Fault-Tolerant Systems</h2><p>In our previous article on fault tolerance, we discussed:</p><ul><li><p>Retries </p></li><li><p>Circuit breakers </p></li><li><p>Bulkheads </p></li><li><p>Graceful degradation </p></li></ul><p>Tracing makes these patterns visible.</p><p>You can observe:</p><ul><li><p>Retry attempts </p></li><li><p>Circuit breaker activations </p></li><li><p>Fallback executions <br></p></li></ul><p>This turns resilience from theory into measurable behavior.</p><h2>Tracing Saga Workflows</h2><p>Saga patterns coordinate long-running distributed transactions.</p><p>Tracing allows developers to follow:</p><ul><li><p>Order creation </p></li><li><p>Inventory reservation </p></li><li><p>Payment processing </p></li><li><p>Shipment generation<br></p></li></ul><p>Across multiple services. </p><p>This provides enormous operational value.</p><h2>Security Considerations</h2><p>Be careful with trace data.</p><p>Avoid recording:</p><ul><li><p>Passwords </p></li><li><p>Credit card numbers </p></li><li><p>Personal information </p></li><li><p>Authentication tokens <br></p></li></ul><p>Telemetry should support troubleshooting without exposing sensitive data.</p><p><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-10.0">Microsoft guidance</a>:</p><p></p><h2>Common Mistakes</h2><p>A frequent mistake is collecting traces without using them.</p><p>Another is tracing everything indiscriminately.</p><p>This creates:</p><ul><li><p>Storage costs </p></li><li><p>Noise </p></li><li><p>Analysis difficulties </p></li></ul><p>Focus on meaningful visibility.</p><h2>When Distributed Tracing Is Most Valuable</h2><p>Distributed tracing provides the greatest value when systems include:</p><ul><li><p>Multiple services </p></li><li><p>External APIs </p></li><li><p>Message queues </p></li><li><p>Background processing </p></li><li><p>Cloud infrastructure<br></p></li></ul><p>Simple applications may not require extensive tracing.</p><p>Complex systems almost always benefit.</p><h2>Real-World Example</h2><p>Imagine a food delivery platform.</p><p>A customer places an order.</p><p>The workflow touches:</p><ul><li><p>User Service </p></li><li><p>Restaurant Service </p></li><li><p>Payment Service </p></li><li><p>Delivery Service </p></li><li><p>Notification Service<br></p></li></ul><p>A trace shows every step.</p><p>If delays occur, engineers can pinpoint the exact dependency causing problems.</p><p>This is the power of distributed tracing.</p><h2>How This Fits Your ASP.NET Core Journey</h2><p>So far, we&#8217;ve explored:</p><ul><li><p>Azure Service Bus </p></li><li><p>Saga Patterns </p></li><li><p>Distributed Caching </p></li><li><p>Retry Strategies </p></li><li><p>Fault-Tolerant Systems<br></p></li></ul><p>Distributed tracing now provides the visibility layer that ties all these architectural patterns together.</p><p>Building distributed systems is only half the challenge.</p><p>Understanding them in production is the other half.</p><h2>Closing Thoughts</h2><p>Modern ASP.NET Core systems are increasingly distributed.</p><p>As complexity grows, traditional logging alone becomes insufficient.</p><p>OpenTelemetry provides a standardized, vendor-neutral approach to distributed tracing that helps developers understand exactly how requests move through their systems.</p><p>By implementing tracing early, teams gain:</p><ul><li><p>Faster troubleshooting </p></li><li><p>Better performance visibility </p></li><li><p>Improved reliability </p></li><li><p>Stronger operational insight<br></p></li></ul><p>As your applications scale, distributed tracing becomes less of a luxury and more of a necessity.</p><h2>Join The Community</h2><p>Enjoyed this article? <a href="https://www.asptoday.com/">Subscribe to ASP Today</a> for practical ASP.NET Core architecture guides, observability strategies, and real-world engineering patterns. Join the Substack Chat and connect with developers building modern cloud-native applications. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.asptoday.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.asptoday.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>]]></content:encoded></item></channel></rss>