IT & Engineering

Stress testing HTTP with Twisted Python and Treq

Being in the API business has its challenges and maintaining the robustness of the system during peak hours is one of them. That’s why we do lots of stress testing here at Mailgun.
Image for Stress testing HTTP with Twisted Python and Treq
July 16, 2019

Being in the API business has its challenges and maintaining the robustness of the system during peak hours is one of them. That’s why we do lots of stress testing here at Mailgun.

We have tried many different approaches over time, from simple Apache bench to more complicated custom testing suites. But this post is about a “quick and dirty” yet very flexible stress testing using Python.

When it comes to writing HTTP clients in Python we are fans of the Requests library. This is what we recommend to our API users. Requests is great, but it has one weakness: It’s a blocking one-call-per-thread affair: it’s hard or impossible to generate tens of thousands of requests quickly with it.

Introducing Treq on Twisted

To solve this problem we looked at Treq (Github repository). Treq is an HTTP client library inspired by Requests, but it runs on Twisted and it possesses the typical Twisted powers: it is asynchronous and highly concurrent when it comes to network I/O.

Treq is not specific to stress testing at all: it’s a great tool for writing highly concurrent HTTP clients in general, like web crawlers. Treq is elegant, simple to use and powerful. Here’s an example:

                            

                                 >>> from treq import get
    >>> def done(response):
    ... print response.code
    ... reactor.stop()
    >>> get("http://www.github.com").addCallback(done)
    >>> from twisted.internet import reactor
    >>> reactor.run() 200
                            
                        

The simple testing script

Below is a simple script which uses Treq to bombard a single URL with maximum possible number of requests.

                            

                                 #!/usr/bin/env python  
    from twisted.internet import epollreactor  
    epollreactor.install()
    from twisted.internet import reactor, task  
    from twisted.web.client import HTTPConnectionPool  
    import treq  
    import random  
    from datetime import datetime
    req_generated = 0  
    req_made = 0  
    req_done = 0
    cooperator = task.Cooperator()
    pool = HTTPConnectionPool(reactor)
    def counter():  
    '''This function gets called once a second and prints the progress at one 
    second intervals. 
    '''
    print("Requests: {} generated; {} made; {} done".format(
    req_generated, req_made, req_done))
    # reset the counters and reschedule ourselves
    req_generated = req_made = req_done = 0
    reactor.callLater(1, counter)
    def body_received(body):  
    global req_done
    req_done += 1
    def request_done(response):  
    global req_made
    deferred = treq.json_content(response)
    req_made += 1
    deferred.addCallback(body_received)
    deferred.addErrback(lambda x: None)  # ignore errors
    return deferred
    def request():  
    deferred = treq.post('http://api.host/v2/loadtest/messages',
         auth=('api', 'api-key'),
         data={'from': 'Loadtest <test@example.com>',
               'to': 'to@example.org',
               'subject': "test"},
         pool=pool)
    deferred.addCallback(request_done)
    return deferred
    def requests_generator():  
    global req_generated
    while True:
    deferred = request()
    req_generated += 1
    # do not yield deferred here so cooperator won't pause until
    # response is received
    yield None
    if __name__ == '__main__':  
    # make cooperator work on spawning requests
    cooperator.cooperate(requests_generator())
    # run the counter that will be reporting sending speed once a second
    reactor.callLater(1, counter)
    # run the reactor
    reactor.run()
                            
                        

The output:

                            

                                2013-04-25 09:30 Requests: 327 generated; 153 sent; 153 received 
2013-04-25 09:30 Requests: 306 generated; 156 sent; 156 received 
2013-04-25 09:30 Requests: 318 generated; 184 sent; 154 received
                            
                        

The “Generated” ones are the requests that have been prepared, but the Twisted reactor has not sent them yet. This script ignores all errors for simplicity, adding the stats for timeouts is left as an exercise for the reader.

The script can be used as a starting point and improved and extended with your own custom application-specific logic. One suggested improvement would be to use collections.Counter instead of the ugly globals. The script runs on a single thread, and to squeeze the maximum number of requests from a machine something like mulitprocessing can be used.

Happy stress testing!

Cheers,
Mailgunners