---
title: "Debugging Tools"
canonical: https://www.quantconnect.com/docs/v2/writing-algorithms/key-concepts/debugging-tools
markdown: https://www.quantconnect.com/docs/v2/writing-algorithms/key-concepts/debugging-tools.md
language: python
source: https://github.com/QuantConnect/Documentation/tree/master/03%20Writing%20Algorithms/01%20Key%20Concepts/14%20Debugging%20Tools
modified: 2026-10-30
index: https://www.quantconnect.com/llms.txt
---

# Debugging Tools

[Documentation](https://www.quantconnect.com/docs/v2.md) > [Writing Algorithms](https://www.quantconnect.com/docs/v2/writing-algorithms.md) > Key Concepts > Debugging Tools

> Code examples are shown in Python. Append `?lang=csharp` to this URL for the C# version of this page.

## Introduction

When your algorithm throws [errors](https://www.quantconnect.com/docs/v2/cloud-platform/projects/debugging.md#02-Coding-Errors) or doesn't operate how you expect, use debugging tools to diagnose and resolve the issue. The tools to debug LEAN algorithms include a built-in debugger, logs, charts, and the Object Store.

## Debugger

The debugger is a built-in tool to help you debug coding errors while backtesting. The debugger enables you to slow down the code execution, step through the program line-by-line, and inspect the variables to understand the internal state of the program. The learn how to start and operate the backtest debugger in your development environment, see the Debugging page in the [Cloud Platform](https://www.quantconnect.com/docs/v2/cloud-platform/backtesting/debugging.md), [Local Platform](https://www.quantconnect.com/docs/v2/local-platform/backtesting/debugging.md), or [CLI](https://www.quantconnect.com/docs/v2/lean-cli/backtesting/debugging.md) documentation.

## Logs

Algorithms can record string messages ('log statements') to a file for analysis after a backtest is complete, or as a live algorithm is running. These records can assist in debugging logical flow errors in the project code or recording key decision moments. Backtests can process millions of data points. If you place a log statement in the wrong place, it can generate gigabytes of logs, which can be difficult to sift through. When you add to the log file, ensure they are readable.

```python
# Logging records messages to a file so you can debug errors and record key decisions.
self.log(f"Signal triggered. Placing limit order: {limit}")
```

Debug statements are the same as log statements, but `debug` method statements are orange in the Cloud Terminal to provide more emphasis. Debug statements are streamed to the Cloud Terminal while the backtest is running. This can be helpful for real-time updates or monitoring in live-trading.

```python
# Debugging streams messages to the Cloud Terminal for real-time analysis.
self.debug("Exposure to the {sector_name} sector is above 50%!")
```

For more information about log statements, see [Logging](https://www.quantconnect.com/docs/v2/writing-algorithms/logging.md).

## Charts

Plotting values over time can help you understand its range, identify outliers in data, and see times when your algorithm generates unusual values. Before you create a plot, develop a theory about what you expect to see before you view it to gain a deeper understanding of your algorithm. For example, a plot of an asset's price standard deviations should be between -4 and +4, with any outliers happening during extreme market conditions like the COVID pandemic in March 2020. LEAN provides a powerful charting API. You can create time series plots with just a single line of code.

```python
# Create time series plots to understand the asset range and identify outliers.
self.plot("Deviations", "aapl", asset_deviation)
```

For more information about creating charts, see [Charting](https://www.quantconnect.com/docs/v2/writing-algorithms/charting.md).

## Object Store

The Object Store is a key-value data store for low-latency information storage and retrieval. During a backtest, you can build large objects you’d like to analyze and write them for later analysis. This workflow can be helpful when the objects are large and plotting is impossible or when you want to perform analysis across many backtests.

```python
# The Object Store provides a key-value data store optimized for fast access and retrieval of information.
self.object_store.save("key", value)
```

For more information about the Object Store, see [Object Store](https://www.quantconnect.com/docs/v2/writing-algorithms/object-store.md). For a specific example of saving indicator values during a backtest into the Object Store and then plotting them in the Research Environment, see [Example for Plotting](https://www.quantconnect.com/docs/v2/writing-algorithms/object-store.md#12-Example-for-Plotting).

## Performance Chart

QuantConnect provides a series of metrics to help you analyze the computational performance of your algorithms. To enable the [Performance Chart](https://www.quantconnect.com/docs/v2/cloud-platform/backtesting/results.md#05-Performance-Chart), set the `self.settings.performance_sample_period` attribute to the desired sampling period:

```python
self.settings.performance_sample_period = timedelta(7)
```

[Python Profilers](https://docs.python.org/3/library/profile.html) from the Python Standard Library enable you to find the functions and methods in your algorithm that consume most of the runtime. To analyze your algorithm's efficiency with the Python Profilers, follow these steps:

1. Open the project in the [Cloud Platform](https://www.quantconnect.com/docs/v2/cloud-platform/projects/getting-started.md#02-View-All-Projects), [Local Platform](https://www.quantconnect.com/docs/v2/local-platform/projects/getting-started.md#04-Open-Projects), or a local IDE if you use the CLI.
2. At the top of the `main.py` file, import the classes you need and enable the `Profile`.
   ```python
   from cProfile import Profile
   from pstats import Stats
   from io import StringIO

   # Create the profile and start collecting profiling data.
   profile = Profile()
   profile.enable()
   ```
3. Define or extend the [on_end_of_algorithm](https://www.quantconnect.com/docs/v2/writing-algorithms/key-concepts/event-handlers.md#15-End-Of-Algorithm-Events) method to `disable` the `Profile` and save information on the most time-consuming functions to the [Object Store](https://www.quantconnect.com/docs/v2/research-environment/object-store.md).
   ```python
   class FunctionTimeConsumptionAlgorithm(QCAlgorithm):
       def initialize(self):
           pass

       def on_end_of_algorithm(self):
           # Stop collecting profiling data
           profile.disable()
           stream = StringIO()
           # Save the top 20 time-consuming functions to a file in the Object Store.
           Stats(profile, stream=stream).sort_stats('cumulative').print_stats(20)
           # Save the profiling data to the Object Store using the algorithm ID.
           self.object_store.save(f"{self.algorithm_id}_profile", stream.getvalue())
   ```
4. [Open the Research Environment](https://www.quantconnect.com/docs/v2/research-environment/key-concepts/getting-started.md#03-Open-Notebooks) and create a `QuantBook`.
   ```python
   qb = QuantBook()
   ```
5. Get the backtest Id and read the profiling data from the Object Store.
   The process to get the backtest Id depends on if you use the [Cloud Platform](https://www.quantconnect.com/docs/v2/cloud-platform/backtesting/getting-started.md#07-Get-Backtest-Id), [Local Platform](https://www.quantconnect.com/docs/v2/local-platform/backtesting/getting-started.md#07-Get-Backtest-Id), or [CLI](https://www.quantconnect.com/docs/v2/lean-cli/backtesting/deployment.md#05-Get-Backtest-Id).
   ```python
   backtest_id = "8b16cec0c44f75188d82f9eadb310e17"
   profile_output = qb.object_store.read(f"{backtest_id}_profile")
   print(profile_output)
   ```

## Memory Metrics

The static `OS` class in the `QuantConnect` namespace exposes memory and CPU metrics that you can read from your algorithm to monitor resource usage during backtests and live trading.

The following table lists the static members of the `OS` class that report memory and CPU usage:

| Member | Data Type | Description |
| --- | --- | --- |
| `APPLICATION_MEMORY_USED` | `int` | Private memory allocated to the current process, including managed and unmanaged memory, in megabytes. |
| `TOTAL_PHYSICAL_MEMORY_USED` | `int` | Managed-runtime RAM usage (sampled with `gc.get_total_memory`), in megabytes. LEAN's memory monitor samples this value. |
| `CPU_USAGE` | `float` | Total CPU usage as a percentage, sampled on a background thread. |
| `get_server_statistics()` | `Dictionary[str, str]` | Snapshot dictionary with `CPU Usage`, `Used RAM`, `Total RAM`, `Hostname`, and `LEAN Version` entries. |

The following examples show how to read these metrics from an algorithm:

```python
# Log the current memory usage of the algorithm process.
self.log(f"Total physical memory used: {OS.TOTAL_PHYSICAL_MEMORY_USED} MB")
self.log(f"Application memory used: {OS.APPLICATION_MEMORY_USED} MB")

# Log the current CPU usage as a percentage.
self.log(f"CPU usage: {OS.CPU_USAGE}%")

# Log a full snapshot of the server statistics.
for kvp in OS.get_server_statistics():
    self.log(f"{kvp.key}: {kvp.value}")
```

LEAN monitors the process's RAM usage during backtests and live trading so it can stop the algorithm gracefully when it approaches the node's memory limit, instead of letting the runtime crash. To avoid killing an algorithm over a short-lived spike, LEAN does not act on the raw reading — it smooths the sampled value first and checks the smoothed value against the limit.

LEAN samples `OS.TOTAL_PHYSICAL_MEMORY_USED` once per second in backtests and once every 10 seconds in live trading, and feeds each sample into an exponential moving average. The engine sets `emaPeriod = 60`, so the smoothed reading covers roughly the last minute of a backtest and the last 10 minutes of a live deployment.

```python
# Update the smoothed memory reading on each sample.
sample = OS.TOTAL_PHYSICAL_MEMORY_USED
memory_used = int((ema_period - 1) / ema_period * memory_used + (1 / ema_period) * sample)
```

LEAN stops the algorithm when either of the following conditions is true:

- The smoothed reading exceeds the node's memory limit.
- A single raw sample exceeds twice the node's memory limit.

The second condition is what makes short-lived spikes survivable. A universe selection pass, an Option chain, or a bulk history request can allocate well above the limit for a few samples without stopping the algorithm, as long as it releases the memory before it lifts the smoothed reading above the limit. An algorithm on a 512MB node can therefore touch just under 1GB on a single sample, but it has to average below 512MB.

Once a minute, if the smoothed reading is above 80% of the limit, LEAN writes `Execution Security Error: Memory usage over 80% capacity. Sampled at {lastSample}` to the log. Treat 80% of the limit as the level to design for, which is 410MB on a 512MB node. An algorithm that sits above it has no room left for the spikes the allowance exists to cover.

When LEAN stops the algorithm, it reports the runtime message `Execution Security Error: Memory Usage Maxed Out - {memoryCap}MB max, with last sample of {lastSample}MB.`, where `{memoryCap}` is the node's configured limit and `{lastSample}` is the most recent reading.

For the memory cost of a subscription and how to pick a node model, see [Node Sizing](https://www.quantconnect.com/docs/v2/writing-algorithms/key-concepts/algorithm-performance.md#07-Node-Sizing).

## JIT Compilation

To increase the speed of some methods, you can use decorators like `@jit` or `@lru_cache`. The `@jit` decorator from Numba is for Just-In-Time (JIT) compilation. It compiles Python code to machine code at runtime to increase the speed of loops and mathematical operations. However, Numba can't compile all Python code. It performs best with NumPy arrays. If you add the `@jit` decorator to your methods, it can make the debugging process more challenging since the code is compiled to machine code. The following code snippet shows an example of using the `@jit` decorator:

```python
import numpy as np
from numba import jit
import time

# Without JIT
def slow_function(arr):
    result = 0
    for i in range(len(arr)):
        result += np.sin(arr[i]) * np.cos(arr[i])
    return result

# With JIT
@jit(nopython=True)
def fast_function(arr):
    result = 0
    for i in range(len(arr)):
        result += np.sin(arr[i]) * np.cos(arr[i])
    return result

# Example usage
arr = np.random.rand(1000000)

# Test without JIT.
start = time.time()
slow_function(arr)
print(f"Slow function took: {time.time() - start} seconds")

# Test with JIT.
start = time.time()
fast_function(arr)  # This will compile first, then run.
print(f"Fast function took: {time.time() - start} seconds")
```

> Slow function took: 1.1557221412658691 seconds
>
> Fast function took: 0.32973551750183105 seconds

The `@lru_cache` decorator from the `functools` module is for Least Recently Used (LRU) caching. It caches the result of a function based on its arguments, so that the function body doesn't have to repeatedly execute when you call the function multiple times with the same inputs. The `maxsize` argument of the decorator defines the number of results to cache. For example, `@lru_cache(maxsize=512)` means that it should cache a maximum of 512 results. If you add the `@lru_cache` decorator to your methods, make sure your function is completely dependent on the arguments (not any global state variables) or else the caching process may lead to incorrect results. The following code snippet shows an example of using the `@lru_cache` decorator:

```python
from functools import lru_cache
import time

# Without caching
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# With caching
@lru_cache(maxsize=512)
def fibonacci_cached(n):
    if n < 2:
        return n
    return fibonacci_cached(n-1) + fibonacci_cached(n-2)

n = 30

# Test without caching.
start = time.time()
for _ in range(n):
    fibonacci(n)
print(f"Fibonacci without cache took: {time.time() - start} seconds")

# Test with caching.
start = time.time()
for _ in range(n):
    fibonacci_cached(n)
print(f"Fibonacci with cache took: {time.time() - start} seconds")
```

> Fibonacci without cache took: 4.387492418289185 seconds
>
> Fibonacci with cache took: 9.322166442871094e-05 seconds
