Controlling ulimit and Resource Limits in Bash Scripts
AI generated
$_
#!/
Bash · Linux · Resource Limits
ulimit and Resource Limits in Bash
Controlling soft and hard limits deliberately for deployment scripts

ulimit controls how many files, processes, and how much virtual memory a process may use, through a clear interplay of soft and hard limits. Knowing the typical cause of too-many-open-files errors in deployment scripts means setting limits deliberately, either scoped to a script's runtime or permanently via systemd, instead of discovering the failure in production.

17 min read ulimit -n · -u · -v soft/hard limits · systemd

1. What ulimit is: soft limits, hard limits, and how they interact

ulimit is a Bash built-in that reads and sets the per-process resource limits enforced by the kernel, such as the maximum number of open files or the maximum process count for a user. Every one of these limits always exists in Bash as a pair: a soft limit, the currently enforced value, and a hard limit, marking the absolute ceiling up to which an unprivileged process is allowed to raise its own soft limit.

A process with no special privileges can lower its own soft limit at any time and raise it again up to the hard limit, but never beyond it, and the hard limit itself can only be lowered, never raised, without root privileges. That interplay explains why a deployment script attempting to set ulimit -n 100000 fails with an error if the system-wide hard limit is lower, even when the script runs with full root privileges, as long as no system-wide limit has been raised.

2. ulimit -n: open file descriptors and the cause of too many open files

ulimit -n caps the number of file descriptors a single process may hold open at once, where every opened file, socket, and pipe each consumes one descriptor. Once a process exceeds that limit, the kernel refuses any further open() or socket() call with the EMFILE error, which typically shows up in application logs as "Too many open files".

In deployment scripts, this error hits web servers or database processes especially often, since they hold many concurrent connections open and run into a soft limit that is too low, frequently inherited from an outdated distribution default of 1024. The short-term fix inside the script itself is ulimit -n 65536 before actually starting the service, provided the hard limit allows that value; otherwise the hard limit must first be raised system-wide.


# Check current limits before starting the service
echo "Soft limit (open files): $(ulimit -Sn)"
echo "Hard limit (open files): $(ulimit -Hn)"

# Raise the soft limit up to the hard limit for this process
ulimit -n 65536 || {
  echo "Could not raise file descriptor limit" >&2
  exit 1
}

exec ./my-webserver

3. ulimit -u: capping the maximum number of processes per user

ulimit -u determines how many processes and threads a user may start simultaneously, and on Linux the kernel counts threads as separate entries, which makes this limit hit far sooner in heavily multithreaded applications than the plain process name might suggest. Once the limit is reached, every further fork() call fails, and a deployment script can no longer start any additional background processes itself.

This limit is especially treacherous when set independently for a service account that launches several parallel worker processes: an overly low ulimit -u does not cause an immediate, clear failure during deployment itself, but rather a seemingly random failure of individual workers once the account's total running thread count crosses the limit, often only visible under production load.


echo "Current process limit: $(ulimit -u)"

if (( $(ulimit -u) < 4096 )); then
  echo "WARNING: process limit below 4096, workers may fail" >&2
fi

4. ulimit -v: capping virtual memory per process

ulimit -v caps the maximum reservable virtual memory of a process in kilobytes, not the physical memory actually used. Because modern applications, especially those on a Java or Node.js runtime, frequently reserve far more virtual address space than they actually use physically, an overly tight ulimit -v often crashes the application right at startup, long before any real memory pressure appears.

For deployment scripts that want to deliberately protect a process from excessive memory consumption, ulimit -v is therefore usually the wrong tool, since it caps virtual rather than physical memory. A more precise tool for that goal is a cgroup-based memory limit through systemd, while ulimit -v in practice remains mainly useful where a deliberately tight sandbox for small, known scripts is desired.


# Deliberately restrictive sandbox for a small trusted script
( ulimit -v 262144  # 256 MB virtual memory
  exec ./small-batch-job.sh )

5. Setting limits only for a script's runtime: the subshell pattern

ulimit calls inside a running Bash script default to affecting the current process and get inherited by every subsequent child process, but stay entirely scoped to the current shell session and vanish once the script ends. Anyone wanting to change a limit for just a single step of a larger script should deliberately wrap that step in a subshell using parentheses.

Limits set inside the subshell apply exclusively to that subshell and the child processes it launches; the surrounding script and every step that follows keep their original limits unchanged. This pattern is especially useful when a deployment script wants to deliberately cap one potentially memory-hungry build step, without affecting limits for the rest of the deployment pipeline.


echo "Before the subshell: $(ulimit -v)"

(
  ulimit -v 1048576   # only inside this subshell: 1 GB
  ./memory-intensive-build.sh
)

echo "After the subshell: $(ulimit -v)"   # unchanged

6. Setting limits permanently: limits.conf versus systemd unit files

A limit set with ulimit inside a script only ever applies to the current process tree instance and is lost once the script ends. For permanent, system-wide, or user-specific limits meant to apply automatically at every new login or service start, /etc/security/limits.conf is the classic place from which PAM-based logins pull their limits, configurable per user, group, or with a wildcard entry for everyone.

For services started through systemd, however, limits.conf frequently does not apply, since systemd services bypass PAM login sessions. The more reliable approach here is setting LimitNOFILE, LimitNPROC, or LimitAS directly in the service's systemd unit file, because systemd enforces these directives itself when starting the service, independently of limits.conf, and thus applies consistently on every restart, regardless of who triggers the service.

7. Typical problems in deployment scripts caused by too-low limits

The most common symptom of a too-low limit in production deployment scripts is a service that starts fine locally during development but crashes sporadically in production with "Too many open files" or "Resource temporarily unavailable", because the production environment ships a more restrictive default configuration than the local development machine, which often runs on more generous distribution defaults.

A second common problem arises when a deployment script raises limits via ulimit in its own process, but the actual service starts through systemd or a process manager that builds its own, independent environment with its own limits. The ulimit values set in the deployment script then simply get ignored, since they only applied to the short-lived deployment process itself, not to the actually long-lived service process.

8. Checking and diagnosing limits: ulimit -a and /proc/[pid]/limits

ulimit -a prints a full overview of every limit currently in effect for the running shell, handy for a quick manual check while developing a deployment script. For diagnosing an already-running, unrelated process, /proc/[pid]/limits is the right source instead, since it reports both soft and hard limits separately for every resource, regardless of which shell, or no shell at all, originally started the process.

A diagnostic script that needs to verify whether an already-running production service actually started with the expected limits should therefore read /proc/[pid]/limits rather than relying on the limits of its own diagnosing shell, which typically has entirely different values than the monitored target service.


# Full overview in the current shell
ulimit -a

# Limits of a specific running process
pid=$(pgrep -f "my-webserver" | head -n1)
grep -E "open files|processes" "/proc/$pid/limits"

9. Best practices for production deployment scripts

A clean deployment script checks required limits explicitly before actually starting the service and aborts with a clear error message, instead of blindly starting the service with insufficient limits and only letting the failure surface in production. That check should always account for both the soft and the hard limit, since only the hard limit reliably shows how far the soft limit can even be raised without extra privileges.

For long-lived, permanently running services, limits belong consistently in the systemd unit file rather than in a one-time deployment script, because systemd reliably re-enforces those limits on every service restart, regardless of when or how the deployment script last ran. ulimit inside the script therefore remains the right tool mainly for short-lived build and batch steps.

Limit Flag Typical problem Recommendation
Open files ulimit -n Too many open files with many connections Check before service start, set LimitNOFILE in systemd
Processes/threads ulimit -u fork() fails with many workers LimitNPROC in systemd, not just in the deployment script
Virtual memory ulimit -v Crash on Java/Node despite enough RAM cgroup limits instead of ulimit -v for physical memory
Runtime scope only Subshell ( ) Global limits accidentally changed Always wrap critical steps in a subshell

Mironsoft

Shell automation, DevOps tooling and deployment infrastructure

Shell scripts that hold up in production?

We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.

Code Review

ShellCheck analysis and manual review for critical Bash pattern violations.

Refactoring

Retrofitting error handling, logging and safe file operations.

CI Integration

Wiring ShellCheck and BATS into pipelines and building regression tests.

10. Summary

ulimit in Bash: The Essentials at a Glance

Soft vs. hard

Soft limit is the currently enforced value, hard limit the ceiling up to which a process may raise it itself.

ulimit -n

The most common source of failure in deployment scripts; the default of 1024 is not enough for many concurrent connections.

Subshell for scope

Set a limit only for one step without affecting the rest of the script, wrapped in parentheses.

Permanent via systemd

For running services, limits belong in LimitNOFILE/LimitNPROC in the unit file, not in the deployment script.

11. FAQ: ulimit in Bash: The Essentials at a Glance

1What is the difference between a soft limit and a hard limit with ulimit?
The soft limit is the currently enforced value, the hard limit the absolute ceiling. An unprivileged process can raise its soft limit up to the hard limit at any time, but never beyond it.
2Why do I get too many open files even though my server has little load?
The soft limit for open files often defaults to an outdated distribution setting of 1024. Many concurrent connections, even under overall low load, can reach that limit quickly.
3How do I raise the open-files limit only for my script?
With ulimit -n before starting the service within the same script, provided the new value is at or below the hard limit. Otherwise the hard limit must first be raised system-wide.
4Does ulimit -v limit a process's physical memory usage?
No, ulimit -v caps the virtual address space, not the physical memory actually used. cgroup-based limits are the more fitting tool for physical memory caps.
5How do I set a limit for only one step of my script?
By wrapping that step in a subshell using parentheses. A ulimit set there only applies inside that subshell and does not affect the rest of the script.
6Why doesn't a limit I set with ulimit affect my systemd service?
Because systemd services bypass PAM login sessions and therefore limits.conf. For systemd services, limits must be set directly as LimitNOFILE or LimitNPROC in the unit file.
7Where do I check the limits of an already-running, unrelated process?
In /proc/[pid]/limits. That file shows soft and hard limits for every resource regardless of which shell originally started the process.
8Can I raise the hard limit without root privileges?
No, an unprivileged process can only lower the hard limit, never raise it. Raising the hard limit requires root privileges or a system-wide configuration change.
9Why do only individual worker processes fail instead of the whole deployment?
Because ulimit -u caps the total number of threads and processes for a user. Only once that total crosses the limit under production load do further fork() calls start failing, seemingly at random.
10Should I set limits in the deployment script or in the systemd unit file?
For long-lived services, limits belong in the systemd unit file, since they get reliably re-enforced on every restart. ulimit in the script fits mainly short-lived build and batch steps.