API Performance and Caching

By the Syncfusion DevTutorials team · Last updated

Make your API fast by not repeating work: fix N+1 queries, answer repeat visits with 304, and tell caches what they may keep.

API performance is about how quickly your API responds and how efficiently it uses resources. Caching is the practice of storing a copy of a response, so that it can be reused instead of being generated again. Together, performance optimization and caching help APIs handle more users, respond faster, and reduce unnecessary work.

A simple rule to remember is: The fastest request is the one you do not have to process again.

Watch One Request Make Six Trips

This API returns five orders and their items. To measure how efficient it is, the example includes a small fake database that counts how many database queries are made during a request. The count appears in the response header X-Db-Queries. Press Run.

Program.cs — N+1 queries

The response looks correct. You receive five orders and their related items. However, notice the header: X-Db-Queries: 6

How did that happen?

  1. One query loaded all orders.
  2. Five additional queries loaded items for each order.

The total is 1 + 5 = 6 queries. With 100 orders, it would be 101 queries. With 1,000 orders, it would be 1001 queries. This is called an N+1 query problem.

In simple words: the API keeps repeating the same database operation instead of fetching everything efficiently.

TL;DR

  • N+1 queries happen when one query loads a list and another query runs for each item.
  • Use a batched query to load related data in one operation.
  • An ETag identifies a specific version of a response.
  • Clients send ETags back using the request header If-None-Match.
  • If nothing changed, return 304 Not Modified with no response body.
  • Use the Cache-Control header to tell caches what they can store and for how long.
  • Use public, max-age=... for shared data.
  • Use private, no-store for personal data.
  • Change the ETag whenever the data changes.

A Real-World Example

Imagine a busy coffee shop. Instead of walking to the storeroom every time someone orders milk, the barista brings enough milk for many drinks in a single trip. The barista also prepares popular drinks in advance, rather than starting from scratch each time.

The goal is simple: avoid repeating work, avoid unnecessary trips, and reuse results whenever possible. Caching follows the same principle.

N+1 performs one query for the list and additional queries for each item. A batched query loads related data efficiently.

What You Will Learn

In this lesson, you will learn:

  1. How to identify N+1 query problems.
  2. How batched queries improve performance.
  3. How ETags and conditional requests work.
  4. How Cache-Control affects browsers and caches.
  5. How cache invalidation keeps data accurate.

Fix N+1 with One Batched Query

The solution is to fetch all related data in a single query. Conceptually:

SELECT *
FROM OrderItems
WHERE OrderId IN (101,102,103,104,105)

The application then groups those items in memory.

Program.cs — one batched query

The response body is unchanged. The difference appears in the header: X-Db-Queries: 2

There is only one query for the orders, and one query for the items. Even if there are 1,000 orders, it stays at 2 queries instead of 1001 queries.

Every database query has a cost. A few milliseconds may not matter once, but hundreds of small queries add up quickly. In Entity Framework Core, methods such as .Include(o => o.Items) often remove common N+1 query problems.

Skip the Response Body with ETags

Some data changes rarely. For example, menu items, store locations, and product categories. If the client already has the latest copy, there is no reason to send it again.

An ETag identifies a specific version of a response. Example: ETag: "menu-1"

The client caches the response and sends the ETag back later. If nothing changed, the server returns 304 Not Modified.

Program.cs — ETag and 304

First Request

The response is 200 OK with ETag: "menu-1". The client stores the response and the ETag.

Second Request

The client sends If-None-Match: "menu-1". Because the data is unchanged, the server returns 304 Not Modified. No body is sent, and the client continues using its cached copy.

Third Request

The client sends If-None-Match: "menu-0". The tag is outdated, so the server returns 200 OK with the latest menu.

Always compare the complete ETag value, including the quotation marks. Compare "menu-1", not menu-1. The quotes are part of the ETag value.

Tell Caches What They May Store

Many caches can exist between a user and your API, such as the browser cache, the mobile cache, a corporate proxy, and a CDN. The Cache-Control header defines the caching rules.

Program.cs — Cache-Control

Shared Data

Example: Cache-Control: public, max-age=300

This means that any cache may store it, for 300 seconds. The API may not receive another request for five minutes. It is suitable for menus, product catalogs, and public reference data.

Personal Data

Example: Cache-Control: private, no-store

This means that no shared cache may store it, the browser should not keep a copy, and every request must fetch fresh data. It is suitable for orders, user profiles, account information, and payment history.

Never use public for user-specific information. Shared caches could accidentally serve the data of one person to another user. Use private, no-store for personalized responses.

Cache Invalidation

Caching only works if cached data stays accurate. When data changes, the cached version must become outdated. A common approach is to build ETags from a version number. Example: menu-1, menu-2, menu-3

Program.cs — invalidation

Example flow:

Initial Request

The response has the header ETag: "menu-1".

Menu Update

The request PUT /menu/1 changes a menu item. The version number goes from 1 → 2.

Next Request

The client sends If-None-Match: "menu-1". The server now responds with 200 OK and ETag: "menu-2". The stale cache is replaced.

This process is called cache invalidation.

Compression

Large JSON responses often contain repeated text. Response compression reduces the amount of data sent across the network.

Example:

builder.Services.AddResponseCompression(
    o => o.EnableForHttps = true);

app.UseResponseCompression();

The benefits are smaller responses, faster downloads, and less bandwidth usage. JSON commonly compresses extremely well.

Output Caching

Instead of running expensive code again for every request, some APIs cache complete responses in memory.

Example:

builder.Services.AddOutputCache();

app.UseOutputCache();

Example endpoint:

app.MapGet("/menu", () => menu)
   .CacheOutput(p =>
       p.Expire(TimeSpan.FromMinutes(5)));

The next request can be served directly from the cache, without running the endpoint logic again.

Measuring Performance with p95 Latency

Latency measures how long a request takes. Many teams focus on p95 latency rather than averages.

What p95 Means

If p95 = 200ms, then 95% of requests finished in 200ms or less, and 5% took longer. Those slow requests often represent real user complaints.

Why Not Use Only the Average?

Example: 9 requests = 20ms and 1 request = 500ms. The average is 68ms, so the average appears healthy. But one customer still experienced 500ms.

Monitoring p95 helps reveal those slow experiences.

Where You See This

  • Mobile apps use ETags to avoid downloading identical data repeatedly.
  • CDNs cache public responses using Cache-Control rules.
  • Shopping and banking systems use no-store for personal information.
  • Database profiling tools expose N+1 query problems.
  • High-traffic APIs rely heavily on caching and compression to reduce load.

Common mistakes

SymptomCauseFix
Performance gets worse as data growsN+1 queriesUse batched queries or eager loading
304 Not Modified never occursETags are missing or compared incorrectlySend and compare complete ETag values
Users see outdated informationETags never changeUpdate ETags whenever data changes
One user sees another user's informationPersonalized data is cached publiclyUse private, no-store
Responses consume too much bandwidthCompression is disabledEnable response compression
Slow requests are missedOnly averages are monitoredTrack p95 and p99 latency

Exercises

What to Remember

  • The fastest request is the one that you do not repeat.
  • Count your queries. N+1 grows with your data. A batched query stays at 2.
  • An ETag labels a version. A matching If-None-Match gets 304 with no body.
  • Cache-Control says who may keep a reply: public, max-age for shared data, and private, no-store for personal data.
  • When data changes, change the ETag.

Next: Rate Limiting and Resilience. You will protect your API when too many requests arrive at once, and you will keep it working when something that it depends on fails.