Skip to content

bun server.ts consumes ~8% CPU permanently while idle #97

Description

@hernanparra

Summary

The admin-panel container's main process (bun server.ts) holds a steady ~8% of one CPU core even with no traffic. Diagnosis points to an internal 1-second telemetry/self-monitoring timer, not to deployment, networking, or the healthcheck.

Environment

  • Runtime: Bun (bun run startbun server.ts)
  • Container: admin-panel (LibreChat), rootless Podman
  • Host: Fedora / RHEL-based, cgroups v2
  • Observed: idle (no user traffic)

Symptom

podman top shows the server process holding a constant CPU average with no incoming requests:

USER   PID   PPID   %CPU    ELAPSED             ARGS
bun    1     0      0.000   1h11m21s            bun run start
bun    3     1      7.848   1h11m21s (5m36s CPU) bun server.ts

CPU time accounting confirms it is continuous, not a stale average:
5m36s CPU / 1h11m21s elapsed ≈ 7.8% — the process has been burning ~8% of a core since startup.

Investigation

All diagnostics were done by reading /proc/<pid>/ — no strace/perf needed.

1. Current syscall — process sits in the event loop:

Sampling /proc/<pid>/syscall returns syscall 441 (epoll_pwait2) almost every time, occasionally caught in running. This is the classic signature of an event loop being woken periodically by a timer — not file-watcher polling (which would show statx/read floods) and not CPU-bound work.

2. File descriptors — three timers, one relevant, plus a self-memory read:

4  -> anon_inode:[eventpoll]     # epoll instance
5  -> anon_inode:[timerfd]       # disarmed
6  -> anon_inode:[eventfd]       # libuv loop wakeup (normal)
7  -> anon_inode:[timerfd]       # disarmed
8  -> anon_inode:[timerfd]       # ACTIVE
10 -> /proc/3/statm              # persistent fd to the process's OWN memory stats
11 -> socket:[...]               # single socket

The persistent open fd to /proc/3/statm (PID 3 = bun server.ts itself, inside the container namespace) indicates the process periodically reads its own memory usage → self-monitoring / telemetry pattern.

3. Timer period — exactly 1 second:

fdinfo for the active timer (fd 8):

it_value:    (0, 726404510)   # next fire in ~726 ms
it_interval: (1, 0)           # re-arms every 1.000 s, indefinitely

The other two timerfds (fd 5, fd 7) are disarmed (it_interval: (0, 0)).

Root cause

A setInterval(..., 1000)-style loop in the application wakes the event loop once per second (~86,400 times/day) and reads process memory via /proc/self/statm. The statm read itself is cheap; the ~8% cost comes from the work hanging off each tick — likely metric serialization, object allocation (GC pressure), and/or pushing stats to a live dashboard. This matches an admin panel that surfaces real-time metrics.

Ruled out:

  • Healthcheck — that's a separate transient bun -e "fetch(...)" process, not PID 3.
  • File watcher (--watch/--hot) — no disk-polling syscalls; only one active timer.
  • Reconnection loop — a single, stable socket; no cycling connection attempts.

Impact / workaround

Purely application-level; deployment is unaffected. As an interim mitigation the container can be capped (e.g. --cpus 0.5) to bound the impact without code changes, but the real fix is the interval.

How to reproduce the diagnosis

# Find the host PID of the server process
podman top admin-panel pid hpid args     # take hpid of `bun server.ts`

# Confirm event loop + timer signature
ls -l /proc/<hpid>/fd/                     # look for active timerfd
grep -H '' /proc/<hpid>/fdinfo/<timerfd>   # read it_interval

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions