numpy
threads
performance optimization
python
computational efficiency

Limit number of threads in numpy

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

When NumPy appears to use many CPU threads, the threads usually come from the native math library underneath it rather than from NumPy itself. Operations such as matrix multiplication, singular value decomposition, and other linear algebra routines often run through OpenBLAS, MKL, or another backend that decides how many worker threads to create.

Core Sections

Know which layer controls the thread count

NumPy delegates many heavy numerical operations to lower-level libraries. That means there is no single universal numpy.set_num_threads() call that works for every installation. Instead, you typically control the backend through environment variables such as OPENBLAS_NUM_THREADS, MKL_NUM_THREADS, or OMP_NUM_THREADS.

This distinction matters because developers often try to optimize NumPy itself when the real behavior is controlled by the linked BLAS or OpenMP runtime.

Set environment variables before importing NumPy

The safest approach is to define the thread limit before Python loads NumPy and its native dependencies. Many backends read their configuration only once during startup.

bash
OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 OMP_NUM_THREADS=1 python benchmark.py

You can also export the variables in the shell session:

bash
1export OPENBLAS_NUM_THREADS=1
2export MKL_NUM_THREADS=1
3export OMP_NUM_THREADS=1
4python benchmark.py

This approach is reliable for local scripts, CI jobs, data pipelines, and container entry points.

Setting limits from Python

If you control the main entry point, you can set the environment variables before importing NumPy. The timing matters. If NumPy or a dependent library is already imported elsewhere, changing the variable may have no effect.

python
1import os
2
3os.environ["OPENBLAS_NUM_THREADS"] = "1"
4os.environ["MKL_NUM_THREADS"] = "1"
5os.environ["OMP_NUM_THREADS"] = "1"
6
7import numpy as np
8
9left = np.arange(9).reshape(3, 3)
10right = np.eye(3)
11print(left @ right)

For larger applications, shell-level configuration is usually safer because it avoids import-order surprises.

Use threadpoolctl when you need scoped control

Sometimes you do not want a global thread limit for the entire process. In that case, threadpoolctl lets you temporarily cap the number of threads around a specific block of work.

python
1import numpy as np
2from threadpoolctl import threadpool_limits
3
4with threadpool_limits(limits=1):
5    a = np.random.rand(1500, 1500)
6    b = np.random.rand(1500, 1500)
7    result = a @ b
8    print(result.shape)

This is useful when one stage of a service should avoid saturating the machine, while a different stage can use the backend default.

Verify which backend you are actually using

If a thread variable seems ineffective, inspect the active native libraries instead of guessing.

python
1from threadpoolctl import threadpool_info
2
3for pool in threadpool_info():
4    print(pool["internal_api"], pool["num_threads"], pool["filepath"])

This tells you whether the process is using OpenBLAS, MKL, or something else. Once you know the backend, you can choose the correct knob and test again.

Why limiting threads can improve performance

More threads do not automatically mean faster code. Oversubscription is common in data-processing systems that already use multiprocessing, task queues, or parallel test runners. For example, if four Python processes each open eight BLAS threads on an eight-core machine, the operating system must schedule thirty-two active workers on eight cores.

python
1from multiprocessing import Pool
2import os
3
4os.environ["OPENBLAS_NUM_THREADS"] = "1"
5
6
7def compute_sum(n: int) -> float:
8    import numpy as np
9    data = np.arange(n, dtype=float)
10    return float((data * data).sum())
11
12
13if __name__ == "__main__":
14    with Pool(4) as pool:
15        print(pool.map(compute_sum, [1_000_000] * 4))

In that setup, one BLAS thread per worker process is often faster and more predictable than letting every process fan out aggressively.

Common Pitfalls

  • Setting OPENBLAS_NUM_THREADS or similar variables after NumPy is already imported often has no effect.
  • Changing only one environment variable without confirming the active backend can lead to false conclusions about thread control.
  • Assuming that higher thread counts always improve throughput ignores oversubscription and memory-bandwidth limits.
  • Blaming NumPy alone for thread behavior can hide the fact that the real tuning point is MKL, OpenBLAS, or OpenMP.
  • Benchmarking on an idle laptop and then deploying the same settings to a shared server often produces very different results.

Summary

  • NumPy thread counts usually come from the native math backend underneath the library.
  • Set backend thread limits before NumPy is imported for the most reliable behavior.
  • Use threadpoolctl when you need temporary or inspectable runtime control.
  • Check which backend is loaded instead of guessing which environment variable matters.
  • Lower thread counts are often the right choice when NumPy runs inside multiprocessing or shared compute environments.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.