Hello World

Be Happy!

Go vs Node.js vs Rails vs Spring — Architecture


Concurrency Architecture Comparison

How Go, Node.js, Ruby on Rails, and Spring Java handle concurrent traffic — and why it matters for performance.

Node.js — Single-threaded Event Loop

┌─────────────────────────────────────────┐
│             Node.js Process              │
├─────────────────────────────────────────┤
│                                         │
│   ┌─────────────────────────────────┐   │
│   │    Single Thread (V8 Engine)    │   │
│   │                                 │   │
│   │   Event Loop (libuv):           │   │
│   │   ┌───────────────────────┐     │   │
│   │   │ 1. Timers             │     │   │
│   │   │ 2. I/O callbacks      │     │   │
│   │   │ 3. Poll (new I/O)     │     │   │
│   │   │ 4. Check (setImmediate)│    │   │
│   │   │ 5. Close callbacks    │     │   │
│   │   └───────────────────────┘     │   │
│   │          ↕                      │   │
│   │   ┌───────────────────────┐     │   │
│   │   │  Thread Pool (libuv)  │     │   │
│   │   │  (4 threads for I/O)  │     │   │
│   │   └───────────────────────┘     │   │
│   └─────────────────────────────────┘   │
│                                         │
│   Non-blocking: handles 1000s of        │
│   connections on 1 thread               │
└─────────────────────────────────────────┘
  • Concurrency: Single-threaded event loop + async I/O
  • Blocking?: CPU-heavy work blocks everything
  • Scaling: Cluster mode (1 process per CPU core)
  • Best for: I/O-heavy, real-time (WebSocket, chat, APIs)

Go (Golang) — Goroutines + M:N Scheduler

┌─────────────────────────────────────────────────┐
│              Go Runtime                          │
├─────────────────────────────────────────────────┤
│                                                 │
│   ┌─────────────────────────────────────────┐   │
│   │         Go Scheduler (GMP model)        │   │
│   │                                         │   │
│   │  G = Goroutine (lightweight, ~2KB stack)│   │
│   │  M = OS Thread                          │   │
│   │  P = Processor (logical CPU)            │   │
│   │                                         │   │
│   │  ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐       │   │
│   │  │ G │ │ G │ │ G │ │ G │ │ G │ ...    │   │
│   │  └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘       │   │
│   │    └──┬───┘     │     └──┬───┘         │   │
│   │       ▼         ▼        ▼              │   │
│   │    ┌─────┐  ┌─────┐  ┌─────┐           │   │
│   │    │  P  │  │  P  │  │  P  │           │   │
│   │    └──┬──┘  └──┬──┘  └──┬──┘           │   │
│   │       ▼        ▼        ▼               │   │
│   │    ┌─────┐  ┌─────┐  ┌─────┐           │   │
│   │    │  M  │  │  M  │  │  M  │           │   │
│   │    │(OS) │  │(OS) │  │(OS) │           │   │
│   │    └─────┘  └─────┘  └─────┘           │   │
│   └─────────────────────────────────────────┘   │
│                                                 │
│   100K+ goroutines on a few OS threads          │
│   Preemptive scheduling (no starvation)         │
└─────────────────────────────────────────────────┘
  • Concurrency: M:N scheduling — millions of goroutines on few OS threads
  • Blocking?: Goroutines block individually, scheduler moves others
  • Scaling: Naturally uses all CPU cores
  • Best for: High-concurrency services, microservices, infrastructure

Ruby on Rails — Multi-process + GVL

┌─────────────────────────────────────────────────────┐
│            Ruby on Rails (Puma server)               │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌─────────────────────────────────────────────┐    │
│  │  Master Process                             │    │
│  │  ┌───────────┐ ┌───────────┐ ┌───────────┐ │    │
│  │  │ Worker 1  │ │ Worker 2  │ │ Worker 3  │ │    │
│  │  │           │ │           │ │           │ │    │
│  │  │ ┌Thread┐  │ │ ┌Thread┐  │ │ ┌Thread┐  │ │    │
│  │  │ │ Req  │  │ │ │ Req  │  │ │ │ Req  │  │ │    │
│  │  │ └──────┘  │ │ └──────┘  │ │ └──────┘  │ │    │
│  │  │ ┌Thread┐  │ │ ┌Thread┐  │ │ ┌Thread┐  │ │    │
│  │  │ │ Req  │  │ │ │ Req  │  │ │ │ Req  │  │ │    │
│  │  │ └──────┘  │ │ └──────┘  │ │ └──────┘  │ │    │
│  │  │  5 threads│ │  5 threads│ │  5 threads│ │    │
│  │  └───────────┘ └───────────┘ └───────────┘ │    │
│  └─────────────────────────────────────────────┘    │
│                                                     │
│  GVL: only 1 thread runs Ruby at a time per process │
│  Total capacity: workers × threads (e.g. 3×5 = 15) │
└─────────────────────────────────────────────────────┘
  • Concurrency: Multi-process (Puma workers) + threads within each
  • Blocking?: GVL limits true parallelism per process; I/O releases it
  • Scaling: Add more processes (costs ~100-300MB each)
  • Best for: Rapid development, CRUD apps, startups

Spring Java — Thread Pool + Virtual Threads

┌─────────────────────────────────────────────────────┐
│            Spring Boot (Tomcat/Netty)                │
├─────────────────────────────────────────────────────┤
│                                                     │
│  Classic (Spring MVC + Tomcat):                     │
│  ┌─────────────────────────────────────────────┐    │
│  │  Thread Pool (200 threads default)          │    │
│  │  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐      │    │
│  │  │Thread│ │Thread│ │Thread│ │Thread│ ...   │    │
│  │  │ Req1 │ │ Req2 │ │ Req3 │ │ Req4 │      │    │
│  │  └──────┘ └──────┘ └──────┘ └──────┘      │    │
│  │  Each thread: ~1MB stack                    │    │
│  │  200 threads = 200 concurrent requests      │    │
│  └─────────────────────────────────────────────┘    │
│                                                     │
│  Modern (Java 21+ Virtual Threads / Loom):         │
│  ┌─────────────────────────────────────────────┐    │
│  │  Millions of Virtual Threads possible       │    │
│  │  ┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐        │    │
│  │  │VT││VT││VT││VT││VT││VT││VT││VT│ ...    │    │
│  │  └┬─┘└┬─┘└┬─┘└┬─┘└┬─┘└┬─┘└┬─┘└┬─┘       │    │
│  │   └───┴───┴┬──┘   └───┴┬──┘              │    │
│  │            ▼            ▼                  │    │
│  │     ┌──────────┐ ┌──────────┐             │    │
│  │     │ OS Thread│ │ OS Thread│  (few)      │    │
│  │     └──────────┘ └──────────┘             │    │
│  └─────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────┘
  • Concurrency: Thread pool (classic) or Virtual Threads (Java 21+)
  • Blocking?: Classic threads block; Virtual Threads park cheaply
  • Scaling: Vertical (RAM/threads) + horizontal
  • Best for: Enterprise, complex business logic, large teams

Traffic Performance Comparison

MetricGoNode.jsSpring JavaRailsRequests/sec (JSON API)~300K–500K~50K–100K~100K–200K~5K–15KLatency (p99)Very lowLowLow-mediumMedium-highMemory per connection~2KB~few KB~1MB (classic) / ~few KB (VT)~10-50MB (process)Max concurrent connectionsMillions100K+10K (classic) / Millions (VT)Hundreds–thousandsCold start~5ms~100ms~2-5 sec~5-10 secCPU-heavy tasksExcellentPoor (blocks loop)ExcellentPoor (GVL)I/O-heavy tasksExcellentExcellentGood-ExcellentModerate

Throughput Visualization

Requests/sec (same hardware):

Go:          ████████████████████████████████████  ~500K
Spring(VT):  █████████████████████                 ~200K
Node.js:     ████████████                          ~100K
Rails:       ██                                    ~10K

Memory per connection:

Go:          █          2KB (goroutine)
Node.js:     █          few KB (event)
Spring(VT):  █          few KB (virtual thread)
Spring:      ██████     1MB (OS thread)
Rails:       ██████████████████  10-50MB (process)

Key Terms Glossary

KeywordFull NameUsed InMeaningGVLGlobal VM LockRubyOnly 1 thread runs Ruby at a time per processGILGlobal Interpreter LockPythonSame concept as GVL for PythonEvent Loop—Node.jsSingle thread cycles through I/O eventslibuv—Node.jsC library implementing event loop + thread poolGoroutine—GoLightweight green thread (~2KB)GMPGoroutine, Machine, ProcessorGoGo's scheduler modelVirtual ThreadProject LoomJava 21+Lightweight thread on JVM (like goroutine)M:N scheduling—Go, Java VTM green threads on N OS threadsV8—Node.jsGoogle's JS engine (compiles to machine code)JITJust-In-TimeJava, Node.jsCompiles to native code at runtimeAOTAhead-Of-TimeGoCompiles before running → fast cold startPuma—RailsMulti-process + multi-thread web serverTomcat—SpringThread-pool based web serverPreemptive—Go, OS threadsScheduler can interrupt running tasksCooperative—Node.jsTask must yield control voluntarily

Why GVL Matters

Ruby (GVL) — 4 CPU cores, 1 process, 5 threads:
  Effective CPU usage: 1 core (GVL blocks others)
  Fix: 4 processes → 4 cores used
  Cost: 4 × 200MB = 800MB RAM

Go (no lock) — 4 CPU cores, 1 process, 1000 goroutines:
  Effective CPU usage: 4 cores (scheduler distributes)
  Cost: 1000 × 2KB = 2MB RAM

  → 400× more memory-efficient for same parallelism

When to Use What

Use CaseBest ChoiceWhyHigh-traffic APIGoRaw performance, low memory, simple deployReal-time (WebSocket, chat)Node.js or GoEvent-driven, many idle connectionsEnterprise / complex logicSpring JavaEcosystem, type safety, large teamsStartup MVP / rapid devRailsDeveloper speed, conventionsCPU-intensive processingGo or JavaTrue parallelism across all cores10K+ concurrent connectionsGoGoroutines scale with minimal overhead

The Core Insight

The real difference is how they handle waiting (DB queries, API calls, network I/O):

  • Go: spawns goroutine (2KB), parks it when waiting, resumes instantly
  • Node.js: registers callback, moves on, comes back when done
  • Java (classic): blocks entire OS thread (1MB) while waiting
  • Java (Virtual Threads): like Go — parks cheaply
  • Rails: blocks thread + limited by GVL, needs more processes = more RAM

For high-traffic services, Go and Node.js architectures are fundamentally better suited because waiting doesn't waste expensive resources.

#golang (1) #nodejs (1) #ruby on rails (1) #spring (1) #java (3) #gvl (2) #event loop (2) #goroutine (2) #concurrency (2) #performance (1)
List