FastAPI server becoming unresponsive when sending Twilio message

169 Views Asked by At

I have a FastAPI app (obvs) that has an endpoint which uses Twilio's Client to send a message sync. This has not worked. I made a bare bones python file that just constructs the client and creates the message and it works fine so I do not suspect it is twilio itself. When making a call using the twilio client the server hangs/freezes. It never times out. If I make a file change during this period the reloader freezes as well (I'm assuming since the server has become non-responsive). This happens regardless if I am using a sync or async path def for this route. Other async and sync routes seem to work fine (I haven't gotten around to testing them all yet).

fastapi==0.104.1
twilio==8.2.0
uvicorn==0.23.2
starlette==0.27.0

I am running the app locally like so (I've also called uvicorn directly from the command line):

if __name__ == '__main__':
    uvicorn.run('app:app', reload=True, port=5002)

I have a router in a separate file and call app.include_router(<the_router>) in a builder function for the app. Here's the twilio client (we have our own lil wrapper):

from twilio.rest import Client
...get env variables

class TwilioAPI
    def __init__(self, phone_number: str):
        self.client = Client(account_sid, auth_token)
        self.phone_number = phone_number

    def send_sms(self, body: str):
        # we enter the function, but this never returns/resolves
        message = self.client.messages.create(
            messaging_service_sid=messaging_service_sid,
            body=body,
            to=self.phone_number,
        )
        return message.sid

The route in question looks like this:

@router.post("/endpoint")
def send_message_or_whatever(input: Input):
    ...get data from input, construct message
    ...we hit our database here and this works
    twilio_api_client = CreateAnInstanceOfOurTwilioClient()
    twilio_api_client.send_sms(message) <--- this is where it goes sideways
    return stuff

All the examples I have found on twilio's own blog do something like

@router.post('/endpoint')
async def do_something():
    client = twilio.rest.Client() # synchronous client
    client.messages.create(...create message params)

Stuff I have tried:

using async and sync path definitions. Even though we are "waiting" on twilio in a sync function it shouldn't really matter? We wait for the db at other points which is a network call with no issue. Right now I don't even care if its not the most optimal thing for performance. The client doesn't return a future that must be awaited.

when using async I have tried to use await asyncio.get_event_loop().run_in_executor(...) to no avail, nothing happens

I tried to use fastapi's background task. It still gets stuck at client.messages.create (I am guessing this is a wrapper around asyncio.to_thread or run_in_executor)

What the hell am I doing wrong?

PS: I am aware of the twilio client's AsyncHTTPClient, but I am primarily interested in understand why this doesn't work

1

There are 1 best solutions below

5
On

The synchronous call is blocking the event loop. That means nothing else can occur while the blocking call is occurring. That's why you see that behavior. Running this in a thread or threadpool executor will be problematic because Twilio uses requests sessions (connection pooling) by default, which are not thread-safe.

Twilio has also made note that their client should not be considered thread-safe.

Thread safety issues can lead to problems like deadlocks. You might avoid this problem by creating your client as a threadlocal variable in the thread in which it's used, instead of reusing the same client potentially across threads.

You might also be able to turn off use of sessions/connection pooling to solve the issue (YMMV, other thread safety issues may still exist):

from twilio.http.http_client import TwilioHttpClient

client = Client(...,
                http_client=TwilioHttpClient(pool_connections=False)
)

Ultimately, your best bet would just be to use the async client.