Error ECONNREFUSED adalah salah satu musuh bebuyutan paling klasik yang pasti ditemui oleh setiap developer, baik yang baru belajar maupun yang sudah berpengalaman bertahun-tahun. Kehadiran error ini sering kali memicu kepanikan karena menyebabkan seluruh alur data dalam aplikasi terhenti total. Aplikasi frontend tidak bisa mengambil data, microservice gagal berkomunikasi satu sama lain, dan proses background job berhenti berjalan. Meskipun terlihat menakutkan, error ini sebenarnya memiliki pola yang sangat prediktabel. Memahami anatomi dari error ini adalah langkah pertama yang krusial agar kita tidak asal mencoba me-restart server atau memukul meja dalam keputusasaan. Pada artikel ini, kita akan membongkar habis-habisan apa yang terjadi di balik layar saat ECONNREFUSED muncul, menyelidiki penyebab-penyebab umumnya, memberikan langkah-langkah perbaikan yang sistematis, hingga merancang solusi permanen agar error ini tidak kembali menghantui sistem kita di masa depan.

Anatomi pesan error

Ketika error ini muncul, runtime environment (seperti Node.js, Python, atau Go) biasanya akan melempar objek error dengan pesan yang cukup verbose. Berikut adalah contoh kutipan pesan error asli (verbatim) yang sering muncul di lingkungan Node.js ketika mencoba melakukan HTTP Request menggunakan library seperti Axios atau Fetch bawaan:

Error: connect ECONNREFUSED 127.0.0.1:3000
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16)

Jika kita menggunakan utility berbasis TCP seperti curl atau telnet untuk melakukan koneksi manual, pesan error yang muncul mungkin terlihat sedikit berbeda namun memiliki esensi yang sama:

curl: (7) Failed to connect to localhost port 3000: Connection refused

Arti singkat dalam bahasa Indonesia: Pesan ECONNREFUSED secara harfiah berarti "Koneksi ditolak". Ini adalah sinyal dari sistem operasi bahwa upaya untuk membuka jalur komunikasi jaringan (TCP socket) ke alamat IP dan port tertentu telah secara aktif ditolak oleh mesin target.

Kenapa error ini muncul? Untuk memahami mengapa error ini muncul, kita harus melihat bagaimana protokol TCP/IP bekerja di balik layar. Ketika sebuah aplikasi (klien) mencoba terhubung ke API (server), klien akan mengirim paket SYN (synchronize) sebagai bagian dari TCP three-way handshake. Jika server sedang berjalan dan mendengarkan (listening) di port yang diminta, server akan membalas dengan paket SYN-ACK, dan koneksi terbangun.

Namun, dalam kasus ECONNREFUSED, alur kejadian berbeda. Klien mengirim paket SYN. Mesin target (yang alamat IP-nya benar dan reachable) menerima paket tersebut. Namun, karena tidak ada aplikasi yang binding atau mendengarkan di port tujuan, sistem operasi pada mesin target secara otomatis akan membalas dengan paket RST (reset) atau terkadang RST-ACK. Paket RST ini adalah cara sistem operasi mengatakan: "Saya menerima panggilan Anda di alamat ini, tapi tidak ada layanan yang aktif di nomor pintu (port) ini, silakan pergi."

Ini adalah perbedaan fundamental yang sangat penting dipahami. ECONNREFUSED bukanlah timeout. Jika mesin target tidak ada atau firewall memblokir paket secara diam-diam (drop), klien akan menunggu balasan hingga batas waktu habis, yang menghasilkan ETIMEDOUT. Karena ECONNREFUSED membutuhkan balasan paket RST, ini berarti mesin target pasti ada dan bisa dijangkau, tetapi layanan API-nya tidak berjalan atau salah port.

Penyebab umum

Memiliki pemahaman tentang anatomi error hanya membawa kita setengah jalan. Setengah jalan lainnya adalah mengidentifikasi penyebab aktual di lapangan. Berikut adalah 4 root cause paling sering terjadi, diurutkan dari yang paling umum hingga yang lebih kompleks:

  • API Server belum dijalankan atau mengalami crash (Paling Sering): Ini adalah kesalahan paling klasik. Developer frontend atau consumer API lainnya mencoba memanggil endpoint, namun lupa menyalakan server backend-nya terlebih dahulu. Atau, server backend memang baru saja di-restart namun prosesnya gagal atau crash saat booting (misalnya karena kekurangan variabel lingkungan atau error sintaks), sehingga proses binding ke port tidak pernah terjadi.
  • Ketidakcocokan Port (Port Mismatch): Developer sering kali membuat asumsi tentang port. Klien dikonfigurasi untuk memanggil port 3000, namun server API sebenarnya berjalan di port 8080 karena konfigurasi environment atau nilai default dari framework yang digunakan berbeda. Port 3000 yang kosong akan merespons dengan RST, memicu error ini.
  • Perbedaan Lingkungan Jaringan / Loopback vs External IP: Ini sangat umum terjadi pada setup Docker atau Kubernetes. Klien mencoba mengakses 127.0.0.1 (localhost) di dalam container-nya sendiri, sementara server API berjalan di container lain atau di host utama. Dalam dunia container, localhost memiliki cakupan yang terisolasi. Klien harus menggunakan IP jaringan bridge atau nama service Docker, bukan 127.0.0.1.
  • Firewall atau Security Group secara aktif menolak (Reject): Sering kali diasosiasikan dengan timeout (drop), namun beberapa aturan firewall (seperti iptables di Linux) dikonfigurasi untuk melakukan REJECT alih-alih DROP. Ketika aturan REJECT terpicu, firewall segera membalas dengan paket RST, yang dari perspektif klien, terlihat persis seperti ECONNREFUSED.

Cara memastikan penyebab mana yang kamu alami: Langkah termudah adalah bertanya pada sistem operasi. Gunakan utility seperti netstat, ss, atau lsof pada mesin tempat API seharusnya berjalan. Cari apakah ada proses yang listening di port target. Jika tidak ada output, itu bukan masalah jaringan—server-nya memang tidak ada. Jika ada proses listening tapi IP binding-nya hanya 127.0.0.1 sementara klien memanggil IP publik, itu masalah binding address. Jika proses listening normal tapi klien dari mesin lain tetap ditolak, periksa firewall.

Langkah perbaikan

Memperbaiki ECONNREFUSED membutuhkan pendekatan sistematis dari lapisan terdekat (lokal) ke lapisan terluar (jaringan). Kita tidak bisa asal menyemburkan resep tanpa tahu titik masalahnya. Berikut adalah langkah-langkah perbaikan yang dapat dilakukan secara berurutan.

Langkah 1: Verifikasi apakah server mendengarkan di port yang benar Langkah pertama adalah memastikan proses API ada dan mengikat (binding) dirinya ke port yang kita harapkan. Masuklah ke mesin/server di mana API seharusnya berjalan dan jalankan perintah pengecekan jaringan.

# Menggunakan ss (lebih modern di Linux)
sudo ss -tlnp | grep 3000

# Atau menggunakan lsof (di macOS/Linux)
sudo lsof -i -P -n | grep LISTEN | grep 3000

Output yang diharapkan:

LISTEN    0    128    0.0.0.0:3000    0.0.0.0:*    users:(("node",pid=1234,fd=18))

Jika perintah ini mengembalikan kosong, itu berarti tidak ada proses yang mendengarkan di port 3000. Solusi langsung: Nyalakan server API Anda. Jika server menyala tapi langsung crash, baca log crash-nya (misal: journalctl -u my-api.service -n 50 untuk systemd, atau log Docker). Jika output menunjukkan binding ke 127.0.0.1:3000 padahal klien memanggil IP publik, ubah konfigurasi host API agar binding ke 0.0.0.0:3000.

Langkah 2: Uji koneksi secara lokal dari mesin yang sama Sebelum menyidik klien dari mesin lain, pastikan server merespons di mesinnya sendiri menggunakan alat seperti curl. Ini mengeliminasi variabel jaringan lintas mesin.

curl -v http://127.0.0.1:3000/api/health

Output yang diharapkan (jika berhasil):

*   Trying 127.0.0.1:3000...
* Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0)
> GET /api/health HTTP/1.1
> Host: 127.0.0.1:3000

Jika curl lokal pun mengalami Connection refused, masalahnya murni pada aplikasi server yang belum siap menerima koneksi (mungkin masih dalam proses booting yang lambat, seperti menunggu koneksi database relasional terlebih dahulu). Fix permanen untuk ini: Implementasikan Health Check endpoint dan pastikan server hanya dianggap "siap" setelah database terhubung dan port berhasil binding.

Langkah 3: Uji koneksi dari mesin klien ke mesin server Jika Langkah 1 dan 2 berhasil, namun klien di mesin lain tetap mendapat ECONNREFUSED, kita harus menembakkan perintah dari mesin klien ke mesin server.

# Ganti 192.168.1.10 dengan IP mesin server
nc -zv 192.168.1.10 3000

Output jika berhasil:

Connection to 192.168.1.10 3000 port [tcp/*] succeeded!

Output jika ECONNREFUSED:

nc: connect to 192.168.1.10 port 3000 (tcp) failed: Connection refused

Jika langkah ini gagal, berarti ada tameng di antara klien dan server. Jika server binding ke 0.0.0.0, maka pelaku utamanya adalah Firewall. Solusi langsung: Buka port 3000 pada firewall mesin server. Di sistem berbasis Ubuntu/Debian, itu berarti menjalankan sudo ufw allow 3000. Di RHEL/CentOS, sudo firewall-cmd --add-port=3000/tcp --permanent && sudo firewall-cmd --reload.

Cara verifikasi solusi berhasil: Setelah melakukan perbaikan, jalankan kembali perintah klien (apakah itu curl, unit test, atau frontend app) yang semula mengeluarkan error. Jika klien sekarang mendapatkan respons (walaupun responsnya adalah HTTP 404 atau 500), itu berarti koneksi TCP sudah sukses terbangun. ECONNREFUSED sudah terpecahkan. Masalah HTTP status code selanjutnya adalah masalah lapisan aplikasi, bukan jaringan.

Fix permanen (bukan workaround)

Mengatasi ECONNREFUSED dengan menyalakan server secara manual atau membuka port firewall di terminal adalah solusi instan (workaround) yang akan terlupakan begitu sistem di-restart atau developer baru bergabung. Kita membutuhkan konfigurasi yang mencegah error ini kembali muncul secara struktural. Fix permanen berakar pada konfigurasi infrastruktur, manajemen environment, dan best practice dalam penulisan kode klien.

1. Sentralisasi Konfigurasi Host dan Port menggunakan Environment Variables Penyebab umum kesalahan konfigurasi (seperti port mismatch atau salah IP) adalah hardcoding. Jangan pernah menulis http://localhost:3000 langsung di dalam kode sumber. Gunakan file .env atau orkestrasi lingkungan untuk menyuntikkan nilai ini.

// KODE BURUK (Hardcoded - Rawan ECONNREFUSED di env lain)
const apiClient = axios.create({
  baseURL: 'http://localhost:3000/api'
});

// KODE BAIK (Dinamis)
const API_HOST = process.env.API_HOST || 'http://localhost:3000';
const apiClient = axios.create({
  baseURL: `${API_HOST}/api`
});

Dengan pendekatan ini, developer lokal dapat menggunakan localhost, sementara di lingkungan staging/production, variabel API_HOST diisi dengan internal IP load baler atau nama service Kubernetes (misal: http://api-service:8080), mengeliminasi potensi koneksi ke mesin yang salah.

2. Orkestrasi Service dan Dependency Health Check (Docker Compose) Dalam arsitektur microservice, sangat sering klien menyala lebih cepat dari server, menyebabkan klien menembak port yang belum di-binding. Docker Compose menyediakan mekanisme depends_on, tapi itu hanya menunggu container menyala, bukan port siap. Tambahkan healthcheck untuk memastikan server benar-benar merespons sebelum klien dimulai.

version: '3.8'
services:
  api-server:
    build: ./api
    ports:
      - "3000:3000"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 5s
      timeout: 3s
      retries: 5

  api-consumer:
    build: ./consumer
    depends_on:
      api-server:
        condition: service_healthy

Konfigurasi di atas adalah fix permanen untuk race condition saat booting. api-consumer tidak akan dijalankan oleh Docker sampai api-server dilaporkan sehat (port 3000 merespons), menghapus kemungkinan ECONNREFUSED akibat inisialisasi yang tidak sinkron.

3. Implementasi Retry Mechanism pada Klien Dalam sistem terdistribusi, kita harus menerima fakta bahwa layanan bisa saja sesekali sedang restarting. Alih-alih langsung crash saat mendapat ECONNREFUSED, klien dapat diimplementasikan dengan logika retry dengan exponential backoff. Ini bukan sekadar menutupi masalah, tapi merupakan standar resiliensi jaringan.

const axios = require('axios');
const axiosRetry = require('axios-retry');

const client = axios.create({ baseURL: process.env.API_HOST });

// Konfigurasi retry
axiosRetry(client, { 
  retries: 3, 
  retryDelay: axiosRetry.exponentialDelay,
  retryCondition: (error) => {
    // Retry khusus jika koneksi ditolak (ECONNREFUSED)
    return error.code === 'ECONNREFUSED';
  }
});

Perbedaan OS dalam Konfigurasi Permanen Penanganan firewall yang memicu RST packet berbeda tergantung sistem operasi:

  • Linux (Ubuntu/RHEL): Gunakan UFW atau firewalld. Pastikan aturan disimpan secara permanen (--permanent di firewalld, atau ufw reload setelah rules ditulis di /etc/ufw). Pada RHEL, secara default firewalld mem-blokir hampir semua port incoming, jadi eksplisit allow port API adalah keharusan.
  • macOS: Firewall bawaan (Application Firewall) umumnya memblokir aplikasi spesifik, bukan port. Jika menerima ECONNREFUSED karena firewall di Mac, kamu