Back to Scaling Python Applications guides

Getting Started with HTTPX: Python's Modern HTTP Client

Stanley Ulili
Updated on March 14, 2025

If you’ve worked with APIs in Python, you’re likely familiar with the requests library. But what if you could get the same ease of use, asynchronous support, HTTP/2 performance gains, and advanced configuration options? That’s HTTPX exactly what offers.

HTTPXis a powerful HTTP client that supports synchronous and asynchronous requests, making it an excellent choice for handling API interactions at any scale.

It provides built-in authentication, connection pooling, and streaming responses, making working with modern web services easier.

This guide will walk you through integrating HTTPX into your Python projects and maximizing its features to handle HTTP requests efficiently.

Prerequisites

Before diving in, install Python 3.13 or higher. While this guide assumes some familiarity with making HTTP requests in Python—especially with the requests library—you can still follow along even if you're new to it.

Getting started with HTTPX

For the best learning experience, create a new Python project to experiment with the concepts covered in this tutorial.

Start by setting up a new directory and initializing a virtual environment:

 
mkdir httpx-demo && cd httpx-demo
 
python3 -m venv venv

Activate the virtual environment:

 
source venv/bin/activate

Following that, install the latest version of httpx with the command below:

 
pip install httpx

Create a new client.py file in the root of your project directory and add the following code:

client.py
import httpx

def get_data():
    response = httpx.get('https://httpbin.org/get')
    return response.json()

if __name__ == "__main__":
    data = get_data()
    print(data)

This snippet imports the httpx package and defines a simple function that makes a GET request to a test endpoint.

Let's go ahead and run this script:

 
python client.py

You should observe output similar to the following:

Output
{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Host": "httpbin.org",
    "User-Agent": "python-httpx/0.28.1",
    "X-Amzn-Trace-Id": "Root=1-67d400f3-23aca99e43e19bf3142ec86e"
  },
  "origin": "105.234.160.4",
  "url": "https://httpbin.org/get"
}

The response from httpbin.org contains information about our request, including the sent headers. This simple example demonstrates the ease of making HTTP requests with HTTPX.

Understanding HTTPX response objects

When you make an HTTP request using httpx, the response object contains important information about the server’s reply. This includes the status code, headers, and response body, which help determine whether the request was successful and how to process the returned data.

Modify your client.py file to inspect different parts of the response:

 
import httpx

def get_data():
    response = httpx.get("https://httpbin.org/get")
print(f"Status Code: {response.status_code}")
print(f"Headers: {response.headers}")
print(f"Content: {response.text}")
return response
if __name__ == "__main__": get_data()

This script retrieves data from httpbin.org and displays important response details. Here’s what each part of the response object represents:

  • response.status_code – Indicates the HTTP status of the request (e.g., 200 for success, 404 for not found).
  • response.headers – A dictionary containing metadata about the response, such as content type, server information, and caching policies.
  • response.text – The full response body returned as a string.
  • response.json() – Parses the response body as JSON, converting it into a Python dictionary for easy data manipulation.

After running the script, you should see something like:

Output
Status Code: 200
Headers: Headers({'date': 'Fri, 14 Mar 2025 10:16:31 GMT', 'content-type': 'application/json', 'content-length': '304', 'connection': 'keep-alive', 'server': 'gunicorn/19.9.0', 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true'})
Content: {
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "python-httpx/0.28.1", 
    "X-Amzn-Trace-Id": "Root=1-67d401ff-6588f575574b55e6172d74ae"
  }, 
  "origin": "105.234.160.4", 
  "url": "https://httpbin.org/get"
}

<Response [200 OK]>

The response output consists of three key parts:

  • Status Code – Indicates whether the request was successful. A 200 status means success, while a 404 suggests that the requested resource was not found, and a 500 points to a server error.
  • Headers – Provide metadata about the response, including details like Content-Type, which specifies the format of the response, and Server, which identifies the web server handling the request.
  • Content – Contains the actual response body, typically a JSON object that includes valuable details about the request, such as the headers sent and the request's origin.

Now that you understand response objects, let's explore how to make different types of HTTP requests using HTTPX.

Understanding basic request methods

Now that you understand how HTTPX handles responses, let's explore different types of HTTP requests beyond GET.

HTTPX supports a variety of methods, including POST, PUT, DELETE, and query parameters with GET, allowing you to interact with APIs that require data submission or modifications.

A POST request is typically used to send data to a server, such as creating a new resource or submitting a form.

Remove all contents from client.py and replace it with the following code to send a POST request with JSON data:

client.py
import httpx

def create_task():
    url = "https://httpbin.org/post"
    payload = {"title": "Learn HTTPX", "completed": False}

    response = httpx.post(url, json=payload)

    print(f"Status Code: {response.status_code}")
    print(f"Response: {response.json()}")

    return response

if __name__ == "__main__":
    create_task()

Run the script:

 
python client.py

You should see output similar to this:

Output
Status Code: 200
Response: {
  "args": {},
  "data": "{\"title\":\"Learn HTTPX\",\"completed\":false}",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Content-Length": "41",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "User-Agent": "python-httpx/0.28.1",
    "X-Amzn-Trace-Id": "Root=1-67d403c3-7dff0dd26aa30e9c3f88df47"
  },
  "json": {
    "completed": false,
    "title": "Learn HTTPX"
  },
  "origin": "105.234.160.4",
  "url": "https://httpbin.org/post"
}

The response confirms that your request was processed successfully. The json field in the response body contains the data you sent, proving that the server received it correctly.

The headers also indicate that the request was sent as application/json, thanks to HTTPX automatically setting the correct content type.

While POST is used to create new resources, a PUT request is used to update existing ones. When you need to modify an entry rather than add a new one, PUT ensures that the existing data is replaced with the updated values.

Here’s an example of making a PUT request with HTTPX:

 
url = "https://httpbin.org/put"
payload = {"title": "Learn HTTPX", "completed": True}

response = httpx.put(url, json=payload)

This request updates an existing resource with new data, ensuring changes are correctly reflected on the server.

If you need to remove a resource instead, a DELETE request is used. This is particularly useful for deleting records from a database or removing items from an API:

 
url = "https://httpbin.org/delete"
response = httpx.delete(url)

Handling query parameters in a GET request

Sometimes, API requests need additional information in the URL, such as filtering or searching for specific data. This is done using query parameters.

Modify client.py to send a GET request with query parameters:

client.py
import httpx

def get_filtered_tasks():
    url = "https://httpbin.org/get"
    params = {"status": "completed"}

    response = httpx.get(url, params=params)

    print(f"Status Code: {response.status_code}")
    print(f"Response: {response.json()}")

    return response

if __name__ == "__main__":
    get_filtered_tasks()

Run the script:

 
python client.py

You should see output like:

Output

Status Code: 200
Response: {'args': {'status': 'completed'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-httpx/0.28.1', 'X-Amzn-Trace-Id': 'Root=1-67d406c3-6b4d928e394b3353355228af'}, 'origin': '105.234.160.4', 'url': 'https://httpbin.org/get?status=completed'}

The "args" field in the response confirms that the query parameter status=completed was sent successfully. This is useful when filtering or searching through API results.

Now that you've learned how to send different HTTP requests, the next step is to ensure your requests are resilient.

Handling timeouts and retries in HTTPX

Network issues, slow server responses, or connection failures can impact your application's performance. HTTPX provides built-in support for timeouts, retries, and error handling, which helps ensure your requests remain stable and reliable.

Setting a timeout for requests

By default, HTTPX waits indefinitely for a response, which can cause your program to hang if the server is slow or unresponsive.

You can prevent this by setting a timeout, which defines how long HTTPX should wait before giving up on a request.

Replace all the contents in the client.py file with the following to implement a timeout:

client.py
import httpx

def fetch_with_timeout():
    url = "https://httpbin.org/delay/5"  # Simulates a delayed response

    try:
        response = httpx.get(url, timeout=3)  # Timeout set to 3 seconds
        print(f"Status Code: {response.status_code}")
        print(f"Response: {response.json()}")
    except httpx.TimeoutException:
        print("Request timed out!")

if __name__ == "__main__":
    fetch_with_timeout()

If the server responds within 3 seconds, the request succeeds, and the response is printed. If the server takes longer than 3 seconds, HTTPX raises a TimeoutException, and the program prints "Request timed out!".

Run the script:

 
python client.py

If the request exceeds the timeout, you'll see:

Output
Request timed out!

This is useful for preventing unresponsive requests from blocking your application.

Retrying failed requests

Sometimes, network failures or temporary server issues cause requests to fail. HTTPX allows you to retry failed requests automatically using an httpx.Client with a custom retry strategy.

Remove all existing code in the client.py file and update it with the following to include retries:

client.py
import httpx
from httpx import RequestError

def fetch_with_retries():
    url = "https://httpbin.org/status/500"  # Simulates a server error

    with httpx.Client() as client:
        for attempt in range(3):  # Retry up to 3 times
            try:
                response = client.get(url)
                response.raise_for_status()  # Raise an error for bad responses (e.g., 500)
                print(f"Status Code: {response.status_code}")
                return response.json()
            except httpx.HTTPStatusError as e:
                print(f"Attempt {attempt + 1}: Received {e.response.status_code} - Retrying...")
            except RequestError:
                print("Network error occurred - Retrying...")

    print("All retry attempts failed.")

if __name__ == "__main__":
    fetch_with_retries()

The script attempts to send a request to an endpoint that returns a 500 error. If an error occurs, it retries up to 3 times before giving up.

The raise_for_status() method ensures we only process successful responses.

If all retry attempts fail, it prints "All retry attempts failed."

Run the script:

 
python client.py

Expected output:

 
Attempt 1: Received 500 - Retrying...
Attempt 2: Received 500 - Retrying...
Attempt 3: Received 500 - Retrying...
All retry attempts failed.

Implementing retries improves your application's resilience to temporary network issues.

Working with authentication in HTTPX

Now that you have learned how to handle timeouts and retries in HTTPX, the next step is securing API requests. Many APIs require authentication before allowing access to protected resources. HTTPX supports various authentication methods, such as Basic Auth, Token-based authentication, and OAuth.

Using basic authentication

Basic Authentication (Basic Auth) is one of the simplest forms of authentication, where the client sends a username and password encoded in the request headers.

Replace the contents of client.py with the following code to use Basic Authentication:

client.py
import httpx

def fetch_with_auth():
    url = "https://httpbin.org/basic-auth/user/pass"
    auth = ("user", "pass")  # Replace with your credentials

    response = httpx.get(url, auth=auth)

    print(f"Status Code: {response.status_code}")
    print(f"Response: {response.json()}")

if __name__ == "__main__":
    fetch_with_auth()

The auth argument accepts a tuple containing the username and password. HTTPX automatically encodes the credentials into the Authorization header as Basic base64(username:password).

Then the server verifies the credentials and responds accordingly.

Run the script with the following:

 
python client.py

Expected output:

Output
Status Code: 200
Response: {'authenticated': True, 'user': 'user'}

This confirms that the authentication was successful. If the credentials are incorrect, the server will return a 401 Unauthorized response.

Using Bearer Token authentication

Many APIs use token-based authentication (e.g., API keys, JWT tokens) instead of Basic Auth. Tokens are typically passed in the Authorization header.

Modify client.py to send an API request with a Bearer Token:

client.py
import httpx

def fetch_with_auth():
url = "https://httpbin.org/bearer"
headers = {
"Authorization": "Bearer your_api_token_here" # Replace with your token
}
response = httpx.get(url, headers=headers)
print(f"Status Code: {response.status_code}") print(f"Response: {response.json()}") if __name__ == "__main__": fetch_with_auth()

The Authorization header is manually set to "Bearer <token>". The server verifies the token and responds accordingly.

Run the script like you have been doing:

 
python client.py

Expected output:

Output
Status Code: 200
Response: {'authenticated': True, 'token': 'your_api_token_here'}

If the token is missing or invalid, you'll receive a 403 Forbidden or 401 Unauthorized response.

Asynchronous requests with HTTPX

So far, you've been making synchronous HTTP requests, meaning each request blocks the program until it responds. This is fine for small-scale applications, but asynchronous programming can significantly improve performance when dealing with multiple API calls or high-latency networks.

HTTPX provides native async support using Python’s asyncio and async/await syntax, allowing requests to be made concurrently instead of sequentially.

Asynchronous requests are beneficial when:

  • You need to make multiple API calls concurrently (e.g., fetching data from multiple endpoints).
  • Your application handles high-latency network requests.
  • You want to improve efficiency without blocking execution.

Instead of waiting for each request to finish before making the next one, async HTTPX allows multiple requests to be sent simultaneously, significantly reducing wait times.

Making an asynchronous GET request

Replace the contents of client.py with the following to send an asynchronous GET request:

client.py
import httpx
import asyncio

async def fetch_data():
    url = "https://httpbin.org/get"

    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        print(f"Status Code: {response.status_code}")
        print(f"Response: {response.json()}")

if __name__ == "__main__":
    asyncio.run(fetch_data())

The fetch_data() function is an asynchronous coroutine that makes a non-blocking GET request.

The async with httpx.AsyncClient() as client statement ensures proper session management, while await client.get(url) sends the request without pausing execution.

Finally, asyncio.run(fetch_data()) starts the event loop, allowing multiple API calls to run concurrently for improved performance.

Run the script:

 
python client.py

Expected output:

Output
Status Code: 200
Response: {'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-httpx/0.28.1', 'X-Amzn-Trace-Id': 'Root=1-67d40ee1-24d1e7e30a55bf8f49edd768'}, 'origin': '105.234.160.4', 'url': 'https://httpbin.org/get'}

The output confirms a successful async request with Status Code: 200, displaying request metadata, source IP, and the requested URL—showcasing non-blocking execution.

Making multiple asynchronous requests

One of the most significant advantages of async requests is that they allow multiple API calls to be made concurrently. Instead of sending them individually, all requests are sent simultaneously, reducing total execution time.

Modify client.py to send multiple GET requests concurrently:

client.py
import httpx
import asyncio
import time


async def fetch_url(url, client):
    response = await client.get(url)
    return url, response.status_code


async def main():
    urls = [
        "https://httpbin.org/get",
        "https://httpbin.org/delay/1",
        "https://httpbin.org/delay/2",
        "https://httpbin.org/delay/3",
    ]

    start_time = time.time()

    # Using an async client as a context manager
    async with httpx.AsyncClient() as client:
        # Create a list of tasks
        tasks = [fetch_url(url, client) for url in urls]

        # Gather all tasks (execute them concurrently)
        results = await asyncio.gather(*tasks)

        # Print results
        for url, status_code in results:
            print(f"URL: {url}, Status: {status_code}")

    end_time = time.time()
    print(f"Total time: {end_time - start_time:.2f} seconds")


if __name__ == "__main__":
    asyncio.run(main())

This program defines fetch_url(), an asynchronous function that requests a GET to a given URL and returns its response status. The main() function builds a list of URLs and sends all requests concurrently using asyncio.gather(*tasks), ensuring they run in parallel.

An AsyncClient is used within a context manager to manage connections efficiently. By executing all requests simultaneously, the program avoids delays in sequential execution.

Rerun the program and you will see output like this:

Output
URL: https://httpbin.org/get, Status: 200
URL: https://httpbin.org/delay/1, Status: 200
URL: https://httpbin.org/delay/2, Status: 200
URL: https://httpbin.org/delay/3, Status: 200
Total time: 5.99 seconds

In a synchronous approach, each request would complete before the next one starts, leading to a total execution time roughly equal to the sum of all delays.

However, with async execution, all requests run simultaneously, drastically reducing the overall wait time.

This method is handy when working with APIs involving network latency or when handling high requests efficiently.

Leveraging HTTP/2 with HTTPX

HTTP/2 is a major iteration of the HTTP protocol that provides a far more efficient transport layer with significant performance benefits. Unlike HTTP/1.1's text-based format, HTTP/2 uses a binary format that enables:

  • Multiplexing: Multiple requests and responses can share a single TCP connection simultaneously
  • Header compression: Reduces overhead by efficiently compressing HTTP headers
  • Stream prioritization: Allows clients to indicate which resources are more important
  • Server push: Enables servers to proactively send resources to clients before they're explicitly requested

These improvements can dramatically enhance performance, especially for applications making multiple concurrent requests.

HTTPX doesn't enable HTTP/2 by default, as HTTP/1.1 is considered the more mature and battle-tested option. To use HTTP/2, you first need to install the required dependencies:

 
pip install 'httpx[http2]'

Once installed, you can enable HTTP/2 by setting the http2 parameter to True when creating a client:

client.py
import httpx

def use_http2():
    # Create a client with HTTP/2 support enabled
    with httpx.Client(http2=True) as client:
        # Make a request
        response = client.get("https://nghttp2.org/httpbin/get")

        # Check which HTTP version was used
        print(f"HTTP Version: {response.http_version}")
        print(f"Status Code: {response.status_code}")
        print(f"Response: {response.json()}")

if __name__ == "__main__":
    use_http2()

When you run this script, you should see output like this:

 
HTTP Version: HTTP/2
Status Code: 200
Response: {'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'nghttp2.org', 'User-Agent': 'python-httpx/0.28.1'}, 'origin': '105.234.160.4', 'url': 'https://nghttp2.org/httpbin/get'}

It's important to note that enabling HTTP/2 in your HTTPX client does not guarantee that your requests will use HTTP/2. Both the client and server must support HTTP/2 for it to be used.

If you connect to a server that only supports HTTP/1.1, the client will automatically fall back to using HTTP/1.1.

You can always check which version of the HTTP protocol was actually used by examining the .http_version property on the response:

client.py
import httpx

def check_http_version(url):
    with httpx.Client(http2=True) as client:
        response = client.get(url)
        print(f"URL: {url}")
        print(f"HTTP Version: {response.http_version}")
        print(f"Status Code: {response.status_code}")
        print()

if __name__ == "__main__":
    # Check various websites for HTTP/2 support
    urls = [
        "https://nghttp2.org/httpbin/get",  # Supports HTTP/2
        "https://httpbin.org/get",          # May or may not support HTTP/2
        "http://example.org/"               # Likely HTTP/1.1 (note: using HTTP, not HTTPS)
    ]

    for url in urls:
        check_http_version(url)

Running this script will show which HTTP version was used for each request:

 
URL: https://nghttp2.org/httpbin/get
HTTP Version: HTTP/2
Status Code: 200

URL: https://httpbin.org/get
HTTP Version: HTTP/2
Status Code: 200

URL: http://example.org/
HTTP Version: HTTP/1.1
Status Code: 200

This demonstrates how HTTPX automatically selects the appropriate protocol based on server support.

Final thoughts

This guide covered HTTPX's key features, from basic requests to advanced capabilities like authentication, async operations, and HTTP/2 support. HTTPX bridges the gap between the familiar requests library API and modern Python development needs.

Its intuitive interface and powerful features make HTTPX suitable for everything from simple scripts to complex service architectures.

For more details, refer to the official HTTPX documentation. Happy coding!

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Get Started with Job Scheduling in Python
Learn how to create and monitor Python scheduled tasks in a production environment
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github