resource.setrlimit(resource.RLIMIT_STACK to increase Linux process stack size is also needed on Python <= 3.10
It apparently stopped mattering on Python 3.11, in which only the sys.setrecursionlimit matters:
sys.setrecursionlimit(2**31 - 1)
I choose 2**31 - 1 as that is the maximum value accepted by sys.setrecursionlimit on my system, anything larger blows up a:
OverflowError: Python int too large to convert to C int
Accoding to my experiments below, the change seems to have happened on Python 3.11. This is alsmost certainly an outcome of the "Faster CPython" project, 3.11 release notes mentions:
Inlined Python function calls
During a Python function call, Python will call an evaluating C function to interpret that function’s code. This effectively limits pure Python recursion to what’s safe for the C stack.
In 3.11, when CPython detects Python code calling another Python function, it sets up a new frame, and “jumps” to the new code inside the new frame. This avoids calling the C interpreting function altogether.
Most Python function calls now consume no C stack space, speeding them up.
Consider the following test code:
main.py
import resource
import sys
recursionlimit0 = sys.getrecursionlimit()
print('sys.getrecursionlimit() = {}'.format(sys.getrecursionlimit()))
print('resource.getrlimit(resource.RLIMIT_STACK) = {}'.format(resource.getrlimit(resource.RLIMIT_STACK)))
# Will segfault without in a few seconds this line.
if len(sys.argv) > 1:
sys.setrecursionlimit(2**31 - 1)
if len(sys.argv) > 2:
resource.setrlimit(resource.RLIMIT_STACK, [0x10000000, resource.RLIM_INFINITY])
print('sys.getrecursionlimit() = {}'.format(sys.getrecursionlimit()))
print('resource.getrlimit(resource.RLIMIT_STACK) = {}'.format(resource.getrlimit(resource.RLIMIT_STACK)))
def f(i):
if i < 100000 or i % 100000 == 0:
print(i)
sys.stdout.flush()
f(i + 1)
f(0)
this are the outcomes on the different test systems. Older Ubuntu were tested under Docker on Uubntu 25.04:
| Ubuntu |
Python |
Nothing |
setrecursionlimit |
setrecursionlimit + resource.setrlimit |
| 16.04 |
2.7.12 |
998 |
29,096 |
900k |
| 16.04 |
3.5.2 |
996 |
27,546 |
800k |
| 25.04 |
3.5.2 |
996 |
27,546 |
800k |
| 25.04 |
3.8.20 (uv) |
995 |
10,896 |
300k |
| 25.04 |
3.10.19 (uv) |
995 |
8050 |
200k |
| 25.04 |
3.11.14 (uv) |
996 |
OOM |
OOM |
| 25.04 |
3.13.3 |
998 |
OOM |
OOM |
TODO: prior to 3.11, after increasing the process stack, what is the next limit that leads to segfault and prevents OOM as desired?
Further explanation of the Linux process stack limit
The Linux kernel limits the stack of processes.
If the Python interpreter tries to go over the stack limit, the Linux kernel makes it segmentation fault.
Certain versions of Python seem to store some data that is used during recursion on the stack of the interpreter. This old thread: CPython - Internally, what is stored on the stack and heap? suggests that Python objects are not stored on the stack, heap only, which implies that resource.setrlimit(resource.RLIMIT_STACK should not matter. However our experiments indicate that the answers on that thread are not entirely correct.
The stack limit size is controlled with the getrlimit and setrlimit system calls.
Python offers access to those system calls through the resource module.
sys.setrecursionlimit mentioned e.g. at https://stackoverflow.com/a/3323013/895245 only increases the limit that the Python interpreter self imposes on its own stack size, but it does not touch the limit imposed by the Linux kernel on the Python process.
From Bash, you can see and set the stack limit (in kb) by running the following commands before launching Python:
ulimit -s
ulimit -s 10000
The default value for me is 8 MB.
If we manage to increase all relevant limits correctly as desired, the interpreter will take more and more RAM until it takes up all the system RAM, a thich point the Linux OOM Killer will be called in to kill some processes and free memory.
See also:
line <n>, in <module>in stack traces) and this code takes 2 stack frames forn=1(because the base case isn < 1, so forn=1it still recurses). And I guess the recursion limit is not inclusive, as in it's "error when you hit 1000" not "error if you exceed 1000 (1001)".997 + 2is less than 1000 so it works998 + 2doesn't because it hits the limit.recursive_function(997)works, it breaks at998. When you callrecursive_function(998)it uses 999 stack frames and 1 frame is added by the interpreter (because your code is always run as if it's part of top level module), which makes it hit the 1000 limit.