---
title: "Practical Solutions for Node.js Single-Thread Performance Bottlenecks"
description: "While extending Uptime Kuma with load balancing and failover, I hit Node.js single-thread limits. Here's how Worker Threads, Cluster, and Bun let me actually saturate multi-core performance."
canonical_url: "https://blog.markkulab.net/en/post/nodejs-worker-threads-bun-performance"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/en/author/mark-ku"
site: "Mark Ku's Tech Notes"
date_published: "2025-12-30 01:01:01 +0800"
category: "Backend"
tags: ["nodejs", "bun", "worker threads", "cluster", "performance", "uptime kuma", "monitoring"]
language: "en"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "when reusing or quoting, credit the author and link back to the original"
---

# Practical Solutions for Node.js Single-Thread Performance Bottlenecks

## Preface

While extending **Uptime Kuma** with load balancing and node failover features, I unexpectedly hit a Node.js performance bottleneck:

The same code that ran **1,000 monitors** smoothly on my home machine started to **stutter at around 800 monitors per node** when running on a **13-year-old test machine**.

That forced me to step back and ask: did I write something bad, or is this a Node.js limitation?

---


## Why does it start stuttering at 800 monitors?

Uptime Kuma is fundamentally a high-frequency polling system: every monitored item periodically sends a request, judges success or failure, and updates state. There's now an extra layer on top.

On newer CPUs these monitors can hold up, but on that 13-year-old test machine, **at around 800 monitors** the following symptoms appeared:

- The admin UI noticeably "lags for a moment" on operations
- Scheduled tasks fire later than configured
- Overall CPU usage approaches single-core saturation, but other cores sit idle

### Before the root cause: a quick word on the JS event loop

JavaScript itself is single-threaded — all synchronous code must execute in order on the call stack. Once the main thread runs a long synchronous task (like heavy computation or an infinite loop), the entire thread is blocked and other tasks can't cut in.

In contrast, operations that need to wait — like `setTimeout` or HTTP requests — don't directly occupy the call stack. The runtime hands them off to lower-level APIs (libuv); when the task completes, the corresponding callback is queued, and the event loop picks it up and runs it when the call stack is idle.

That's why the event loop lets JavaScript, despite having only one main thread, schedule large amounts of I/O concurrently via async — without the program halting because it's waiting on external resources.

### The root cause
When the number of monitors is large and polling frequency is high, **a pile of "needs-to-recompute" logic stacks up on the same event loop**. These computations aren't waiting on I/O — they really do consume CPU. Over time, they naturally become a performance bottleneck.

So without upgrading the single-core CPU, here are the practical approaches I've collected to lift performance:

## Solution 1: Lower the polling frequency (adjust interval)


In a high-frequency polling architecture, many "complex schedules" are actually driven by polling. Lowering the polling frequency directly reduces how often CPU-bound logic fires, easing event-loop pressure.

Take Uptime Kuma: each monitor is a polling task. With 1,000 monitors and a 60-second interval, every minute you have 1,000 requests, success/failure judgments, status updates, and dashboard refreshes. These actions consume CPU. If the interval is too short, a swarm of "complex schedules" gets crammed into the same event loop in the same window. Stretching the polling interval reasonably (e.g., from 15s to 30s, or pushing less-critical monitors to longer intervals) directly cuts how often this CPU-bound logic fires, dropping event-loop pressure and reducing the lag.


---

## Solution 2: Switch to multi-node (horizontal scaling of Uptime Kuma)

When single-machine resources are limited, distributing monitors across multiple nodes is more robust. Each node only handles a portion of monitors, with state unified through shared storage (DB/Redis), and a reverse proxy or task allocation layer in front.

Reference: I have a write-up on multi-node / Cluster approach here as further reading: [Uptime Kuma Cluster Implementation Notes](https://blog.markkulab.net/implement-uptime-kuma-cluster-vibe-coding/)

### Implementation points

- Shared storage: database/Redis as a single source of truth — avoid keeping state only in node memory.
- Task sharding: use tags, hashing, or queues to assign monitors to different nodes; avoid duplicate monitoring.
- Health checks and takeover: when a node fails, tasks should be taken over automatically by other nodes (Failover).
- Observability: centralized logs, metrics, and alerts for ops and faster issue localization.

### Effects

- Single-machine pressure drops significantly; overall throughput and availability improve.
- Better fault isolation — a single node's problem won't drag down the whole service.

---

## Solution 3: Use Bun as a high-performance Runtime

### What is Bun?
While discussing Uptime Kuma with a colleague, he suggested I try Bun, so I dug in. Bun is essentially a high-performance JavaScript Runtime, with traits like:

- JIT / runtime designed for performance — both startup and execution efficiency are optimized
- Built-in bundler, test tools, and package manager (`bun install`)
- A degree of Node.js API compatibility, but not 100% (worth flagging)

---

## Solution 4: Hand "complex schedules" to Worker Threads

> Note: this section is my current research log — not yet implemented in the project. The content focuses on the approach and concept, not a finished solution.

The approach I've researched is **targeting the "complex schedules" themselves** — extracting the most CPU-hungry block and handing it to `worker_threads`.

### Things suited for Worker Threads

- Recomputing each node's **weight and health** based on monitoring results
- Running **complex rule judgments** (e.g., multi-condition failover strategies)
- **Batch analysis / sorting / statistics** over many monitors

Conceptually: the main thread is responsible for:

- Receiving monitoring results
- Queueing / dispatching tasks to workers
- Receiving the worker's computed results and updating state

The truly heavy logic moves into the worker file.

### Code example

A simplified scaffold (illustrative, not complete):

```javascript
// main.js
const { Worker } = require("worker_threads");

function recalcLoadBalancing(monitors) {
  return new Promise((resolve, reject) => {
    const worker = new Worker("./recalc-worker.js", {
      workerData: { monitors },
    });

    worker.on("message", (result) => resolve(result));
    worker.on("error", reject);
    worker.on("exit", (code) => {
      if (code !== 0) reject(new Error(`Worker exited: ${code}`));
    });
  });
}
```

```javascript
// recalc-worker.js
const { parentPort, workerData } = require("worker_threads");

function heavyRecalc(monitors) {
  // Run the complex, CPU-bound algorithm here
  // Return new node weights / sort results / etc.
  return { /* ... */ };
}

const result = heavyRecalc(workerData.monitors);
parentPort.postMessage(result);
```

The result:

- The main thread is no longer blocked by "computing load for 800 monitors at once"
- Even on old CPUs, UI lag is noticeably better
- To use more cores, just spin up more workers (with sensible limits)

---

## Solution 5: Cluster / multiple Node processes to saturate multi-core

> Note: this section covers research direction and feasibility analysis — not yet implemented in the live service.

If your service is a **multi-user, multi-request Web API**, beyond extracting complex schedules you can also use **Cluster / multi-process** to spread the load.

The idea is similar:

- Use `cluster` or PM2 to run the same Node.js app as multiple processes
- For a 4-core CPU, run 4 worker processes
- Place a reverse proxy in front (Nginx / HAProxy / Traefik) for load balancing

### Cluster example

```javascript
const cluster = require("cluster");
const os = require("os");

if (cluster.isMaster) {
  const numCPUs = os.cpus().length;
  console.log(`Master process ${process.pid} is running`);
  console.log(`Forking ${numCPUs} workers...`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on("exit", (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died, restarting...`);
    cluster.fork();
  });
} else {
  // Worker process — runs the actual application logic
  require("./app.js");
  console.log(`Worker ${process.pid} started`);
}
```

---

## Summary

| Approach | When to use | Pros | Cons |
|------|----------|------|------|
| Lower polling frequency (interval) | Pressure from high-frequency polling | Reduces complex-schedule frequency, lowers main-loop load | Slower reaction time, may miss brief outages |
| Multi-node (horizontal scale) | Single-machine resources limited, need to scale | Distributes load, fault isolation, easy to scale out | Cross-node sync and config complexity rises |
| Bun | New scripts, standalone services | Fast execution, fast startup | Compatibility isn't 100% yet |
| Worker Threads | CPU-bound complex schedules | Doesn't block main thread, can use multi-core | Need to handle data serialization |
| Cluster | Multi-request Web services | Multi-process load distribution, high fault tolerance | State requires shared storage |

If you're also debugging Node.js performance issues and stuck on an old machine, give the approaches above a try.

---

## About this article and its author

Originally published on [Mark Ku's Tech Notes](https://blog.markkulab.net/en/post/nodejs-worker-threads-bun-performance)

License: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — when reusing or quoting, credit the author and link back to the original

### About the author

**[Mark Ku](https://blog.markkulab.net/en/author/mark-ku)** — Software Solution Provider

- 10+ years senior software engineer, now an AI Builder
- Focused on large-platform architecture — North-American e-commerce, AI SaaS subscription billing
- Combining AI Agents and automation to build evolvable product foundations

### Free tools built by the author

All of these are free to use:

- [Free PDF Sign Tool](https://blog.markkulab.net/en/tools/pdf-sign): Online PDF sign tool — draw, type, or upload a signature, then drag, resize, and download. Everything runs in your browser; nothing is uploaded.
- [VS Code Refactory](https://blog.markkulab.net/en/tools/refactory): Refactory is a VS Code refactoring extension: 34 actions plus a 37-rule code-smell inspection layer with a Code Health dashboard, across 18 languages, backed by 534 tests. It learns your repo's conventions: where interfaces live, where DI is registered, whether 'use client' belongs. It ranks files by git churn × complexity so you know what to fix first, and hands any smell to the Claude Code already on your machine. Free to use, and your source never leaves your computer.
- [DB-Kit Database Manager](https://blog.markkulab.net/en/tools/db-kit): DB-Kit is a lightweight, cross-platform database manager built with Tauri + Rust + React. Manage MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, SQLite, MongoDB, Redis, Kafka, Elasticsearch and RabbitMQ from one consistent interface: passwords encrypted in the OS keychain, SSH tunnels, full CRUD, a visual query builder, stacked multi-statement result sets, cross-connection data transfer and compare/sync, Excel / CSV import & export, visualized execution plans, ER diagrams, scheduled backups, SQL stress testing with p50–p99 latency percentiles, a 15-rule SQL review engine, Kafka message browsing with monitoring & alerts, a bilingual UI (Traditional Chinese / English), a built-in AI assistant (natural-language SQL, AI review and tuning advice) and the dbk CLI. Free and open source (MIT), with installers for Windows, macOS and Linux.
- [VS Code Super Mermaid](https://blog.markkulab.net/en/tools/super-mermaid): Super Mermaid is a VS Code extension for beautiful Mermaid diagrams out of the box: auto-colored live preview, mouse pan & zoom, high-res PNG / SVG export, 21 templates and multiple themes. Free and open source (MIT).
- [React Super Mermaid](https://blog.markkulab.net/en/tools/react-super-mermaid): react-super-mermaid is an open-source React component library: render beautiful Mermaid diagrams with a single <MermaidViewer>, with built-in colorful / sketch themes, pan & zoom, in-diagram search, and high-res SVG / PNG export. Lightweight, SSR-safe, fully typed. Free and open source (MIT).
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/en/tools/jira-super-mermaid): An Atlassian Forge app: write Mermaid syntax directly inside a Jira issue or a Confluence page and get flowcharts, sequence diagrams, state machines and Gantt charts. 11 diagram types, SVG / PNG export, light and dark themes, full CJK support. Runs on Atlassian: your diagrams live in your own site and the app calls no third-party service. Free, coming soon to the Atlassian Marketplace.
- [Mermaid Live Preview](https://blog.markkulab.net/en/tools/mermaid-preview): Write Mermaid in your browser, see it render instantly, and share the whole diagram as a single link. No sign-up, nothing uploaded to a server, and mermaid.live share links work as-is.
- [React Intl Phone Number](https://blog.markkulab.net/en/tools/react-intl-phone-number): react-intl-phone-number is an open-source React component: framework-agnostic and antd-free, with E.164 in/out, a searchable flag / country-code dropdown, configurable validation levels (strict / mobile-strict / loose), themeable CSS, and i18n — phone logic powered by google-libphonenumber. Lightweight and fully typed. Free and open source (MIT).
- [Uptime Kuma Cluster](https://blog.markkulab.net/en/tools/uptime-kuma-cluster): Turn single-node Uptime Kuma into a highly available cluster: OpenResty + Lua smart load balancing, shared MariaDB state, health checks and automatic failover, plus cluster-management REST APIs. One Docker Compose command to start. Free and open source (MIT).
- [Special Education](https://blog.markkulab.net/en/education): Learning materials crafted for special education students

### Daily podcasts

- [Mark's Tech Insights — Daily AI News](https://blog.markkulab.net/en/category/tech-news): Daily curated AI and tech trends. Catch the latest developments via audio summaries — covering AI applications, software architecture, DevOps, and engineering practice. — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/en/category/ai-stock-chat): Every trading day, an AI-analyzed take on the Taiwan stock market, delivered as a two-host conversation covering the session and the next-day outlook. — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml
- [開源好物週報](https://blog.markkulab.net/en/category/open-source-weekly): A weekly two-host pick of free open-source tools surfaced from real Hacker News, GitHub, and Reddit buzz — what pain they solve and the fastest way to get started. — RSS: https://blog.markkulab.net/open-source-weekly/feed.xml

### Newsletter

[Subscribe to the newsletter](https://blog.markkulab.net/en/subscribe) — Be the first to know about new posts. No spam, unsubscribe anytime.
