Hi,
we have multiple projects/setups running Django+Wagtail (5.2/7.0) with gunicorn+uvicorn (latest versions, no threading) as ASGI applications with django-channels. We use pgbouncer and in Django we use CONN_MAX_AGE = 0. We are running this setups for about 4 years now and we didn’t have any issues with connection limits.
In the last weeks, I assume due to package updates (we’ve upgrade Django from 4.2 to 5.2), the setups started raising FATAL: no more connections allowed (max_client_conn) (so we’re hitting the relative high limit of pgbouncers client connections), after bots crawled for many unknown URLs concurrently. This is not unusual and never resulted in connection issues in the past.
One of those setups is very small: the instance has 2 CPUs, python3.12, it runs a gunicorn with 2 workers, no threading, 1 AsyncHttpConsumer without auth or database access (also it was never hit by the crawlers), and the rest is a typical Django/Wagtail site with synchronous middleware and views. pgbouncer is configured with max_client_conn = 100.
Even this simple and small setup got the no more connections allowed (max_client_conn) after being crawled.
Now, I’m trying to understand why this is possible.
My understanding of the ASGI-setup is, that the threads created by asgiref(?) to handle requests are limited by the default thread pool being used (which could be manually sized with ASGI_THREADS according to the channels-docs).
So in my understanding, if there are 2 CPUs, the default threadpool should have a size of 7 (2 CPUs + 5) and 2 workers/processes should therefore together hold a maximum of 14 connections, since due to CONN_MAX_AGE = 0 the connections are closed after every request.
Since the application is hitting the 100 connections, I probably miss something here, or there is an issue with database connections not being closed.
For me it looks like the request handling is somewhere leaking those connections, or it takes longer to close this connections while other threads are demanding new connections…
We actually wanted to switch to Django’s native connection pooling for performance, but as long as I don’t understand the setup correctly, this switch would probably make it worse, as with native pooling and therefore direct connections to the db, we would have to use smaller connection pools (since the db is shared with other projects), which would get exhausted even faster.
Can you help me?
- Is my understanding of the ASGI-setup wrong? Is this normal behavior and those threads used for requests and therefore the used connections are actually never limited?
- Is there are way to determine the maximum number of used threads?
- Can we limit those threads and therefore the maximum used connections?