Skip to content
gohacks.dev
Databases

Fix Too Many Connections Errors From a Go Service Fast

Fix PostgreSQL sorry too many clients already and MySQL Error 1040 in a Go service: reproduce them, read the cause, cap MaxOpenConns and add up the instances.

6 min read
Summarize in AI
Flat illustration of a log with a failing line and a wrench.

The message appears in logs at the worst time, during a traffic spike or a deploy, and it reads like a database problem:

Tested with Go 1.25.6, pgx v5.11.0, go-sql-driver/mysql v1.9.3, PostgreSQL 18.6 and MySQL 8.4.11 in Docker, macOS on Apple Silicon (arm64)

Text
FATAL: sorry, too many clients already (SQLSTATE 53300)
Error 1040: Too many connections

The server is refusing new connections because it has reached its limit. The cause is nearly always on the client side: too many connections requested, or connections held too long. This article reproduces both errors on Go 1.25.6, shows the numbers, and walks through the fix in the order that matters.

Reproduce it

main.goGo
package main

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"sync"
	"sync/atomic"

	"github.com/go-sql-driver/mysql"
	_ "github.com/jackc/pgx/v5/stdlib"
)

func hammer(driver, dsn, sleepSQL string, n int) (ok, failed int64, first string) {
	var wg sync.WaitGroup
	var o, f atomic.Int64
	var once sync.Once
	// No SetMaxOpenConns: the pool is allowed to open a connection per concurrent query.
	db, _ := sql.Open(driver, dsn)
	defer db.Close()
	start := make(chan struct{})
	for range n {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			var x int
			if err := db.QueryRowContext(context.Background(), sleepSQL).Scan(&x); err != nil {
				f.Add(1)
				once.Do(func() { first = err.Error() })
				return
			}
			o.Add(1)
		}()
	}
	close(start)
	wg.Wait()
	return o.Load(), f.Load(), first
}

func main() {
	// PostgreSQL: max_connections is 100 by default, with a few reserved.
	ok, failed, first := hammer("pgx", "postgres://postgres:[email protected]:55432/lab?sslmode=disable", `select 1 from pg_sleep(1)`, 130)
	fmt.Printf("postgres: ok=%d failed=%d\n", ok, failed)
	fmt.Printf("postgres first error ends with: %s\n", first[max(0, len(first)-58):])

	// MySQL: lower the limit so the demonstration is small.
	admin, _ := sql.Open("mysql", "root:secret@tcp(127.0.0.1:33099)/lab")
	defer admin.Close()
	_, _ = admin.Exec(`set global max_connections = 12`)
	ok, failed, first = hammer("mysql", "root:secret@tcp(127.0.0.1:33099)/lab", `select sleep(1)`, 30)
	_, _ = admin.Exec(`set global max_connections = 151`)
	fmt.Printf("mysql: ok>0=%v failed>0=%v\n", ok > 0, failed > 0)
	var me *mysql.MySQLError
	fmt.Printf("mysql first error: %s\n", first[:min(len(first), 60)])
	_ = me
	_ = strings.TrimSpace
}
Shell
go run . 2>&1
Text
postgres: ok=100 failed=30
postgres first error ends with: r: FATAL: sorry, too many clients already (SQLSTATE 53300)
mysql: ok>0=true failed>0=true
mysql first error: Error 1040: Too many connections

The program fires a burst of queries at each database at once and counts what happens. Each query sleeps for a second on the server, so a connection is held for that long. The pool is a plain sql.DB with no SetMaxOpenConns.

PostgreSQL. 130 goroutines sent a one-second query together. The pool opened a connection for every one, because with no limit, a goroutine that finds no idle connection makes a new one. PostgreSQL's default max_connections is 100, and the output shows ok=100 failed=30. The failures end with FATAL: sorry, too many clients already (SQLSTATE 53300). The ordinary clients stop at 97 slots and the last three are reserved for superusers, so as the superuser this lab reached exactly 100; an application role would hit the wall a few connections earlier.

MySQL. The server's max_connections defaults to 151, so the lab lowered it to 12 with set global max_connections = 12 to keep the burst small. Thirty concurrent queries then got some successes and some refusals, and the first failure was Error 1040: Too many connections. The limit is restored afterwards.

The mechanism is the same on both. The pool is not at fault: asked to run 130 queries concurrently with no limit, it did exactly that.

Read the situation first

Before changing anything, find out who owns the connections. On PostgreSQL:

SQL
select usename, application_name, state, count(*)
from pg_stat_activity group by 1, 2, 3 order by 4 desc;

show max_connections;

On MySQL:

SQL
show processlist;
show variables like 'max_connections';
show status like 'Threads_connected';

Three patterns come up.

  • Many idle connections from one application. Pools are too large or too many instances. A pool holding idle connections does no harm until the sum passes the limit.
  • Many idle in transaction. Code opened a transaction and did not finish it. This is a bug, usually an early return without Rollback or a transaction spanning an external call.
  • Many active running the same query. Slow queries are holding connections, and the pool needs more connections to keep up. Raising the limit makes it worse; fix the query or its index.

Fix it, in this order

1. Bound every pool

Set SetMaxOpenConns in every service and every binary that touches the database, including workers, cron jobs and migration tools:

Go
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(20)
db.SetConnMaxLifetime(30 * time.Minute)

With a limit, the 130th goroutine waits for a connection instead of opening one. The cost is waiting; the benefit is that the database stays up. Choose the number in the pool settings article.

2. Do the multiplication

The limit that matters is the total: instances times pool size, summed over every service, plus admin tools and monitoring. Ten replicas at MaxOpenConns=20 are 200 connections. A rolling deploy briefly runs old and new instances together, so the peak can be double. Write the budget down: max_connections minus a reserve for operators, divided across services. If it does not fit, reduce pool sizes or add a pooler.

3. Find the leaks

A connection held forever is a leak. In Go, the usual sources are a Rows that is never closed, a Tx that is neither committed nor rolled back, and a Conn that is not closed. Check db.Stats().InUse: if it stays at the limit while traffic is quiet, something holds connections. The rows article shows the leak and the fix, and the transaction article shows the defer Rollback pattern that closes the other main hole.

4. Set timeouts so stuck work releases connections

A query with no deadline holds its connection for as long as the database takes. Use QueryContext with a deadline, and a server-side statement_timeout on PostgreSQL or max_execution_time on MySQL as the backstop. See query timeouts.

5. Put a pooler in front for many clients

When the client count is large, many small services or serverless functions, bounding each pool is not enough. A connection pooler multiplexes thousands of client connections onto a few server connections. PgBouncer is the common choice for PostgreSQL, and ProxySQL for MySQL; cloud providers offer managed proxies. In transaction pooling mode, session features such as SET, advisory locks and server-side prepared statements across calls do not work, and the driver needs the simple protocol; see PostgreSQL connections.

6. Raise the server limit last

Raising max_connections is the tempting fix and the last one to try. Each PostgreSQL connection is a process with its own memory, and a high limit invites a thundering herd that makes the whole server slower. If you do raise it, raise it with the memory budget in mind, and keep client pools bounded.

Check that the fix works

Re-run the burst against your service with the cap in place. The expected result changes: no 53300 or 1040 errors, and instead a rise in WaitCount and some latency, which the context deadline turns into a clean error if the wait is too long. That trade, a slower request instead of a failed server, is the point of the cap. Add an alert on pg_stat_activity connection count above 80 percent of max_connections, and on the pool's WaitCount rate.

What the error does not tell you

The message names the symptom, not the client. In a shared database, the service that hits the limit may not be the one that caused it. Look at application_name (set it in the DSN: application_name=orders-api) and at the user to see who holds the connections. Set it in every service so that the next incident takes minutes.

The server settings are documented in the PostgreSQL connection settings chapter and the MySQL manual's page on too many connections.

Taggederror fix
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.