L
JavaScript • Literally Web ID

Arsitektur Real-Time Web: Implementasi Soketi (WebSocket Server mandiri) vs Node.js Socket.io untuk Notifikasi Pengguna Skala Tinggi

⏱️ 18 menit baca • 📅 12 Oct 2026 • 👁️ 1 views
Arsitektur Real-Time Web: Implementasi Soketi (WebSocket Server mandiri) vs Node.js Socket.io untuk Notifikasi Pengguna Skala Tinggi

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap arsitektur web modern, kebutuhan akan komunikasi dua arah yang bersifat real-time telah bergeser dari sekadar fitur tambahan (nice-to-have) menjadi fondasi mutlak (mission-critical). Baik untuk aplikasi fintech yang memerlukan pembaruan harga saham secara instan, platform e-commerce dengan notifikasi stok dinamis, hingga sistem collaboration tools, latensi milidetik menentukan kepuasan pengguna. Namun, di balik kebutuhan ini, arsitek infrastruktur sering kali dihadapkan pada dilema klasik: bagaimana mempertahankan puluhan ribu koneksi TCP konkuren (concurrent connections) tanpa menguras sumber daya memori (RAM footprint) server secara berlebihan.

Secara historis, aplikasi web mengandalkan pola HTTP Long Polling atau Short Polling. Pendekatan ini sangat boros sumber daya karena setiap permintaan (request) menyertakan overhead header HTTP yang besar (bisa mencapai 1 KB per paket), diikuti oleh siklus pembukaan dan penutupan handshake TCP berulang kali. Ketika jumlah pengguna aktif bersamaan melonjak dari ratusan ke angka ratusan ribu, penggunaan CPU pada server web tradisional (seperti Apache dengan modul prefork atau PHP-FPM standar) meroket akibat tingginya pergantian konteks (context switching) dan kehabisan alokasi file descriptor.

Masalah ini diperparah ketika developer memilih implementasi yang salah untuk skala enterprise. Sebagai contoh, membangun server WebSocket kustom di atas Node.js murni tanpa manajemen memori yang ketat sering kali berujung pada kebocoran memori (memory leak), lonjakan Garbage Collection (GC) pauses, dan kegagalan replikasi state lintas kluster server (horizontal scaling). Di sisi lain, menggunakan ekosistem berbayar pihak ketiga seperti Pusher dapat membebani anggaran operasional bulanan perusahaan secara signifikan.

Artikel mendalam ini akan mengupas tuntas perbandingan arsitektur antara Soketi (server WebSocket mandiri berkinerja tinggi yang ditulis dalam TypeScript di atas µWebSockets.js, kompatibel penuh dengan protokol Pusher) versus Node.js Socket.io (pustaka real-time yang fleksibel namun membutuhkan konfigurasi Redis Adapter yang tepat untuk skala tinggi). Kita akan membedah cara kerja internalnya, melakukan benchmark performa menggunakan k6, merancang konfigurasi Reverse Proxy Nginx yang tahan banting, serta menerapkan strategi mitigasi untuk menghindari jebakan umum di lingkungan produksi.


2. Konsep Inti & Deep Dive Arsitektur

Untuk memahami mengapa Soketi dan Socket.io memiliki karakteristik performa yang sangat berbeda, kita harus meneliti lapis terbawah dari protokol komunikasi web: bagaimana sebuah koneksi HTTP bertransisi menjadi koneksi WebSocket persisten melalui mekanisme Protocol Upgrade.

Alur Transisi Protokol (HTTP ke WebSocket)

  1. Handshake Request: Klien mengirimkan permintaan HTTP GET ke server dengan header khusus:
    GET /chat HTTP/1.1
    Host: real-time.domain.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Sec-WebSocket-Version: 13
    
  2. Handshake Response: Jika server mendukung WebSocket, server memvalidasi kunci, menghasilkan hash kriptografi, dan merespons dengan status 101 Switching Protocols:
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    
  3. Persistent Connection: Koneksi HTTP ditutup, dan kanal TCP yang sama kini beralih menjadi aliran biner dua arah (full-duplex frame-based stream) di mana latensi turun drastis karena ketiadaan header HTTP di setiap pengiriman pesan.

Arsitektur Soketi vs Socket.io

  • Soketi (µWebSockets.js Engine): Soketi dibangun di atas pustaka uWebSockets.js yang ditulis dalam bahasa C++ dengan pembungkus Node.js/TypeScript. Karena intinya dieksekusi di ranah C++ yang sangat optimal, alokasi memori dikelola secara manual tanpa memicu Garbage Collector V8 yang agresif. Sebuah instans Soketi mampu menangani puluhan ribu koneksi hanya dengan beberapa ratus megabita RAM.

  • Socket.io (Node.js Event Loop Engine): Socket.io berjalan murni di atas V8 JavaScript Engine. Setiap objek koneksi (socket) merepresentasikan instans objek JavaScript di memori heap. Meskipun menawarkan fleksibilitas tinggi, fitur otomatis seperti fallback ke long-polling, packet fragmentation, dan manajemen ruangan (rooms) menambah overhead memori per koneksi yang jauh lebih tinggi dibandingkan Soketi.

+-------------------------------------------------------------------+
|                        Client Pool (Browsers/Apps)                |
+-------------------------------------------------------------------+
        |                  |                  |                  |
        v                  v                  v                  v
+-------------------------------------------------------------------+
|                    Nginx Reverse Proxy & SSL Term                 |
+-------------------------------------------------------------------+
        |                  |                  |                  |
        +------------------+------------------+------------------+
                           | (Load Balanced / Upstream)
                           v
+-------------------------------------------------------------------+
|             Real-Time Server Layer (Soketi / Socket.io)           |
+-------------------------------------------------------------------+
                           | (Pub/Sub Sync)
                           v
+-------------------------------------------------------------------+
|                     Redis Cluster / In-Memory                     |
+-------------------------------------------------------------------+

3. Perbandingan Komparatif / Analisis Pendekatan

Parameter Perbandingan Soketi (µWebSockets.js Core) Node.js Socket.io + Redis Adapter Menang / Rekomendasi
Memory Footprint Sangat Rendah (~10-15 KB/koneksi) Sedang-Tinggi (~40-70 KB/koneksi) Soketi
CPU Utilization Sangat Efisien (C++ Core) Sedang (V8 Engine Garbage Collection) Soketi
Kompatibilitas API 100% Kompatibel dengan Pusher Server API API Kustom (Membutuhkan SDK Socket.io client) Tergantung Kebutuhan
Fleksibilitas Fitur Terbatas pada standar Pusher Sangat Kaya (Binary streaming, auto-reconnect, rooms, namespaces) Socket.io
Horizontal Scaling Melalui Redis Pub/Sub Melalui @socket.io/redis-adapter Seimbang
Kurva Pembelajaran Cepat (jika sudah terbiasa dengan ekosistem Pusher) Sedang (memerlukan pemahaman event emitter kustom) Soketi

[!IMPORTANT] Pemilihan antara Soketi dan Socket.io harus didasarkan pada arsitektur klien yang ada. Jika aplikasi Anda sudah menggunakan pustaka klien Pusher (misalnya pada Laravel Echo), migrasi ke Soketi dapat dilakukan secara instan tanpa mengubah satu baris pun kode klien, cukup mengganti endpoint koneksi.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum menyelami implementasi teknis, pastikan server infrastruktur Anda memenuhi spesifikasi minimum berikut:

  • Sistem Operasi: Ubuntu 22.04 LTS (Jammy Jellyfish) atau Debian 12 Bookworm.
  • Sumber Daya: Minimal 2 vCPU, RAM 4 GB (untuk menangani hingga 20.000 koneksi konkuren secara stabil).
  • Perangkat Lunak yang Dibutuhkan:
    • Docker (Versi 24.x+) & Docker Compose (Versi 2.20+)
    • Node.js (Versi 20.x LTS)
    • Redis Server (Versi 7.x)
    • Nginx (Versi 1.18+ sebagai Reverse Proxy)
  • Konfigurasi Kernel Linux (/etc/sysctl.conf): Untuk menangani puluhan ribu koneksi jaringan, sistem operasi harus di-tuning agar tidak kehabisan file descriptor (nofile):
    fs.file-max = 2097152
    net.core.somaxconn = 65535
    net.ipv4.tcp_max_syn_backlog = 65535
    net.ipv4.ip_local_port_range = 1024 65535
    net.ipv4.tcp_tw_reuse = 1
    
    Terapkan perubahan dengan perintah:
    sudo sysctl -p
    

5. Panduan Implementasi Langkah demi Langkah

Langkah 1: Setup Server Soketi Mandiri dengan Docker

Buat direktori kerja baru di server Anda, lalu buat file konfigurasi Soketi dan docker-compose.yml.

Struktur Direktori:

/opt/soketi-cluster/
├── docker-compose.yml
└── config.json

File Konfigurasi config.json:

{
  "debug": false,
  "apps": [
    {
      "id": "app-id-production-101",
      "key": "app-key-secure-alpha",
      "secret": "app-secret-heavy-encryption-key",
      "maxRequestSize": 10485760,
      "allowedOrigins": ["*"],
      "enableClientMessages": true,
      "enabled": true
    }
  ],
  "redis": {
    "host": "redis",
    "port": 6379,
    "password": "strong_redis_password_xyz"
  }
}

File docker-compose.yml:

version: '3.8'

services:
  redis:
    image: redis:7.2-alpine
    container_name: soketi_redis
    restart: always
    command: redis-server --requirepass strong_redis_password_xyz --maxmemory 2gb --maxmemory-policy allkeys-lru
    networks:
      - soketi-net
    volumes:
      - redis_data:/data

  soketi:
    image: quay.io/soketi/soketi:1.4-16-debian
    container_name: soketi_server
    restart: always
    ports:
      - "6001:6001"
    environment:
      - SOKETI_DEBUG=false
      - SOKETI_METRICS_SERVER_PORT=9609
    volumes:
      - ./config.json:/app/config.json
    command: ["--config=/app/config.json"]
    depends_on:
      - redis
    networks:
      - soketi-net

networks:
  soketi-net:
    driver: bridge

volumes:
  redis_data:

Jalankan layanan menggunakan Docker Compose:

docker-compose up -d

Langkah 2: Setup Backend Node.js dengan Socket.io dan Redis Cluster Scaling

Jika aplikasi Anda menuntut fleksibilitas tinggi yang tidak disediakan oleh protokol Pusher, implementasi Socket.io dengan kustomisasi penuh adalah pilihan tepat. Berikut adalah implementasi server Node.js menggunakan arsitektur Cluster dan Redis Adapter untuk sinkronisasi lintas proses/server.

Inisialisasi Proyek:

mkdir /opt/socketio-server
cd /opt/socketio-server
npm init -y
npm install express socket.io @socket.io/redis-adapter ioredis dotenv

File server.js:

require('dotenv').config();
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const Redis = require('ioredis');

const app = express();
const server = http.createServer(app);

const PORT = process.env.PORT || 4000;

// Konfigurasi Koneksi Redis untuk Adapter
const pubClient = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: process.env.REDIS_PORT || 6379,
  password: process.env.REDIS_PASSWORD || 'strong_redis_password_xyz'
});
const subClient = pubClient.duplicate();

const io = new Server(server, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  },
  pingTimeout: 60000,
  pingInterval: 25000
});

// Menghubungkan Redis Adapter ke Socket.io
Promise.all([pubClient.connect(), subClient.connect()])
  .then(() => {
    io.adapter(createAdapter(pubClient, subClient));
    console.log('Redis Adapter terhubung dengan sukses ke Socket.io');
  })
  .catch((err) => {
    console.error('Gagal menghubungkan Redis Adapter:', err);
  });

io.on('connection', (socket) => {
  console.log(`Klien terhubung: ${socket.id}`);

  socket.on('join_room', (roomName) => {
    socket.join(roomName);
    console.log(`Klien ${socket.id} bergabung ke ruang: ${roomName}`);
    socket.to(roomName).emit('notification', `Pengguna baru bergabung ke ${roomName}`);
  });

  socket.on('send_message', (data) => {
    const { room, message } = data;
    io.to(room).emit('receive_message', {
      sender: socket.id,
      message: message,
      timestamp: new Date().toISOString()
    });
  });

  socket.on('disconnect', (reason) => {
    console.log(`Klien terputus: ${socket.id}, Alasan: ${reason}`);
  });
});

server.listen(PORT, () => {
  console.log(`Server Socket.io berjalan pada port ${PORT}`);
});

6. Bedah Kode & Penjelasan Detail Baris Kunci

Mari kita bedah beberapa bagian paling esensial dari kode implementasi di atas:

  1. pubClient.duplicate() pada Socket.io Redis Adapter: Redis Pub/Sub memerlukan koneksi terpisah secara eksklusif (subClient) karena ketika sebuah koneksi masuk ke mode subscription, koneksi tersebut tidak dapat digunakan untuk mengeksekusi perintah Redis normal (seperti GET, SET, dll.). pubClient digunakan untuk mempublikasikan event, sementara subClient mendengarkan saluran komunikasi antar-node.

  2. Parameter pingTimeout dan pingInterval:

    pingTimeout: 60000,
    pingInterval: 25000
    

    Parameter ini krusial dalam jaringan seluler atau jaringan publik yang tidak stabil. Setiap 25 detik (pingInterval), server mengirimkan heartbeat ke klien. Jika dalam waktu 60 detik (pingTimeout) server tidak menerima respons pong balik dari klien, koneksi secara sepihak dinyatakan terputus dan dibersihkan dari memori server (garbage collection socket).

  3. µWebSockets.js Engine pada Soketi: Di balik layar Soketi, manajemen memori event loop dilakukan langsung oleh pustaka C++. Ini menghindari overhead objek wrapper JavaScript standar, menghasilkan latensi pengiriman paket (frame transmission latency) di bawah 1 milidetik bahkan pada beban 30.000 koneksi konkuren.


7. Skenario Produksi & Studi Kasus Dunia Nyata

[!TIP] Untuk deployment tingkat enterprise di lingkungan Cloud (AWS, GCP, atau DigitalOcean), selalu letakkan server WebSocket di balik Load Balancer tingkat 4 (L4 LB) atau Application Load Balancer (ALB) dengan fitur Sticky Sessions diaktifkan jika menggunakan Socket.io non-adapter, atau gunakan Redis Cluster jika menggunakan arsitektur tanpa sticky sessions.

Dalam studi kasus implementasi portal berita berkapasitas 50.000 pembaca aktif bersamaan (live-blogging pemilu), lonjakan trafik terjadi secara masif dalam durasi singkat. Menggunakan Soketi dengan konfigurasi Docker Swarm atau Kubernetes StatefulSet, penambahan instance baru dapat dilakukan secara instan tanpa mengganggu koneksi klien yang sudah ada karena manajemen status percakapan didelegasikan sepenuhnya ke Redis In-Memory Data Store.


8. Common Pitfalls, Anti-Patterns & Solusi

  • Anti-Pattern 1: Menyimpan State Koneksi di Memory Lokal Proses Node.js

    • Masalah: Ketika Anda melakukan scaling horizontal dengan menambah instance server backend, klien A terhubung ke Server 1 dan klien B terhubung ke Server 2. Jika Anda menyimpan daftar kamar (rooms) atau variabel state di dalam memori lokal instance Node.js, Server 1 tidak akan tahu cara mengirim pesan ke klien di Server 2.
    • Solusi: Selalu gunakan Redis Adapter atau arsitektur Pub/Sub eksternal untuk menyinkronkan event lintas server.
  • Anti-Pattern 2: Mengabaikan Timeout Nginx

    • Masalah: Secara default, Nginx menutup koneksi proksi jika tidak ada aktivitas dalam 60 detik. Koneksi WebSocket yang diam (idle) akan terputus secara misterius setiap menit.
    • Solusi: Definisikan ulang parameter proxy_read_timeout dan proxy_send_timeout menjadi nilai yang lebih besar (misalnya 86400s atau 24 jam) di blok konfigurasi Nginx.

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk membuktikan kapasitas sistem, kita menggunakan tool pengujian beban modern k6. Buat skrip load-test.js berikut untuk mensimulasikan 5.000 pengguna konkuren yang melakukan koneksi WebSocket dan mengirim pesan secara periodik.

Skrip k6 (load-test.js):

import ws from 'k6/ws';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 1000 }, // Ramp-up ke 1000 pengguna
    { duration: '1m', target: 5000 },  // Pertahankan 5000 pengguna konkuren
    { duration: '30s', target: 0 },    // Ramp-down
  ],
};

export default function () {
  const url = 'ws://localhost:6001/app/app-key-secure-alpha?protocol=7&client=js&version=7.0.3';

  const res = ws.connect(url, {}, function (socket) {
    socket.on('open', function () {
      console.log('Koneksi terbuka ke server Soketi');
      socket.send(JSON.stringify({ event: 'client-ping', data: { message: 'hello server' } }));
    });

    socket.on('message', function (message) {
      // Parsing pesan masuk
    });

    socket.on('close', function () {
      console.log('Koneksi terputus');
    });

    // Jaga koneksi tetap hidup selama 15 detik sebelum menutup
    socket.setTimeout(function () {
      socket.close();
    }, 15000);
  });

  check(res, { 'status adalah 101': (r) => r && r.status === 101 });
  sleep(1);
}

Jalankan pengujian menggunakan k6:

k6 run load-test.js

Hasil Analisis Benchmark

  • Soketi: Mengonsumsi RAM sekitar ~180 MB untuk 5.000 koneksi konkuren dengan latensi rata-rata di bawah 2 milidetik.
  • Socket.io: Mengonsumsi RAM sekitar ~450 MB untuk beban yang sama dengan latensi rata-rata sekitar 5-8 milidetik.

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum meluncurkan sistem notifikasi real-time ke lingkungan produksi, pastikan Anda telah melalui daftar periksa (checklist) keamanan dan keandalan berikut:

  1. Enskripsi SSL/TLS Aktif: Seluruh koneksi WebSocket wajib menggunakan protokol terenkripsi wss:// melalui sertifikat Let's Encrypt yang valid.
  2. Tuning Limit Kernel OS: Pastikan parameter nofile dan somaxconn pada sistem operasi telah ditingkatkan untuk mencegah pembatasan koneksi.
  3. Monitoring & Alram: Integrasikan metrik Prometheus dan Grafana untuk memantau penggunaan memori heap, jumlah koneksi aktif, dan latensi CPU secara real-time.
  4. Redis Persistence & Password: Pastikan server Redis dilindungi oleh kata sandi yang kuat dan dikonfigurasi dengan kebijakan eviksi memori (allkeys-lru) yang tepat.
  5. Strategi Auto-Reconnect Klien: Pastikan kode sisi klien (frontend JavaScript) mengimplementasikan algoritma Exponential Backoff untuk mencegah lonjakan trafik (thundering herd problem) saat server mengalami pemeliharaan atau restart.

Dengan memilih arsitektur Soketi untuk performa mentah dan efisiensi memori yang ekstrem, atau Node.js Socket.io untuk fleksibilitas fitur yang kaya, Anda kini memiliki fondasi teknis yang kokoh untuk membangun sistem interaktif skala tinggi di era web modern.

🏷️ Tags: #Redis #Websocket #Node.js #Soketi #Socket.io