How to asynchronously run Matplolib server-side with a timeout? The process hangs randomly
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Matplotlib hangs on servers because its default backend (TkAgg, Qt5Agg) tries to connect to a display server that does not exist. The fix is to use the non-interactive Agg backend (matplotlib.use('Agg')) before importing pyplot, and run plot generation in a separate process with a timeout using concurrent.futures.ProcessPoolExecutor. Thread-based approaches fail because matplotlib is not thread-safe — the Agg backend uses global state that causes deadlocks under concurrent access.
The Problem
The default backend attempts to create a GUI window. On a server without a display ($DISPLAY not set), this blocks indefinitely or crashes.
Fix 1: Use the Agg Backend
matplotlib.use('Agg') must be called before any matplotlib.pyplot import. The Agg backend renders to a file or buffer without requiring a display.
Fix 2: Process-Based Execution with Timeout
ProcessPoolExecutor creates a separate process with its own matplotlib state. If the process hangs, future.result(timeout=N) raises TimeoutError instead of blocking forever.
Fix 3: Flask/Django Web Server Integration
Always call plt.close(fig) after saving to free memory. Without it, each request leaks a figure object, eventually exhausting server memory.
Fix 4: Thread Safety with Locks
The object-oriented API (Figure + FigureCanvasAgg) avoids pyplot's global state, making it safe for concurrent use without locks.
Fix 5: Environment Variable Approach
Setting MPLBACKEND=Agg as an environment variable applies the backend globally without modifying code.
Common Pitfalls
- Importing pyplot before setting the backend:
import matplotlib.pyplot as plttriggers backend initialization.matplotlib.use('Agg')must come before anypyplotimport, otherwise it raises a warning and has no effect. - Not closing figures (
plt.close()): Eachplt.figure()orplt.subplots()allocates memory. Withoutplt.close(fig), figures accumulate in memory, causing OOM crashes on long-running servers. - Using threads with pyplot:
pyplotmaintains global state (current figure, current axes) that is not thread-safe. Concurrent threads corrupt each other's plots. Use the OO API (Figure+FigureCanvasAgg) or a process pool instead. - Font cache building on first run: The first matplotlib import on a new server triggers font cache building, which can take 30+ seconds and look like a hang. Pre-warm the cache during deployment with
python -c "import matplotlib.pyplot". - Missing system fonts in Docker: Minimal Docker images may lack fonts, causing matplotlib to fall back to a slow font search. Install
fontconfigand basic fonts (apt-get install fonts-dejavu-core).
Summary
- Use
matplotlib.use('Agg')before importingpyplot— or setMPLBACKEND=Aggenvironment variable - Run plot generation in
ProcessPoolExecutorwith a timeout to prevent indefinite hangs - Always call
plt.close(fig)after saving to prevent memory leaks - Use the object-oriented API (
Figure+FigureCanvasAgg) for thread-safe rendering - Pre-warm the font cache during deployment to avoid first-run delays
- Install
fonts-dejavu-corein Docker images for reliable font rendering

