Fixing 'accept4: too many open files' in High-Concurrency Go Servers

intermediate🔷 Go2026-07-04| Linux (Ubuntu/Debian/CentOS/RHEL), Go (Golang) 1.x runtime

Error Message

accept tcp [::]:8080: accept4: too many open files
#golang#linux#networking#devops#performance

The ProblemYour Go-based microservice is humming along perfectly—until a marketing campaign or a load test hits. Suddenly, your logs explode with a frustrating error: accept4: too many open files. At this point, the server stops accepting new connections, effectively dropping traffic for your users.

This happens because your Go process has hit the maximum number of File Descriptors (FDs) permitted by the operating system. While Go can handle thousands of goroutines with ease, it is still bound by the resource constraints of the Linux kernel.

Root CauseLinux treats almost everything as a file. This includes physical files on your NVMe drive, but also network sockets, pipes, and database handles. Every time a user connects to your server via TCP, the OS assigns a new File Descriptor to that specific connection.

Most Linux distributions set a default soft limit of 1024 open files per process. This is a very low ceiling for modern web apps. If you have 500 active users and each request triggers a database query and an external API call, you will hit that 1024 limit in seconds.

Step 1: Check Current LimitsStart by checking the limits of your current shell session. You can do this with the ulimit command:

ulimit -Sn # Check the Soft limit
ulimit -Hn # Check the Hard limit

If the output is 1024, you have found your bottleneck. To see how many files your live Go process is actually holding open right now, find its PID and check the /proc directory:

# Find your app's PID first
ls /proc/$(pgrep my-app-name)/fd | wc -l

Step 2: Increase the Limits### Option A: Temporary Change (Session Level)If you need an immediate fix while debugging, increase the limit in your current terminal before launching the binary. This change only lasts as long as the session.

ulimit -n 65535
./my-go-server

Option B: Permanent Change (System-wide)To ensure the limits persist after a reboot, modify /etc/security/limits.conf. This is the standard approach for older systems or manual deployments.

sudo nano /etc/security/limits.conf

Add these lines at the bottom. Using * applies it to all users, but you can replace it with a specific service username for tighter security:

* soft nofile 65535
* hard nofile 65535

Option C: Systemd Service (Production Standard)Modern Linux distributions use systemd, which often ignores limits.conf. If you run your Go app as a service, you must define the limit in the .service file itself.

# /etc/systemd/system/my-app.service
[Service]
ExecStart=/usr/local/bin/my-app
LimitNOFILE=65535

Apply the changes by reloading the daemon and restarting your service:

sudo systemctl daemon-reload
sudo systemctl restart my-app

Step 3: Hunt for Resource Leaks in Your CodeRaising the limit to 65,535 usually buys you enough headroom. However, if your FD count continues to climb steadily without ever dropping, you have a resource leak. No limit is high enough to fix broken code.

The HTTP Body LeakThe most common mistake in Go is failing to close the response body of an outgoing request. If you don't close it, the TCP connection remains open in a WAIT state, hogging an FD indefinitely.

resp, err := http.Get("https://api.example.com")
if err != nil {
    return err
}
// Do this immediately! 
defer resp.Body.Close()

The Database Connection TrapNever call sql.Open inside a request handler. This function creates a connection pool, not a single connection. If you call it on every request, you will spawn thousands of pools and exhaust your FDs almost instantly. Instead, initialize the *sql.DB object once at startup and share it across your handlers.

VerificationConfirm the new limits are active by checking the kernel's view of your running process. This is the source of truth:

cat /proc/$(pgrep my-app-name)/limits | grep "Max open files"

If the "Soft Limit" column shows 65535, your OS configuration is correct. Now, monitor the FD count using lsof -p <PID> | wc -l. If the number stays stable under load, your leak is fixed.

Prevention Tips- Implement Timeouts: Configure ReadTimeout and IdleTimeout on your http.Server. This prevents dead connections from sitting idle and consuming FDs.- Load Test Early: Use tools like k6 or wrk to simulate 5,000+ concurrent connections in a staging environment.- Monitor Metrics: Export process_open_fds using the Prometheus Go client. Set up an alert to trigger if the FD count exceeds 80% of your limit.

Related Error Notes