How to debug a file descriptor limit.

•

•

Too Many Open Files Means a Leak

I remember sitting in a darkened apartment in 2016, staring at a terminal window while my coffee went cold, watching a service slowly suffocate. Everything looked fine on the dashboard—CPU was low, memory was stable—but the application was throwing “connection refused” errors like it was being shot. I spent two hours chasing a ghost in the network layer before I realized the truth: we were hitting a hard ceiling on our open handles. Learning how to debug a file descriptor limit isn’t something you find in a glossy, high-level tutorial; it’s something you learn when your pager goes off and the standard metrics tell you absolutely nothing about why your sockets are dying.

I’m not going to waste your time with a lecture on what a file descriptor is or why Linux uses them. You probably already know that. Instead, I want to show you the actual commands I use to find the leak, how to distinguish between a legitimate traffic spike and a leaky connection pool, and how to fix it so you aren’t staring at your screen at 2 AM. This is just a record of what actually works when the system starts hitting the wall.

Table of Contents

Why Troubleshooting Too Many Open Files Error Matters

Why Troubleshooting Too Many Open Files Error Matters

It’s easy to treat a “Too many open files” error as a simple configuration tweak—just bump the limit and move on. But in my experience, that’s usually just masking a deeper rot. If you’re just increasing system open files limit every time a service hits a ceiling, you aren’t fixing the problem; you’re just giving a leaking bucket more room to hold water. Eventually, the ceiling moves, and the bucket still leaks.

The real danger is that these errors are rarely isolated. When a process hits its limit, it doesn’t just stop accepting new work; it often enters a zombie-like state where it’s technically “running” but functionally dead. I’ve seen entire microservice clusters cascade into failure because one service started identifying resource leaks in applications too late. By the time the monitoring alerts went off, the exhaustion had already cascaded through the network, making the root cause look like a massive spike in traffic rather than what it actually was: a single, poorly managed connection pool.

Ultimately, troubleshooting this matters because it forces you to look at how your software actually interacts with the OS. It’s about moving past the symptoms and understanding the lifecycle of your sockets and file handles. If you don’t get this right, you’re just waiting for the next 2 AM page to go off.

Identifying Resource Leaks in Applications Before They Strike

Identifying Resource Leaks in Applications Before They Strike

The problem with most “preventative” strategies is that they are reactive by design. You don’t realize you have a leak until the service starts throwing errors, but by then, the damage is already done. In my experience, the most effective way to avoid a middle-of-the-night outage is to stop looking at your application logs and start looking at the OS-level telemetry. I’ve learned that monitoring socket connections usage is a much more reliable early warning system than waiting for a 500 error to pop up in your dashboard. If you see a slow, linear climb in open descriptors that never quite returns to its baseline after a traffic dip, you aren’t looking at a spike; you’re looking at a leak.

Once you see that trend, you have to get surgical. I usually jump straight to the `lsof` command for process monitoring to see exactly what those descriptors are doing. It’s one thing to know you’re hitting the limit; it’s another to realize that 90% of your handles are stuck in a `CLOSE_WAIT` state because some junior dev forgot to close a client connection in a `finally` block. Identifying resource leaks in applications shouldn’t be a guessing game involving trial and error; it’s about mapping the file descriptors back to the specific code paths that are failing to clean up after themselves.

Five ways I stop chasing ghosts in the logs

  • Stop guessing and use `lsof -p [PID]`. I used to waste time checking network configs when the real culprit was just a rogue process holding onto thousands of stale log files. `lsof` tells you exactly what the file is, where it lives, and whether it’s a socket or a regular file.
  • Watch the `FD` column in `/proc/[PID]/fd`. If you see a massive list of files pointing to the same path or a specific library, you aren’t looking at “heavy load”—you’re looking at a leak. If that number keeps climbing linearly while your traffic stays flat, you’ve found your smoking gun.
  • Check your connection pool settings, not just your OS limits. I once spent a whole night bumping `ulimit -n` only to realize the application was just creating a new database connection for every single incoming request instead of reusing them. Increasing the limit just delayed the inevitable crash.
  • Use `netstat` or `ss` to differentiate between file handles and network sockets. In Linux, everything is a file. If your FD count is spiking, you need to know if you’re actually running out of disk handles or if your TCP connections are stuck in `CLOSE_WAIT` because the other end isn’t playing nice.
  • Audit your third-party client libraries. It’s rarely your own code that leaks; it’s usually the HTTP client or the cloud SDK you imported. If you aren’t explicitly calling `.close()` on a response body or a client instance, you’re essentially leaving the tap running until the system drowns.

What I’ve learned from the trenches

Don’t just bump the `ulimit`. Increasing the limit is a temporary bandage that often just delays the inevitable crash; you need to find the actual leak, otherwise you’re just building a bigger dam for a river that’s still flooding.

Monitoring is your only real defense. If you aren’t tracking the trend of open file descriptors over time in your dashboard, you aren’t going to see the slow climb toward the ceiling until the service is already dead.

Always check your third-party libraries. I’ve lost count of how many times the “leak” wasn’t in my code, but in a database driver or a logging utility that wasn’t properly closing its connections or file handles.

Avoiding the Next 2 AM Wake-up Call

Avoiding the Next 2 AM Wake-up Call.

At the end of the day, debugging a file descriptor crisis isn’t about memorizing every `lsof` flag; it’s about building a mental model of how your application actually handles resources. We’ve looked at how to track down the specific process hogging the handles, how to audit your connection pools, and why simply bumping the `ulimit` is usually just masking a leak rather than fixing it. If you can identify whether the culprit is a runaway log rotation, a database driver that isn’t closing sockets, or a misconfigured sidecar, you’ve already won half the battle. Don’t just patch the limit; find the leak and close it for good.

I’ve spent enough nights staring at terminal screens to know that these incidents feel personal when they happen. But if you treat every “Too many open files” error as a free lesson in how your system actually behaves under pressure, you stop being a victim of your infrastructure. My goal isn’t for you to become a Linux kernel expert, but to reach a point where you can look at a stack trace and predict the failure before the pager goes off. Write down what you found, automate the check, and hopefully, you’ll get to sleep through the night.

Frequently Asked Questions

How do I tell the difference between a legitimate spike in traffic and a slow, creeping resource leak?

The easiest way to tell is to look at the slope of the graph. A traffic spike is a mountain: it goes up fast, stays high, and then drops back down when the load subsides. A leak is a staircase or a slow ramp. If your file descriptor count keeps climbing even during the quietest hours of the night, you aren’t looking at a traffic problem; you’re looking at a leak that’s slowly drowning your process.

If I find the process leaking descriptors, how do I actually trace it back to the specific line of code or unclosed socket?

Once you’ve identified the PID, don’t just guess. I usually start with `lsof -p ` to see exactly what’s open. If it’s a mess of sockets, I use `strace -p -e trace=open,socket,close` to watch the system calls in real-time. If you see a pattern of `socket()` calls without a corresponding `close()`, you’ve found your leak. It’s tedious, but it beats hunting through thousands of lines of code blindly.

Is it safer to just bump the `ulimit` in the OS, or am I just delaying the inevitable crash?

Bumping the `ulimit` is like putting a larger bucket under a leaking pipe. It buys you time, but the water is still coming. If you have a genuine leak—say, a database connection pool that isn’t closing sockets—increasing the limit just delays the inevitable crash and makes the eventual failure harder to debug because the symptoms are masked. Increase it if you’re scaling for legitimate load, but if the number keeps climbing linearly, find the leak.

How do I monitor this in real-time so I'm not finding out about it from a PagerDuty alert at 2 AM?

I learned the hard way that waiting for the alert means you’ve already lost. If you’re using Prometheus, don’t just track the process-level limit; you need to export the actual count of open file descriptors via node_exporter. I set up a simple rate-of-change alert. I don’t care if we have 1,000 files open, but if that number is climbing linearly every ten minutes without plateauing, I want to know before the kernel starts killing connections.

Tomasz Wierzbicki

About Tomasz Wierzbicki

I am not interested in arguing about frameworks. I am interested in why the thing broke at two in the morning and what would have prevented it. Most of what I know came from being wrong in production and writing it down afterwards.