Programming

Node.js/Express server CPU spikes and event loop stalls during concurrent multipart image uploads due to synchronous image processing

Environment

Environment

Node.js 16.20, Express 4.18 application running on a single-core container in Kubernetes, upload endpoint accepts multipart/form-data and performs on-request image resizing/conversion using a pure-JavaScript library synchronously before responding. Under 10 concurrent upload clients, the server CPU hits 100% and response latency grows without additional logs.

Symptoms

Symptoms

HTTP POST to /upload succeeds for small images but when multiple clients upload large images concurrently, other unrelated endpoints become unresponsive, Node process CPU remains pegged, and event loop delay measurements spike (1500+ ms). No uncaught exceptions; process stays alive but is unable to handle new requests in a timely fashion.

Observations

Observations

Profiling with clinic/0x shows the main thread dominated by a long-running synchronous image decode/encode function inside the request handler. Replacing the image library with an async native binding (sharp) in a quick test reduced CPU impact substantially. Adding setImmediate around the heavy function did not appreciably improve throughput. Scaling the pod to additional replicas restores responsiveness for new requests but individual requests still take long.

Already attempted

Already attempted

Tried moving synchronous function calls behind process.nextTick/setImmediate (did not help). Attempted to offload work via child_process.fork for one test endpoint (worked but increased memory usage). Swapping to sharp for async processing in a test branch improved performance but full migration not yet completed.

Question

Question

What are appropriate, production-safe approaches to eliminate event-loop blocking for CPU-heavy image processing in Express, and how should I migrate without introducing race conditions or losing in-flight uploads?

COMMUNITY RESPONSES

Verified community troubleshooting

0 replies

No answers yet..

Showing 0 of 0 community responses
AI DIAGNOSTICS

Incident analysis

Updated 1w ago• Trouble ID

Most likely causes

Use of synchronous, CPU-bound image processing libraries (pure JS) inside the main event loop. 100%
Insufficient native bindings (e.g., not using libvips-based libraries that perform work off-thread). 95%
Lack of worker-thread or external worker architecture for CPU-bound tasks. 90%

Summary

Node.js runs JavaScript on a single thread; any CPU-bound synchronous work will block the event loop and starve other requests. The root cause here is synchronous image processing within request handlers. Remediations are to offload CPU work to worker threads, native async libraries (libvips/sharp), external processing services, or horizontal scaling with a processing queue to limit concurrency. Wrapping CPU work in setImmediate does not remove the CPU work, it only yields briefly; it does not make the work non-blocking.

Suggested Fixes

Diagnostic step 1
Replace synchronous image processing with an asynchronous native library such as sharp (libvips-backed) which performs heavy work off the main thread. Test memory and CPU usage under load before rolling out.
Diagnostic step 2
If library replacement is not immediately feasible, offload processing to worker_threads or a pool of workers using the worker_threads API, or use a small pool of child_process workers. Implement a bounded queue so you limit concurrent CPU tasks (e.g., maxWorkers = number of vCPUs).
Diagnostic step 3
Consider moving uploads to temporary storage (object store or shared volume) and enqueue a background job (RabbitMQ, BullMQ, or simple Redis queue) that a separate set of worker processes consumes; this decouples request latency from processing time. Return an immediate 202 Accepted with a job ID to the client if appropriate.

Continue troubleshooting

Matched from the title, description and tags