When threads wait so long for a connection that the user gives up.
A Connection Pool usually uses a Semaphore to limit concurrent database access. If the pool has 10 connections, and 15 threads want one, 5 threads must wait in a queue. If the first 10 threads run slow queries (or the database itself is lagging), the 5 waiting threads are "Starved." They sit in the queue doing nothing. Eventually, the HTTP request times out, the user sees a 502 Bad Gateway, and the thread crashes—meaning it waited for nothing.
To prevent infinite starvation, every professional connection pool implements a connection_timeout (how long a thread is willing to wait in the queue before giving up). If the pool is exhausted, it's better to fail immediately (Fail Fast) rather than have 10,000 threads pile up in RAM waiting for a locked database.
// Java HikariCP Example
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost/test");
// Pool Size: Max 10 connections
config.setMaximumPoolSize(10);
// Starvation Protection: If a thread waits > 5 seconds
// for a connection, throw an Exception and give up.
config.setConnectionTimeout(5000);
Tuning the pool size and timeout is an art. Too small a pool, and threads starve constantly. Too large a pool, and you overwhelm the database with context switching, making all queries slower (which ironicallly causes starvation!). A common rule of thumb for Postgres is: (Core Count * 2) + Effective Spindle Count.