skip to content

Using resource.setrlimit, which limit changes are reversible and which are not?

level: middleimportance: must knowfreq 46%

answer

  1. One direction is free, the other is not
  2. Soft moves anywhere below the hard value
  3. Lowering the hard limit is permanent
  4. Raising a hard limit needs privilege
  5. CPython signals both refusals with ValueError

basics

~20 s

A process may move its soft limit freely between zero and its hard limit, so those changes are reversible. Lowering the hard limit is one-way: raising it again needs privilege an ordinary process lacks, and the drop is inherited by children.

solid answer

~50 s

`resource.setrlimit(which, (soft, hard))` always takes both values, so the idiom is to read the pair with `resource.getrlimit` and change one half. The **soft** limit may be set anywhere in `[0, hard]` and moved back again — that is the reversible direction, and raising it up to the hard limit is the standard startup line for `resource.RLIMIT_NOFILE`. The **hard** limit can only be lowered by an unprivileged process, and that drop is permanent for the process and inherited by everything it forks; raising it again requires an effective root user (`CAP_SYS_RESOURCE` on Linux). CPython reports both refusals as `ValueError` — "current limit exceeds maximum limit" when the soft value is above the hard one, "not allowed to raise maximum limit" for the privilege failure. Set limits before spawning workers, since children inherit the values in force at spawn time.

code

python · 9 lines
python
import resource

soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
target = soft if hard == resource.RLIM_INFINITY else hard
try:
    resource.setrlimit(resource.RLIMIT_NOFILE, (target, hard))
except ValueError as exc:
    print("refused:", exc)
print("soft limit is now", resource.getrlimit(resource.RLIMIT_NOFILE)[0])

go deeper

for a junior

Recall that both values are passed as one tuple, and that the everyday use is raising the soft open-file limit up to the hard one at startup.

for a middle

Explain the direction rules precisely: soft anywhere within [0, hard] and reversible, hard downward only and permanent, with ValueError as the refusal for both failure cases.

for a senior

Demonstrate placement judgement — raise limits before workers exist, know that children inherit at spawn time, and test hard-limit drops in a child interpreter so the suite survives.

for a principal

Own the policy: which ceilings the platform sets versus which the application tightens on itself, and when a deliberately irreversible drop is worth the operational rigidity it buys.

### The rule in one line A process may move its **soft** limit anywhere between zero and its **hard** limit, and may move its **hard** limit **down only**. Going back up requires privilege that an ordinary service process does not have, so lowering a hard limit is a one-way door for the lifetime of that process and every child it later spawns. ### The call `resource.setrlimit(which, (soft, hard))` takes the resource constant and a two-element tuple — you always write both values, even when you only mean to change one, which is why the idiom is to read the current pair first: ```python soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard)) # raise soft up to the hard ceiling ``` `resource.RLIM_INFINITY` is a legal value wherever the kernel permits it. Note that raising the soft limit to the hard limit is the single most common line of `resource` code in real services: the environment ships a low soft limit and a high hard limit, and the program opts in to the higher one. ### How CPython reports refusal Both refusals arrive as `ValueError`, which surprises people who expect a `PermissionError`: * asking for a soft limit above the hard limit gives `ValueError: current limit exceeds maximum limit` (the kernel returned `EINVAL`); * asking to raise the hard limit without privilege gives `ValueError: not allowed to raise maximum limit` (the kernel returned `EPERM`). Anything else — an unsupported resource on this platform, for instance — comes back as `OSError`; `resource.error` is simply an alias for `OSError`. Catch `ValueError` around a startup call and degrade gracefully rather than crashing a service because one environment ships a lower ceiling. ### Why the one-way rule exists, and what it costs you The hard limit is a self-imposed sandbox. A process that is about to run something it does not fully trust — parsing a hostile file, running a plugin — can lower its own hard `resource.RLIMIT_NOFILE` or `resource.RLIMIT_AS` and know the restriction cannot be lifted afterwards, by that process or by anything it forks. Raising it again needs an effective root user (on Linux, the `CAP_SYS_RESOURCE` capability), which the sandboxed code is precisely not supposed to have. The cost is that the drop is unrecoverable and it propagates. Tests are the usual casualty: a test that lowers a hard limit to prove the failure path works has permanently damaged the interpreter process running the suite, and every later test that needs the old ceiling now fails. The fix is to exercise hard-limit drops in a child process — spawn a fresh interpreter, lower the limit there, and assert on its exit status and output — and keep the parent's limits untouched. ### Where to set the limit Limits are inherited across `fork()` and preserved across `exec()`, so the *when* matters as much as the *what*: a child receives whatever its parent had in force at the moment it was spawned, and a later `resource.setrlimit` in the parent does not reach a child that already exists. Do the raise as early as possible in start-up, before any worker processes are created. Python 3.14 sharpens this. The default `multiprocessing` start method on Unix other than macOS is now `forkserver` (macOS and Windows use `spawn`, and `fork` must now be requested explicitly). With `forkserver` a helper process is started once and every later worker is forked from *that* helper — so the limits your workers see are the ones in force when the fork server was first started, not the ones in force when you called for a worker. Raise limits before the first worker is requested, or raise them inside each worker as its first action. ### The trap that raising `RLIMIT_NOFILE` does not fix A high descriptor limit does not make every API able to use it. `select.select` is bounded by the fixed `FD_SETSIZE` compiled into the C library (commonly 1024) and will fail or misbehave on a descriptor number above it, no matter what the rlimit says. Code that genuinely needs tens of thousands of descriptors must use a scalable readiness interface — `selectors.DefaultSelector` picks `epoll` or `kqueue` for you, and `asyncio` sits on the same machinery. Raising the limit is necessary, but the polling API has to be able to spend it.

  • Which exception does CPython raise when the requested soft limit exceeds the hard limit?
    `ValueError`, not `PermissionError` — with the message "current limit exceeds maximum limit", which comes from the kernel's `EINVAL`. The privilege failure when raising a hard limit is also a `ValueError`, with "not allowed to raise maximum limit", from `EPERM`. Other failures, such as an unsupported resource, surface as `OSError`; `resource.error` is an alias for `OSError`.
  • Why should a service raise resource.RLIMIT_NOFILE before it spawns any workers?
    Limits are inherited across `fork()` and preserved across `exec()`, so a child gets whatever was in force at spawn time and a later change in the parent never reaches it. On Python 3.14 this is sharper: `multiprocessing` defaults to `forkserver` on Unix other than macOS, so workers inherit from the fork server as it existed when first started. Raise limits at the top of start-up, or in each worker as its first action.
  • Does raising resource.RLIMIT_NOFILE let select.select watch more descriptors?
    No. `select.select` is bounded by the fixed FD_SETSIZE compiled into the C library, commonly 1024, regardless of the rlimit; a descriptor number above it fails or corrupts the call. Code that needs tens of thousands of descriptors must use a scalable readiness interface — `selectors.DefaultSelector` chooses epoll or kqueue, and `asyncio` builds on the same machinery.
  • How would you test a code path that only runs when a hard limit is low?
    In a child interpreter, never in the test process. Lowering a hard limit is irreversible, so a test that does it in-process permanently damages the suite for every later test. Spawn a fresh interpreter with `subprocess`, lower the limit there, run the scenario, and assert on the child's exit status and output.

The hard limit is a ratchet: you can always click it tighter, and nothing short of a privileged key releases it again.

saying these in an interview costs you the question

  • Claiming a process can raise its own hard limit back later
  • Expecting PermissionError instead of ValueError from setrlimit
  • Passing a single integer instead of a (soft, hard) tuple
  • Lowering a hard limit inside a test process
  • Assuming children see limits the parent set after spawning them
  • Thinking a bigger RLIMIT_NOFILE alone makes select.select scale

context