Scenario
advanced
CPU-Bound Task Blocking the Async Event Loop
Fix a FastAPI service that becomes unresponsive during image thumbnail generation by offloading CPU-bound work to a process pool or task queue.
The Situation
Senior Python developer / backend engineering interviews
Context: Your FastAPI service processes image thumbnails on upload. When a user uploads a large image, the API becomes unresponsive for all other users for ~3 seconds. Why and how do you fix it?
# BROKEN — CPU-bound work on the async event loop blocks everything
from PIL import Image
@app.post("/upload")
async def upload_image(file: UploadFile):
data = await file.read()
img = Image.open(io.BytesIO(data))
thumbnail = img.resize((200, 200)) # CPU-bound — blocks event loop!
# During this 3 seconds, no other requests are served
return save_thumbnail(thumbnail)
# FIX — run CPU-bound work in a thread pool executor
import asyncio
from concurrent.futures import ProcessPoolExecutor
from PIL import Image
process_pool = ProcessPoolExecutor(max_workers=4)
def generate_thumbnail(data: bytes) -> bytes:
"""Pure function — safe to run in subprocess."""
img = Image.open(io.BytesIO(data))
thumbnail = img.resize((200, 200), Image.LANCZOS)
buf = io.BytesIO()
thumbnail.save(buf, format="JPEG", quality=85)
return buf.getvalue()
@app.post("/upload")
async def upload_image(file: UploadFile):
data = await file.read()
loop = asyncio.get_event_loop()
# Run in process pool — event loop stays free
thumbnail_data = await loop.run_in_executor(
process_pool, generate_thumbnail, data
)
return store_thumbnail(thumbnail_data)
# For truly heavy workloads — use a task queue (Celery + Redis)
from celery import Celery
celery = Celery("tasks", broker="redis://localhost/0")
@celery.task
def process_image_task(image_data: bytes):
return generate_thumbnail(image_data)
@app.post("/upload")
async def upload_image(file: UploadFile):
data = await file.read()
task = process_image_task.delay(data)
return {"task_id": task.id, "status": "processing"}
@app.get("/upload/{task_id}")
async def get_result(task_id: str):
task = AsyncResult(task_id)
return {"status": task.status, "result": task.result}
What You Learned
- Why CPU-bound work blocks the event loop
- run_in_executor with a process pool
- Offloading heavy work to Celery
Add More Questions to This Guide
Know a question that should be here? Share it and help the community!
Open Google Form