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?
- One query loaded all orders.
- 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 Modifiedwith no response body. - Use the
Cache-Controlheader to tell caches what they can store and for how long. - Use
public, max-age=...for shared data. - Use
private, no-storefor 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:
- How to identify N+1 query problems.
- How batched queries improve performance.
- How ETags and conditional requests work.
- How Cache-Control affects browsers and caches.
- 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-storefor personal information. - Database profiling tools expose N+1 query problems.
- High-traffic APIs rely heavily on caching and compression to reduce load.
Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| Performance gets worse as data grows | N+1 queries | Use batched queries or eager loading |
304 Not Modified never occurs | ETags are missing or compared incorrectly | Send and compare complete ETag values |
| Users see outdated information | ETags never change | Update ETags whenever data changes |
| One user sees another user's information | Personalized data is cached publicly | Use private, no-store |
| Responses consume too much bandwidth | Compression is disabled | Enable response compression |
| Slow requests are missed | Only averages are monitored | Track 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
304with no body. - Cache-Control says who may keep a reply:
public, max-agefor shared data, andprivate, no-storefor 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.