An API may work correctly when its database responds quickly, but that does not mean it will behave reliably in production. Database queries can become slower during peak traffic, connection pools can run out of available connections, and a database server may become temporarily unavailable during maintenance or a network interruption.
When these conditions occur, API requests may take too long, return unexpected errors, or consume application resources while waiting for the database. If the application does not handle these situations properly, one slow dependency can affect unrelated requests and eventually make the entire service unresponsive.
Testing an API under database failure conditions helps developers discover these problems before they affect users. It verifies whether the API applies timeouts, returns appropriate status codes, releases resources, limits retries, and recovers when the database becomes available again.
This article explains how to test API behavior when database operations are slow or fail, how to simulate common failure scenarios, and how to validate timeouts, connection pooling, cancellation, and recovery in an ASP.NET Core application.
Understand How Database Problems Affect an API
A typical API request follows a sequence of operations:
The client sends an HTTP request.
ASP.NET Core receives the request and executes the endpoint.
The application obtains a database connection.
The database executes the query or transaction.
The application processes the result.
The API returns an HTTP response.
A delay at any database-related stage can increase the total request duration.
For example, suppose an endpoint normally returns a response in 100 milliseconds. During a database slowdown, the query takes five seconds. If the API has hundreds of concurrent requests, many requests may remain active while waiting for the same database.
This can increase memory usage, consume connection-pool capacity, and create additional queueing. New requests may then become slow even when their queries are relatively simple.
Database failure testing should cover more than whether the API returns an error. It should also verify how the application behaves under concurrent requests, how quickly it abandons failed operations, and whether it recovers without requiring a restart.
Identify the Failure Scenarios to Test
Before writing tests, define the database failures that matter to your application.
Scenario | What the test should verify |
|---|
Slow query | The API respects its latency and timeout requirements |
Database unavailable | The API returns a controlled error rather than exposing internal details |
Connection timeout | The request does not wait indefinitely |
Connection pool exhaustion | The application fails or queues work within defined limits |
Query cancellation | Abandoned requests do not continue unnecessary work longer than expected |
Temporary network interruption | The API handles the failure and recovers when connectivity returns |
Database restart | The application can establish new connections when the database is available again |
Concurrent failures | The application does not exhaust threads, memory, or connection resources |
Not every application needs every scenario. Prioritize tests based on the database technology, deployment architecture, API latency objectives, and consequences of failed requests.
Separate API Tests from Database Integration Tests
Different testing techniques reveal different problems.
Unit tests are useful for verifying application behavior when a repository or database service returns a simulated failure. They are fast and allow developers to test error handling without starting a database.
Integration tests verify that the API interacts correctly with a real database provider. They can reveal problems involving SQL execution, transactions, connection handling, and provider-specific exceptions.
Resilience tests introduce controlled delays, failures, or resource pressure to verify the application's behavior under adverse conditions.
A practical test suite uses all three where appropriate. A mocked repository cannot prove that a real database command honors cancellation, and a database integration test does not automatically prove that the API remains stable under heavy concurrency.
Build a Testable ASP.NET Core API
Consider an API that retrieves an order from a database. A useful design keeps the HTTP endpoint separate from the database access implementation so that each layer can be tested independently.
The following example uses an application service that can be replaced during testing.
public sealed record OrderDto(
int Id,
string Status,
decimal Total);
public interface IOrderService
{
Task<OrderDto?> GetOrderAsync(
int orderId,
CancellationToken cancellationToken);
}
The service accepts a CancellationToken so that the operation can respond when the request is cancelled or an application-level deadline is reached.
An endpoint can use this service as follows:
app.MapGet(
"/orders/{id:int}",
async (
int id,
IOrderService orderService,
CancellationToken cancellationToken) =>
{
var order = await orderService.GetOrderAsync(
id,
cancellationToken);
return order is null
? Results.NotFound()
: Results.Ok(order);
});
ASP.NET Core binds the endpoint's cancellation token to the HTTP request lifecycle. When the client disconnects or the request is otherwise aborted, the token can be cancelled.
However, passing the token to a service does not guarantee that every database operation stops immediately. The database provider and the underlying database must support the cancellation behavior, and the application must pass the token through to the relevant asynchronous operations.
This distinction should be verified in integration tests.
Test Slow Database Responses with a Fake Service
The simplest way to test API behavior under a delay is to replace the service with a test implementation.
For example, the fake service can simulate a database operation that takes several seconds.
public sealed class SlowOrderService : IOrderService
{
private readonly TimeSpan _delay;
public SlowOrderService(TimeSpan delay)
{
_delay = delay;
}
public async Task<OrderDto?> GetOrderAsync(
int orderId,
CancellationToken cancellationToken)
{
await Task.Delay(_delay, cancellationToken);
return new OrderDto(
orderId,
"Processing",
125.00m);
}
}
This implementation is useful for testing request cancellation and application-level latency handling. It does not simulate a real database query or connection-pool delay.
Register it in the test application's dependency injection container in place of the production service.
For example, a test factory based on ASP.NET Core's WebApplicationFactory can replace the service registration. The exact setup depends on how the application is structured.
Write a test for the delay
Using an HTTP client against the test server, measure the endpoint's response duration.
[Fact]
public async Task GetOrder_ReturnsOrderAfterServiceCompletes()
{
using var response = await _client.GetAsync("/orders/42");
Assert.Equal(
HttpStatusCode.OK,
response.StatusCode);
}
This example verifies a successful response after the fake operation completes. A separate test should verify cancellation and deadline behavior.
Avoid relying on a very tight timing assertion such as requiring a response to arrive within exactly 100 milliseconds. Test environments have variable scheduling and execution overhead. Prefer realistic upper bounds and ensure that the test measures the intended behavior rather than incidental machine performance.
Test API Timeouts and Cancellation
An API needs a clear policy for operations that take longer than the application can tolerate.
There are several distinct timeout concepts:
HTTP request deadline: The maximum time the application is willing to spend handling a request.
Database command timeout: The maximum execution time allowed for a database command by the provider.
Connection timeout: The maximum time spent establishing a database connection.
Client timeout: The maximum time the caller waits for a response.
These limits solve different problems. Setting one does not automatically configure the others.
Use a request deadline where appropriate
An application can use a linked cancellation token with a deadline for a database operation.
using var timeoutCts =
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(3));
var order = await orderRepository.GetOrderAsync(
orderId,
timeoutCts.Token);
This example creates a three-second deadline for the operation while preserving cancellation from the original request token.
The repository must pass the token to its asynchronous database calls. Cancellation is cooperative, so the actual stopping behavior depends on the provider and database operation.
Also, do not create a separate timeout in every layer without a coordinated policy. Conflicting deadlines can make behavior difficult to understand and can lead to unnecessary cancellations.
Decide what happens when the deadline expires
If the application cancels an operation because its own deadline expires, it may return a gateway timeout or service-unavailable response depending on the endpoint's role and error policy.
A request that is explicitly cancelled by the client should not automatically be treated as a server timeout.
Distinguish cancellation caused by the incoming request from cancellation caused by an application deadline. This helps avoid returning misleading errors and improves observability.
Test Database Command Timeouts
A database command timeout is different from a delay introduced in application code.
For example, Entity Framework Core allows applications to configure a command timeout:
optionsBuilder.UseSqlServer(
connectionString,
sqlOptions =>
sqlOptions.CommandTimeout(5));
This example configures a five-second command timeout for the SQL Server provider. It is not a universal configuration for every database provider.
For PostgreSQL, MySQL, and other databases, use the appropriate provider settings and verify their behavior in the installed provider version.
To test a command timeout, use a controlled integration-test environment and a query or database operation that reliably exceeds the configured limit. Verify the resulting exception, the API's response, and whether the connection and transaction are left in a usable state.
Do not use an arbitrary production query to force a timeout. A deliberately expensive query can affect other workloads and consume significant database resources.
Simulate a Database That Is Temporarily Unavailable
A fake service can simulate an exception, but it does not reproduce all the behavior of a database that cannot accept connections.
For realistic integration testing, run the application against a disposable database instance, such as a database container dedicated to the test suite.
The test can then perform the following steps:
Start the database and verify that it accepts connections.
Start the API and confirm that a normal request succeeds.
Make the database unavailable using a controlled test-environment mechanism.
Send requests to the API and verify its failure response.
Restore database connectivity.
Send another request and verify that the API recovers.
The mechanism used to interrupt connectivity depends on the test environment. A container can be stopped or isolated through the test harness, while a network-level test environment can introduce a controlled connection failure.
Avoid using a database restart as the only failure test. A restart may cause a brief interruption, but it does not necessarily reproduce DNS failures, dropped packets, connection exhaustion, or an unavailable database endpoint.
Verify the API response
When the database is unavailable, the API should not return raw SQL errors, connection strings, stack traces, or other internal details to the caller.
Choose a consistent error contract. Depending on the API's role, a temporary database outage may be represented as HTTP 503 Service Unavailable. A gateway that fails to receive a timely response from an upstream service may use HTTP 504 Gateway Timeout.
The correct status code depends on the service boundary and failure condition. Do not map every database exception to the same response without distinguishing timeouts, validation failures, missing records, and unexpected application errors.
Log the detailed exception internally while returning a safe, actionable error response to the client.
Test Connection Pool Exhaustion
Database connection pools reuse connections to reduce the cost of opening a new connection for every request. However, the pool has finite capacity.
If many requests hold connections while waiting for slow queries, new requests may have to wait for an available connection. This can create a second layer of latency before a query even begins.
To test this condition, use a controlled load test that generates concurrent database requests while introducing a safe, repeatable delay in the database operations.
Measure:
Number of active requests.
Database connections in use.
Connection acquisition wait time, where observable.
Database command duration.
HTTP response latency.
Request failures and timeouts.
CPU and memory consumption.
Avoid increasing the connection-pool size as the first response. More connections can increase database concurrency and make a struggling database slower.
Instead, determine whether requests hold connections longer than necessary, whether transactions remain open while unrelated work executes, and whether query performance needs improvement.
Test Retries Without Creating More Load
Retries can help recover from transient failures, but they can also amplify a database outage.
Suppose an API sends a database operation that fails. If every request retries the operation several times immediately, the database may receive more work precisely when it is least able to handle it.
Test the retry policy under repeated failures and confirm that it has appropriate limits, backoff, and cancellation behavior.
A reasonable retry policy should consider:
Whether the failure is transient.
Whether the operation is safe to retry.
Whether the original operation may already have committed.
How many attempts are permitted.
How long the caller can wait.
Whether the retry budget is shared across layers.
Retries for read-only operations may be simpler than retries for writes. If a write operation commits but the connection fails before the application receives confirmation, retrying blindly may duplicate the business operation.
For operations that must not be duplicated, use an appropriate idempotency strategy or transaction design rather than assuming that a timeout means nothing was committed.
Verify Recovery After the Database Returns
A service that returns an error during a database outage has passed only part of the test. It must also recover when the dependency becomes available again.
After restoring connectivity, verify that:
New requests can establish or reuse valid connections.
The connection pool does not remain stuck waiting on failed connections.
Requests succeed without restarting the API unnecessarily.
Retry behavior returns to normal.
Error rates and latency return to their expected ranges.
Transactions and application state remain consistent.
Some database providers automatically discard broken connections and establish replacements when needed. The exact behavior depends on the provider, connection-pool implementation, and failure type.
Test this behavior with the provider and configuration used by the actual application.
Common Mistakes When Testing Database Failures
Testing only with mocked dependencies
Mocks are useful for testing API error handling, but they cannot validate real connection timeouts, transaction behavior, or provider cancellation.
Use integration tests for those scenarios.
Treating all failures as timeouts
A refused connection, a failed authentication attempt, a command timeout, and a cancelled HTTP request are different conditions. Tests should verify that each is handled according to the application's error contract.
Adding retries without measuring their effect
Retries can increase load and delay recovery. Test them under concurrent failures, and ensure that attempts are bounded and operations are safe to repeat.
Ignoring connection-pool pressure
A slow database can keep connections occupied long enough to cause additional request failures. Monitor pool usage and test with realistic concurrency.
Assuming cancellation always stops the query immediately
Cancellation behavior depends on the application, provider, and database. Verify that cancellation reaches the database operation and that resources are released correctly.
Best Practices for Production API Reliability
Define acceptable response times and failure behavior for each endpoint. Configure connection and command timeouts according to the database provider and the application's latency requirements.
Propagate cancellation tokens through asynchronous application and database operations. Keep transactions short, avoid holding connections during unrelated work, and monitor connection-pool pressure.
Use structured logging and metrics to record database command duration, timeout counts, connection failures, and request latency. Avoid logging sensitive connection details or user data.
Run integration tests against the actual database engine and provider configuration. Introduce slow queries and temporary outages only in isolated test environments, and verify both failure handling and recovery.
Finally, review retry behavior, idempotency, and transaction semantics before deploying resilience changes. Reliability means more than returning an error quickly; it also means preserving data correctness and recovering predictably when dependencies return to normal.
Advantages and Disadvantages of Database Failure Testing
Advantages
Reveals timeout and error-handling defects before production incidents.
Identifies connection-pool exhaustion and resource contention.
Verifies that cancellation reaches database operations.
Tests recovery after temporary outages.
Helps confirm that retries do not duplicate operations or overwhelm the database.
Disadvantages
Realistic failure tests require a controlled environment and additional setup.
Database load tests can consume significant resources.
Timing-based tests can be flaky if they depend on narrow time limits.
Provider-specific behavior means that results may not transfer directly between database engines.
Testing every possible network and database failure can be expensive.
Summary
Database slowdowns and temporary outages are normal conditions that a reliable API should be prepared to handle. Start by testing the API's error handling with a fake database service, then use integration tests to verify real command timeouts, connection failures, cancellation, and recovery.
Measure connection-pool usage, query duration, request latency, and error rates under controlled concurrency. Test retry policies carefully, especially for write operations that may have committed before a connection failed.
The goal is to ensure that a slow or unavailable database does not cause the API to wait indefinitely, exhaust resources, expose internal errors, or corrupt application state. By testing failure behavior as deliberately as normal behavior, developers can build APIs that remain predictable when their database dependency is under pressure.
Join the conversation! Your thoughts help the community grow.