What would be the best way to manage separate processes in JavaScript

Summary

This incident examines a common architectural failure in JavaScript backends: long‑running device‑polling processes launched from HTTP handlers without a lifecycle management strategy. The system behaved unpredictably because the API attempted to start, track, and stop background processes using ad‑hoc references rather than a structured process manager.

Root Cause

The root cause was treating long‑running device queries as request‑scoped work, which led to:

  • Processes continuing after the HTTP request ended
  • No persistent registry of running tasks
  • No safe way to stop or restart tasks
  • Confusion between workers, threads, and background jobs in Bun/Node
  • Lack of a dedicated orchestration layer for device polling

Why This Happens in Real Systems

Real systems frequently fall into this trap because:

  • HTTP handlers are synchronous by nature, but device polling is not
  • Engineers assume “just spawn a worker” is enough
  • JavaScript lacks built‑in equivalents to goroutines, channels, or supervisors
  • Bun Workers look like a solution but do not provide orchestration, only isolation
  • Without a registry, processes become “ghost tasks” that cannot be controlled

Real-World Impact

This design flaw typically causes:

  • Zombie processes continuing to poll devices after users click “stop”
  • Memory leaks from untracked workers
  • Duplicate polling when users click “start” multiple times
  • Inconsistent device state in Redis and DB
  • Unrecoverable crashes when workers die silently
  • Operational chaos because no one knows what is running

Example or Code (if necessary and relevant)

Below is a minimal example of a task manager pattern using Bun Workers.
This demonstrates the correct separation between HTTP requests and background processes.

// taskManager.js
import { Worker } from "bun";

const tasks = new Map();

export function startTask(id, config) {
  if (tasks.has(id)) return;

  const worker = new Worker("./poller.js", { workerData: config });
  tasks.set(id, worker);

  worker.onmessage = msg => {
    // store in DB or Redis
  };

  worker.onexit = () => {
    tasks.delete(id);
  };
}

export function stopTask(id) {
  const worker = tasks.get(id);
  if (worker) {
    worker.terminate();
    tasks.delete(id);
  }
}
// poller.js
import { parentPort, workerData } from "bun";

async function loop() {
  while (true) {
    const data = await queryDevice(workerData);
    parentPort.postMessage(data);
    await Bun.sleep(1000);
  }
}

loop();

How Senior Engineers Fix It

Experienced engineers solve this by introducing explicit orchestration:

  • A global task registry (in‑memory or Redis)
  • Dedicated worker processes for device polling
  • A supervisor layer that restarts failed tasks
  • Decoupling HTTP from background work
  • Using message passing instead of shared state
  • Persisting task state so restarts are safe
  • Graceful shutdown hooks to terminate workers cleanly

They treat each device poller as a managed service, not a side effect of an API call.

Why Juniors Miss It

Juniors often miss this issue because:

  • They assume “spawn a worker” is the same as “manage a worker”
  • They underestimate the complexity of long‑running processes
  • They treat background jobs like synchronous functions
  • They don’t anticipate stop/restart requirements
  • They lack exposure to supervisors, job queues, or process managers
  • They think Bun Workers behave like Go routines, but they do not

The key misunderstanding is believing that JavaScript automatically manages background processes, when in reality you must build the orchestration layer yourself.

Leave a Comment