Performance describes how quickly and efficiently a system responds to requests and completes its work.
Imagine opening a shopping application and waiting 10 seconds for a product page to load. The application may be available and reliable, but the user experience is still poor.
Performance is therefore an important part of System Design.
What Is Performance?
Performance is mainly concerned with questions such as:
- How quickly does the system respond?
- How many requests can it process?
- How much CPU and memory does it use?
- How quickly can the database execute queries?
- How efficiently does the system use network and storage resources?
For example, suppose an API normally responds in 200 milliseconds.
If the same API starts taking 5 seconds during high traffic, there is a performance problem.
Real-World Example
Consider an e-commerce application.
A customer searches for a mobile phone.
flowchart LR
User --> API
API --> Database
Database --> API
API --> User
The request may involve several operations:
- Receive the request.
- Validate the request.
- Execute a database query.
- Process the result.
- Return the response.
If any of these operations is slow, the total response time increases.
Latency
Latency is the time taken for an operation to complete or for a response to be received.
For example:
Client → API = 20 ms
API → Database = 50 ms
Database processing = 80 ms
API processing = 20 ms
Response = 20 ms
The total time is approximately 190 milliseconds.
Reducing unnecessary work in any part of the request can improve the overall response time.
ASP.NET Core Example
Consider this API:
[HttpGet]
public async Task GetEmployees()
{
var employees = await _context.Employees
.ToListAsync();
return Ok(employees);
}
This works, but suppose the Employees table contains millions of records.
Returning every employee in one response can consume significant database, memory and network resources.
A better approach is pagination.
[HttpGet]
public async Task GetEmployees(
int page = 1,
int pageSize = 20)
{
var employees = await _context.Employees
.AsNoTracking()
.OrderBy(x => x.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return Ok(employees);
}
Now the API returns only the required records.
SQL Server Performance
Database queries are often an important part of application performance.
Consider this query:
SELECT *
FROM Employees
WHERE Department = 'IT';
If the table contains millions of rows and Department is not indexed appropriately, SQL Server may need to examine a large amount of data.
An index can help:
CREATE INDEX IX_Employees_Department
ON Employees(Department);
Now SQL Server can use the index to find matching records more efficiently.
Do Not Return Unnecessary Data
Another common performance problem is retrieving more columns than the application actually needs.
Instead of:
var employees = await _context.Employees
.ToListAsync();
we can select only the required fields:
var employees = await _context.Employees
.Select(x => new
{
x.Id,
x.Name,
x.Department
})
.ToListAsync();
This can reduce the amount of data transferred from the database.
Use Caching
Suppose product information is requested thousands of times but changes only occasionally.
Querying SQL Server for every request may be unnecessary.
A cache can store frequently requested data.
flowchart TD
Request --> Cache
Cache --> Found
Found --> Response
Found --> Database
Database --> Cache
Cache --> Response
When data is available in the cache, the application can return it without querying the database.
This can reduce database load and improve response time.
Database Connection Management
Applications communicate with SQL Server using database connections.
Creating unnecessary connections can hurt performance.
ASP.NET Core applications using Entity Framework Core normally rely on connection pooling and dependency injection to manage database connections efficiently.
For example:
builder.Services.AddDbContext(options =>
options.UseSqlServer(connectionString));
Connection pooling allows existing database connections to be reused instead of creating a completely new connection for every operation.
Async Programming
ASP.NET Core applications commonly use asynchronous operations for I/O work.
For example:
var employees = await _context.Employees
.AsNoTracking()
.ToListAsync();
Using asynchronous database calls allows the application to avoid blocking a thread while waiting for SQL Server to respond.
This becomes particularly useful when the application handles many concurrent requests.
External API Performance
Suppose our application calls a payment service.
flowchart LR
User --> API
API --> PaymentService
PaymentService --> ExternalAPI
ExternalAPI --> PaymentService
PaymentService --> API
API --> User
If the external API takes 5 seconds to respond, our API may also become slow if we wait synchronously for it.
We should consider:
- Timeouts
- Asynchronous processing
- Caching where appropriate
- Connection reuse
- Retry policies for suitable failures
Move Slow Work to Background Processing
Not every operation needs to happen before returning the API response.
Suppose a user places an order and we need to send an email notification.
We may not need to make the user wait for the email to be sent.
flowchart LR
User --> OrderAPI
OrderAPI --> Database
OrderAPI --> Queue
Queue --> NotificationService
NotificationService --> Email
The API can save the order and place a message on a queue. A background service can process the notification later.
This can reduce the response time experienced by the user.
Performance Bottlenecks
When an application becomes slow, we should not immediately add more servers.
First, identify the bottleneck.
It could be:
- Slow SQL query
- Missing database index
- Large response payload
- High CPU usage
- Memory pressure
- Network latency
- External API latency
- Too much application processing
- Locking or blocking in the database
flowchart TD
SlowAPI --> CheckApplication
SlowAPI --> CheckDatabase
SlowAPI --> CheckNetwork
SlowAPI --> CheckExternalService
CheckApplication --> FindBottleneck
CheckDatabase --> FindBottleneck
CheckNetwork --> FindBottleneck
CheckExternalService --> FindBottleneck
Monitoring Performance
Performance should be measured rather than guessed.
Useful metrics include:
- Average response time
- 95th percentile latency
- 99th percentile latency
- Requests per second
- CPU usage
- Memory usage
- Database query duration
- Error rate
For example, an average response time of 100 milliseconds may look good, but if 5% of requests take 5 seconds, users can still experience poor performance.
Performance vs Scalability
Performance and scalability are related but different.
Performance asks:
How quickly and efficiently can the system handle its current workload?
Scalability asks:
Can the system continue handling increasing workload as demand grows?
A system can be very fast for 1,000 users but fail when traffic increases to 1 million users.
Key Takeaways
- Performance is about how efficiently and quickly a system works.
- Slow database queries can become major performance bottlenecks.
- Indexes can improve database query performance.
- Pagination prevents unnecessarily large responses.
- Selecting only required columns reduces data transfer.
- Caching can reduce repeated database operations.
- Async I/O helps ASP.NET Core handle concurrent requests efficiently.
- Background processing can keep slow operations out of the main request path.
- Always measure performance and identify the actual bottleneck.
Conclusion
Performance is not about making only one part of an application fast.
We need to look at the complete request path, including the API, database, cache, network and external services.
Good performance comes from reducing unnecessary work, optimizing database operations, controlling payload sizes, using caching where appropriate and moving suitable long-running work to background processing.
But one question still remains: How many requests can our system actually handle?
That leads to the next concept: Latency vs Throughput.