View : 209

08/05/2026 06:52am

JS2GO EP.41 Advanced Concurrency Patterns in Go and JavaScript

JS2GO EP.41 Advanced Concurrency Patterns in Go and JavaScript

#Worker Pool

#Concurrency

#JavaScript

#Go

#Fan-in Fan-out

In large-scale systems, concurrency is not simply “running multiple tasks at the same time.” True concurrency is the art of designing how tasks are scheduled, coordinated, and passed between execution units so the system remains stable, fast, and resource-efficient.

 

Both JavaScript (Node.js) and Go (Golang) support concurrency—but their architectures differ fundamentally:

LanguageConcurrency Model
JavaScriptEvent Loop + async/await (single-threaded)
GoGoroutines + Channels (lightweight multi-threading)

 

In this EP.41, we dive into four real-world concurrency patterns used in Production environments, with clear comparisons between JavaScript and Go:

✅ Worker Pool
✅ Fan-out / Fan-in
✅ Rate Limiter
✅ Pipeline Optimization

Let’s jump in.

 

1. Worker Pool Pattern 👷‍♀️

 

Why do we need a Worker Pool?

If you spawn too many goroutines, promises, or worker threads, the system may:

  • spike CPU usage
  • run out of memory
  • overload the event loop or OS scheduler
  • degrade performance under high load

 

A Worker Pool forces tasks to run concurrently only up to a controlled limit, ensuring the system stays stable.

 

🔹 Worker Pool in Go (built-in concurrency superpower)

 

package main

import (
	"fmt"
	"sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()
	for j := range jobs {
		fmt.Printf("Worker %d processing job %d\n", id, j)
		results <- j * 2
	}
}

func main() {
	const numWorkers = 3
	jobs := make(chan int, 5)
	results := make(chan int, 5)
	var wg sync.WaitGroup

	for w := 1; w <= numWorkers; w++ {
		wg.Add(1)
		go worker(w, jobs, results, &wg)
	}

	for j := 1; j <= 5; j++ {
		jobs <- j
	}
	close(jobs)

	wg.Wait()
	close(results)

	for r := range results {
		fmt.Println("✅ Result:", r)
	}
}

 

✔ Why Go excels here

  • goroutines are extremely lightweight (~2KB stack)
  • channels synchronize workers without explicit locking
  • true parallel execution on multi-core CPUs
  • stable and predictable under heavy load

 

🔹 Worker Pool in JavaScript (using worker_threads)

 

const { Worker } = require('worker_threads');

const jobs = [1, 2, 3, 4, 5];
const maxWorkers = 3;
let active = 0;

function startWorker(job) {
  return new Promise((resolve) => {
    const worker = new Worker(`
      const { parentPort } = require('worker_threads');
      parentPort.postMessage(${job} * 2);
    `, { eval: true });

    worker.on('message', resolve);
  });
}

async function runPool() {
  const results = [];

  for (const job of jobs) {
    while (active >= maxWorkers)
      await new Promise((r) => setTimeout(r, 10));

    active++;
    startWorker(job).then((res) => {
      results.push(res);
      active--;
    });
  }

  while (results.length < jobs.length)
    await new Promise((r) => setTimeout(r, 10));

  console.log("✅ Results:", results);
}

runPool();

 

⚠️ Limitations of JavaScript Worker Pool

  • worker_threads are heavy compared to goroutines
  • requires manual message-passing
  • CPU parallelism is possible but not ergonomic
  • more code complexity

 

2. Fan-out / Fan-in Pattern 🌪

 

Useful when processing:

  • large data sets
  • batch jobs
  • I/O pipelines
  • multi-stage computations

 

🔹 Fan-out / Fan-in in Go (channels make it natural)

 

func producer(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		for _, n := range nums {
			out <- n
		}
		close(out)
	}()
	return out
}

func worker(id int, in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		for n := range in {
			fmt.Printf("Worker %d processing %d\n", id, n)
			out <- n * 2
		}
		close(out)
	}()
	return out
}

func fanIn(chs ...<-chan int) <-chan int {
	out := make(chan int)
	var wg sync.WaitGroup

	for _, ch := range chs {
		wg.Add(1)
		go func(c <-chan int) {
			defer wg.Done()
			for n := range c {
				out <- n
			}
		}(ch)
	}

	go func() {
		wg.Wait()
		close(out)
	}()

	return out
}

 

✔ Strengths of Go in Fan-out/Fan-in

  • channels behave like queues
  • fan-in aggregation is safe & predictable
  • goroutines allow true parallel work
  • extremely scalable in production

 

🔹 Fan-out / Fan-in in JavaScript

 

async function worker(id, data) {
  await new Promise((r) => setTimeout(r, 100));
  console.log(`Worker ${id} processed ${data}`);
  return data * 2;
}

async function fanOut(data) {
  const promises = data.map((d, i) => worker(i + 1, d));
  return Promise.all(promises);
}

(async () => {
  const results = await fanOut([1, 2, 3, 4, 5]);
  console.log("Results:", results);
})();

 

Observations

  • Excellent for I/O tasks
  • Does not do true CPU parallelism unless using worker_threads
  • Simpler to write but less powerful for CPU workloads

 

3. Rate Limiter Pattern ⏱

 

🔹 In Go (Ticker / Token Bucket)

 

limiter := time.Tick(500 * time.Millisecond)
for i := 1; i <= 5; i++ {
	<-limiter
	fmt.Println("Processing request", i)
}

 

🔹 In JavaScript (delay-based)

 

async function rateLimited(tasks, interval = 500) {
  for (const [i, task] of tasks.entries()) {
    await new Promise((r) => setTimeout(r, interval));
    console.log(`Processing task ${i + 1}`);
    task();
  }
}

 

4. Pipeline Optimization ⚙️

 

Go Optimization Techniques

  • use buffered channels to reduce blocking
  • use sync.Pool to reuse memory allocations
  • minimize copying by using pointers
  • goroutines allow concurrent multi-stage processing

 

JavaScript Optimization Techniques

  • use Streams instead of loading everything into memory
  • use async generators
  • avoid unnecessary object creation
  • offload CPU tasks to workers if needed

 

📌 Final Comparison

 

PatternGoJavaScript
Worker Pool⭐ Native, simple, scalableNeeds worker_threads
Fan-in / Fan-out⭐ Best with channelsPromise-based only
Rate LimiterTicker / token bucketsetTimeout / queues
Pipeline OptimizationTrue parallelismGreat I/O, limited CPU

 

⭐ Conclusion

  • If your system is CPU-bound, concurrency-heavy, or data-pipeline oriented → Go is the superior choice.
  • If your system is I/O-bound, API-driven, real-time web, or frontend related → JavaScript is ideal.

 


 

🚀 Next Episode EP.42

 

We dive deeper into: “Goroutine Pools and Worker Pools in Go & JavaScript” Learn how to control thousands of concurrent tasks, avoid memory leaks, and handle workloads at massive scale.