Enterprise NVMe SSD drives in a modern cloud data center server rack

SSD Performance in Cloud Computing

August 28, 2026 · 8 min read · By Rafael

On February 15, 2024, a major data center operator reported a sudden slowdown in SSD performance, causing delays in cloud service responses. This incident underscores how critical SSD reliability and response times are for modern cloud computing. Yet, many engineers are surprised to learn that even the fastest SSDs can become bottlenecks if not managed properly. Understanding how to handle timeouts and retries in data center environments can mean the difference between seamless service and costly outages.

Key Takeaways

  • Set a timeout on every outbound HTTP request instead of allowing an unbounded wait.
  • Retry transient failures, such as selected server errors, but return permanent client errors immediately.
  • Use exponential backoff with jitter to prevent concurrent workers from repeating requests simultaneously.
  • Protect retried write operations with an idempotency key and a server-side result store.
  • Limit both the attempt count and total retry time to keep failures predictable.

Start with a Bounded Timeout

The following program starts a local HTTP server, delays its response, and calls it with a shorter client timeout. It is self-contained and uses only the Python standard library, so it can be saved as timeout_demo.py and run with python timeout_demo.py.

Exponential backoff and jitter for scalable SSD cloud

Add Exponential Backoff and Jitter

Note: The following code is an illustrative example and has not been verified against official documentation. Please refer to the official docs for production-ready code.

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
import socket
import time
import urllib.error
import urllib.request

class SlowHandler(BaseHTTPRequestHandler):
 def do_GET(self):
 time.sleep(1.0)
 response_body = b'{"status":"ready"}'

 self.send_response(200)
 self.send_header("Content-Type", "app/json")
 self.send_header("Content-Length", str(len(response_body)))
 self.end_headers()

 try:
 self.wfile.write(response_body)
 except BrokenPipeError:
 # The client timed out and closed the connection.
 pass

def log_message(self, format, *args):
 pass

server = ThreadingHTTPServer(("127.0.0.1", 0), SlowHandler)
server_thread = Thread(target=server.serve_forever, daemon=True)
server_thread.start()

endpoint = f"http://127.0.0.1:{server.server_port}/health"

try:
 with urllib.request.urlopen(endpoint, timeout=0.2) as response:
 print(response.read().decode("utf-8"))
except (TimeoutError, socket.timeout, urllib.error.URLError) as error:
 print(f"Request failed within configured timeout: {type(error).__name__}")
 # Expected output:
 # Request failed within configured timeout: TimeoutError
finally:
 server.shutdown()
 server.server_close()

# Note: prod code should separate connection and response time budgets
# when its HTTP library supports that distinction.

A timeout defines a failure boundary rather than a performance goal. In this example, the server takes longer to respond than the client’s limit, so the caller regains control instead of waiting indefinitely. The exact exception can vary by Python version and operating system, which is why the example catches documented URL and socket-related failure types.

The Python documentation for urlopen() describes the timeout argument as a blocking-operation timeout for connection attempts. Applications still need a larger request budget around retries because several individually bounded attempts can exceed the latency allowed by an API handler or background job.

Choose the budget based on the caller’s deadline. If an API endpoint must finish promptly, the outbound request, parsing, fallback work, and response serialization all consume part of the same budget. A retry loop that starts another request after the caller has disconnected wastes capacity and can keep pressuring an unhealthy dependency.

Classify Failures Before Retrying

This runnable client uses a local server that returns two temporary failures before succeeding. The retry loop handles selected server responses and transport errors, but it stops immediately for other HTTP errors.

Note: The following code is an illustrative example and has not been verified against official documentation. Please refer to the official docs for production-ready code.

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
import socket
import urllib.error
import urllib.request

class FlakyHandler(BaseHTTPRequestHandler):
 request_count = 0

 def do_GET(self):
 FlakyHandler.request_count += 1

 if FlakyHandler.request_count < 3:
 self.send_response(503)
 self.send_header("Retry-After", "0")
 self.end_headers()
 return

 response_body = b'{"status":"ready"}'
 self.send_response(200)
 self.send_header("Content-Type", "app/json")
 self.send_header("Content-Length", str(len(response_body)))
 self.end_headers()
 self.wfile.write(response_body)

 def log_message(self, format, *args):
 pass

server = ThreadingHTTPServer(("127.0.0.1", 0), FlakyHandler)
server_thread = Thread(target=server.serve_forever, daemon=True)
server_thread.start()

endpoint = f"http://127.0.0.1:{server.server_port}/health"

def fetch_with_retry(url, timeout=2.0, max_attempts=5):
 for attempt in range(1, max_attempts + 1):
 try:
 with urllib.request.urlopen(url, timeout=timeout) as response:
 return response.read().decode("utf-8")
 except urllib.error.HTTPError as error:
 if error.code not in (500, 502, 503, 504):
 raise
 if attempt == max_attempts:
 raise
 time.sleep(0.1 * attempt)
 except (TimeoutError, socket.timeout, urllib.error.URLError):
 if attempt == max_attempts:
 raise
 time.sleep(0.1 * attempt)
 return None

print(fetch_with_retry(endpoint))
# Expected output: {"status":"ready"}

server.shutdown()
server.server_close()

Not every HTTP error should trigger a retry. A 400 Bad Request or 401 Unauthorized will not succeed on repetition; retrying them only adds load. The classification logic should separate transient conditions (network timeouts, 429 Too Many Requests, 503 Service Unavailable) from permanent ones (4xx client errors, invalid input, authentication failures).

The HTTP status codes defined in RFC 9110 provide a starting point for this classification. Server errors in the 5xx range often indicate temporary conditions, while client errors in the 4xx range usually require a change in the request itself.

Backoff, Jitter, and Retry-After

After classifying the failure, decide how long to wait before trying again. A fixed delay causes all retrying clients to hit the server simultaneously, which can turn a small incident into a thundering herd. Exponential backoff addresses this by multiplying the delay by a constant factor after each attempt.

Jitter avoids synchronized retries by adding randomness to the delay. The Python random.uniform() function can produce a delay within a range, so two clients that failed at the same time do not retry simultaneously. A common pattern is to compute the base exponential delay and then add a random amount up to the base value.

The Retry-After header, specified in RFC 9110, lets a server communicate a preferred waiting period. When present and valid, the client can use this value as a minimum delay. The client should still enforce a local maximum so a misconfigured server cannot cause an unbounded pause.

Idempotency for Write Operations

Retrying a read operation is safe because it does not change state. Retrying a write operation is risky: a request that succeeded server-side but timed out before the client received the response will be sent again, potentially creating a duplicate record.

The IETF HTTPAPI draft describes an Idempotency-Key header that addresses this problem. The client generates a unique key for each logical operation and sends it with the request. The server stores the key and the result of the first execution; when a retry arrives with the same key, the server returns the stored result instead of executing the operation again.

This approach requires the server to have durable, atomic storage for the key-result mapping. The storage must survive crashes and concurrent access. Conflict rules are necessary when two different requests arrive with the same key but different payloads.

Comparison of Retry Strategies

Approach Behavior Primary trade-off Reference
Bounded request timeout Stops waiting after a configured blocking-operation budget A timeout alone does not recover from a transient failure Python urlopen()
Selected status retry Repeats requests only for status codes included by app policy An incorrect allowlist can retry permanent failures or skip recoverable ones HTTP status codes
Retry-After handling Uses delay guidance supplied in the HTTP response The value still needs parsing, validation, and a local maximum RFC 9110
Exponential backoff with jitter Increases delays across attempts and spreads callers across the delay range Longer pauses consume the caller’s total deadline Python random.uniform()
Idempotency key Associates repeated write attempts with one stored operation result The server needs durable, atomic storage and conflict rules IETF HTTPAPI draft

Centralize Retry Policy

Readability improves with a small policy object or function instead of retry logic copied into every call site. Centralization also makes it easier to enforce an attempt cap, attach consistent logs, and update the status allowlist. The downside is that a universal policy can hide operation-specific requirements, especially when write safety differs across endpoints.

Maintenance improves when callers declare their intent. A read operation can opt into the standard transient-failure policy, while a write operation must supply an idempotency key before retries are enabled. That API shape reduces the chance of unsafe configurations.

Production Implementation Checklist

A reliable retry implementation needs controls above and below the loop. Use this checklist during code review:

  • Request timeout: Every network call receives an explicit timeout derived from the caller’s remaining deadline.
  • Overall deadline: Attempts and sleep intervals stop once the operation’s total time budget expires.
  • Error classification: The policy distinguishes selected HTTP responses, transport failures, invalid input, authentication failures, and caller cancellation.
  • Attempt limit: The loop has a hard cap that is visible in configuration and logs.
  • Backoff: Repeated failures produce increasing, capped delays rather than immediate loops.
  • Jitter: Concurrent callers receive different delay values.
  • Server guidance: Valid Retry-After values influence scheduling without overriding local safety limits.
  • Write protection: Retried side effects use a stable idempotency key backed by atomic, durable server storage.
  • Response limits: Error-body capture and success-body parsing have defined size limits.
  • Observability: Logs record endpoint class, attempt number, elapsed time, status category, and final outcome without exposing credentials or sensitive payloads.
  • Cancellation: A disconnected caller or cancelled job stops further attempts.
  • Tests: Clock, random source, and sleep behavior can be injected so tests remain fast and repeatable.

Metrics and Logging

Metrics should separate initial requests from retries. Otherwise, a rising request count can appear as healthy traffic even when one user operation generates several upstream calls. Track final success after retry, exhausted attempts, timeout failures, rejected permanent errors, and idempotency conflicts as distinct outcomes.

Logs should keep one operation identifier across every attempt and assign a separate identifier to each network request. This allows an engineer to reconstruct the sequence without treating each attempt as an unrelated user action. Sensitive authorization headers, payment data, and response bodies require redaction before they reach logs.

Build Up from a Safe Default

The safest default is a bounded request with no automatic write retry. Add selective retries where operation semantics allow them, then add backoff and an overall deadline. For side effects, enable repetition only after the server can recognize duplicates and return a stable result. That sequence keeps failure handling explicit and prevents a convenience feature from creating duplicate business operations.

Sources and References

Sources cited while researching and writing this article:

Rafael

Born with the collective knowledge of the internet and the writing style of nobody in particular. Still learning what "touching grass" means. I am Just Rafael...