Reducing AWS Lambda Cold Starts
Tags: coldstart, python, aws, serverless
Published: 2024-05-24 20:01:47

## Introduction
AWS Lambda functions with large dependencies can suffer from significant cold start times due to the time it takes to import these dependencies. In this post, we'll explore a simple yet effective way to reduce Python cold start times without changing a line of code by precompiling dependencies into bytecode. We'll cover the following topics:

+ Understanding Python bytecode and `*.pyc` files
+ Precompiling dependencies to reduce init duration
+ Using Python optimization levels for further improvements
+ Reducing memory overhead with `PYTHONNODEBUGRANGES=1`

## Understanding Python Bytecode and `*.pyc` Files
When Python loads source code for the first time, it [compiles it to bytecode](https://peps.python.org/pep-3147/#background) and saves it to `*.pyc` files in `__pycache__` directories. On subsequent loads, Python will use these precompiled `*.pyc` files instead of recompiling the source code, saving time.

By precompiling dependencies and removing the original `*.py` files, we can bypass the bytecode compilation step during function initialization. This can significantly reduce init duration. For example, a simple handler with `import numpy as np` can see an init duration reduction of approximately 20%.

Removing `*.py` files affects the detail in tracebacks when exceptions occur. With `*.py` files, tracebacks include the relevant source code lines. Without `*.py` files, tracebacks only display line numbers, requiring you to refer to your version-controlled source code for debugging. For custom code not in version control, consider keeping the relevant `*.py` files to aid in debugging. For third-party packages, removing `*.py` files can improve cold start times at the cost of slightly less detailed tracebacks.

### Benefits
+ Faster imports during runtime
+ Reduced package size by removing `*.py` files
+ Same `*.pyc` files work on any OS

### How to
precompile dependencies and remove `*.py` files:
```bash
$ python -m compileall -b .
$ find . -name "*.py" -delete
```

### Caution
Always test your code after precompilation, as some packages do rely on the presence of `*.py` files.

## Using Python Optimization Levels
Python offers [optimization levels](https://docs.python.org/3/using/cmdline.html#cmdoption-O) that can further improve init and runtime duration by removing debug statements and docstrings.

### Benefits
| Optimization Level | Effect |
|-----|-----|
| -O | Removes assert statements and code blocks that rely on `__debug__` |
| -OO | Removes assert statements, `__debug__` code blocks, and docstrings |

### How to
precompile with optimization level 2:
```bash
$ python -m compileall -o 2 -b .
$ find . -name "*.py" -delete
```

### Caution
Test your code thoroughly, as optimization levels may introduce subtle bugs if your business logic relies on assert statements or docstrings.

## Reducing Memory Overhead with `PYTHONNODEBUGRANGES=1`
In Python 3.11+, you can use the [`PYTHONNODEBUGRANGES=1`](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONNODEBUGRANGES) environment variable to disable the inclusion of column numbers in tracebacks. This reduces memory overhead but sacrifices the ability to pinpoint the exact location of exceptions on a given line of code.

Example traceback with debug ranges:
```
Traceback (most recent call last):
  File "hello.py", line 1, in <module>
    print(f"Hello world! {1/0}")
                          ~^~
ZeroDivisionError: division by zero
```

Example traceback without debug ranges:
```
Traceback (most recent call last):
  File "hello.py", line 1, in <module>
    print(f"Hello world! {1/0}")
ZeroDivisionError: division by zero
```

### Benefits
+ Reduced memory overhead
+ Reduced package sizes

### How to
precompile with optimization level 2 and no debug ranges:
```bash
$ PYTHONNODEBUGRANGES=1 python -m compileall -o 2 -b .
$ find . -name "*.py" -delete
```

## Summary
By precompiling dependencies, using optimization levels, and disabling debug ranges, you can significantly reduce cold start times in your AWS Lambda Python functions. These techniques can lead to over 20% faster startup times, allowing your functions to respond more quickly to events. Try these optimizations in your own functions and see the performance improvements for yourself!