A shell’s ulimit builtin changes resource limits for that shell and the processes it subsequently starts. For file descriptors, RLIMIT_NOFILE has a current soft value and a hard ceiling. Understanding where those values are inherited is essential when a terminal reports a generous limit but an independently managed service still fails to open files or accept connections.

When it breaks

The soft value is the limit normally enforced for new descriptor allocation. An unprivileged process can raise its soft value only within the existing hard ceiling and can lower its hard limit. Raising a hard limit requires the relevant authority and remains subject to kernel constraints. A low hard value inherited at startup can therefore prevent the application from applying its intended soft setting.

Changing a child’s limit does not change the parent that launched it. Likewise, a command in a newly opened terminal does not retroactively configure a daemon started by systemd. The service’s actual parent and launch configuration determine its initial limits. A shell profile only has an effect when the launch path actually reads and applies that profile.

Bash option selection matters too. Reading ulimit -n normally reports the soft value, but assigning a value without selecting -S or -H changes both soft and hard limits. An operator intending a temporary soft reduction can accidentally lower the hard ceiling as well. Check the shell’s documented behavior instead of treating every similar-looking command as equivalent.

Check it quickly

Run this Python script as an administrator in a disposable Linux container with Bash and Python installed. It launches one experiment under an unprivileged numeric identity and leaves the administrator controller’s limits unchanged. The recorded environment used Linux 6.12.76, Bash 5.2.15, and Python 3.11.2. No existing account configuration or service unit is edited:

import resource,subprocess

before=resource.getrlimit(resource.RLIMIT_NOFILE)
assert before[0]>=64 and before[1]>=64
script=r'''
set -eu
ulimit -S -n 32
ulimit -H -n 64
python3 - <<'CHILD'
import resource
assert resource.getrlimit(resource.RLIMIT_NOFILE)==(32,64)
resource.setrlimit(resource.RLIMIT_NOFILE,(48,64))
assert resource.getrlimit(resource.RLIMIT_NOFILE)==(48,64)
print('first child inherits 32/64 and raises its own soft limit to 48')
CHILD
python3 - <<'CHILD'
import resource
assert resource.getrlimit(resource.RLIMIT_NOFILE)==(32,64)
print('second child still inherits shell values 32/64')
CHILD
if ulimit -S -n 65 2>/dev/null; then exit 11; fi
if ulimit -H -n 128 2>/dev/null; then exit 12; fi
(
    ulimit -n 16
    python3 - <<'CHILD'
import resource
assert resource.getrlimit(resource.RLIMIT_NOFILE)==(16,16)
print('Bash assignment without -S/-H changed both limits in this subshell')
CHILD
)
python3 - <<'CHILD'
import resource
assert resource.getrlimit(resource.RLIMIT_NOFILE)==(32,64)
print('parent shell limits survived child changes and rejected invalid increases')
CHILD
'''
# Run the experiment under an unprivileged numeric identity in this disposable container.
result=subprocess.run(['bash','--noprofile','--norc','-c',script],user=61001,group=61001,extra_groups=[],
                      env={'PATH':'/usr/bin:/bin','LC_ALL':'C'},capture_output=True,text=True,timeout=10)
assert result.returncode==0,(result.returncode,result.stderr)
assert resource.getrlimit(resource.RLIMIT_NOFILE)==before
print(result.stdout,end='')
print('administrator controller limits remain unchanged')

The test shell starts by explicitly setting soft 32 and hard 64. Its first Python child inherits that pair, then successfully raises its own soft limit to 48 while keeping hard 64. This demonstrates an allowed increase within the ceiling; it does not require administrative authority inside the child.

A second Python child, launched by the same shell, still receives 32 and 64. The first child’s change stayed in that child, so there is no reverse propagation to the shell or its later children. Both observations read the actual resource-limit API rather than inferring values from the text of a startup script.

Attempts to set soft 65 above hard 64 and to raise the hard value to 128 are rejected in the unprivileged shell. The fixture checks the commands’ failure results. It does not suppress an error and then pretend the larger values were applied, and it does not rely on root accidentally making an otherwise forbidden operation succeed.

Within a subshell, ulimit -n 16 changes both values to 16. After that subshell ends, another child of the original test shell still sees 32 and 64. This separates two rules that often become confused: an assignment can affect both limit fields, while its process-local scope still prevents it from rewriting the parent.

Finally, the administrator controller compares its original limits with its current pair and confirms they are unchanged. All reductions and rejected increases occurred in disposable child processes. The exercise validates inheritance and permission rules, not a production server’s required descriptor budget or the behavior of every possible shell implementation.

Configure and inspect the real launch path

For a systemd service, configure the appropriate LimitNOFILE value in its effective unit settings and start a replacement process when that startup policy changes. For a shell-launched application, apply the intended setting in the launch path before execution. A one-time terminal command is not persistent configuration unless something deliberately runs it again on the next launch.

Inspect the target process’s limits, such as through /proc/PID/limits, and correlate them with the correct running instance. Reading the control shell’s limit answers a different question. Container runtime settings, user managers, and intermediate wrappers can each affect inheritance, so trace the actual process chain when expected values differ.

Choose a budget that includes sockets, ordinary files, pipes, event descriptors, and operational headroom. Raising the ceiling consumes no descriptors by itself, but allowing more live work can increase memory and downstream load. Validate the application’s event mechanism and ownership discipline as well as the numbers; higher limits are useful when the workload can use them correctly.

Sources