Talking to APIs Robustly
Almost everything in the cloud is controlled via HTTP APIs — AWS, GCP, Azure, PagerDuty, Datadog, GitHub, Vault. The requests library is the de-facto standard for making HTTP calls in Python. While a simple requests.get(url) works for quick tests, production ops scripts need three additional capabilities: session reuse, timeouts, and automatic retries.
1. Session Objects — Efficiency and Shared State
Every time you call requests.get() directly, Python opens a new TCP connection, completes the TLS handshake, makes the request, and closes the connection. For a single call, this is fine. For a script making 50 API calls to the same host, you are paying the connection overhead 50 times.
A requests.Session() object maintains a connection pool and reuses existing connections (HTTP keep-alive). It also lets you set headers, authentication, and other parameters once, and they apply to every request made through that session.
import requests
session = requests.Session()
# Set auth headers once — applies to all requests through this session
session.headers.update({
"Authorization": "Bearer my-api-token",
"Content-Type": "application/json"
})
# Both calls reuse the same TCP connection
response1 = session.get("https://api.example.com/servers")
response2 = session.get("https://api.example.com/databases")
2. Timeouts — Never Hang Forever
By default, requests will wait indefinitely for a server to respond. If an external API goes down or a network route becomes black-holed, your automation script will hang forever, blocking deployments or alerting pipelines.
Always set a timeout. The timeout parameter accepts either a single number (total time) or a tuple of (connect_timeout, read_timeout):
# Wait max 3 seconds to establish connection, max 10 seconds for data
response = session.get("https://api.example.com/data", timeout=(3, 10))
3. Retries — Handling Transient Failures
Networks are unreliable. APIs temporarily return 503s during deployments. A robust script should not crash on the first transient error — it should retry with exponential backoff.
The urllib3 library (which requests uses internally) provides a Retry object that you attach to a session via an HTTPAdapter:
from requests.adapters import HTTPAdapter, Retry
retry_strategy = Retry(
total=3, # Maximum number of retries
backoff_factor=1, # Wait 1s, then 2s, then 4s between retries
status_forcelist=[429, 500, 502, 503, 504], # Retry on these HTTP codes
allowed_methods=["GET", "POST"] # Which HTTP methods to retry
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.mount("https://", adapter)
session.mount("http://", adapter)
With this configuration, any GET or POST that receives a 503 response will be automatically retried up to 3 times, with increasing delays between attempts. Your calling code doesn't need to know this is happening — it just sees either a successful response or a final RetryError exception.
Always check the response status: A 200 OK means the request succeeded. A 4xx or 5xx means something went wrong. Use response.raise_for_status() to automatically raise an exception on error codes, rather than silently processing an error response as if it were data.
response = session.get("https://api.example.com/instances", timeout=(3, 10))
response.raise_for_status() # Raises HTTPError if status >= 400
data = response.json() # Parse JSON response body