Free — no signup required

Mastering HTTP with `requests`

3 min read

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

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free