jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Network Programming · Part 10 — Robust Networking: Errors, Retries & Debugging

Capstone: treat the network as unreliable — socket error codes, mandatory error handlers, timeouts at every layer, retries with backoff & jitter, reconnecting clients, and a layered debugging toolkit with Node.js + TypeScript.

This is Part 10 of 10 — the capstone of our network programming series with Node.js + TypeScript {Đây là Phần 10 / 10bài kết của series lập trình mạng với Node.js + TypeScript}. Parts 1–9 taught you how data travels, how to open sockets, speak HTTP, encrypt with TLS, frame messages, and scale servers {Phần 1–9 dạy dữ liệu đi thế nào, mở socket, nói HTTP, mã hóa TLS, đóng khung message, và scale server}. Today we answer the question every production system eventually faces: what happens when the network fails? {Hôm nay ta trả lời câu hỏi mọi hệ thống production cuối cùng đều gặp: điều gì xảy ra khi mạng hỏng?}.

The honest answer: failure is the default {Câu trả lời thật: lỗi là mặc định}. Packets drop, cables flap, DNS caches go stale, load balancers drain nodes, TLS certs expire, and clients disconnect mid-request {Packet rơi, cáp giật, cache DNS cũ, load balancer drain node, cert TLS hết hạn, client ngắt giữa request}. Code that assumes “if I called connect(), it worked forever” will crash, hang, or corrupt data {Code giả định “gọi connect() là xong mãi mãi” sẽ crash, treo, hoặc hỏng dữ liệu}. Robust networking means expecting failure and building recovery paths at every layer {Mạng robust nghĩa là chờ lỗi và xây đường phục hồi ở mọi tầng}.


The network is unreliable by default {Mạng mặc định không tin cậy}

In Part 1 we learned that TCP reassembles packets into an ordered stream {Ở Phần 1 ta học TCP ráp packet thành luồng có thứ tự}. That reliability is best-effort end-to-end — not a guarantee that your app will always succeed on the first try {Độ tin cậy đó là best-effort đầu-cuối — không đảm bảo app luôn thành công lần đầu}. Between your socket.write() and the peer’s socket.on('data') sit routers, NAT boxes, firewalls, kernel buffers, and GC pauses {Giữa socket.write()socket.on('data') của peer có router, NAT, firewall, buffer kernel, và GC pause}.

Your process → OS socket buffer → NIC → Internet → … → peer
              ↑ any hop can drop, delay, reset, or refuse

Design assumption {Giả định thiết kế}: every outbound call can fail; every inbound connection can die without warning; every retry can make things worse if you are careless {mọi lời gọi ra có thể fail; mọi kết nối vào có thể chết không báo; mọi retry có thể làm tệ hơn nếu bạn cẩu thả}. The rest of this post is the toolkit to live with that reality {Phần còn lại của bài là bộ công cụ để sống với thực tế đó}.


Common socket errors {Lỗi socket phổ biến}

Node surfaces OS-level errors as err.code on Error objects (and as errno on some APIs) {Node đưa lỗi cấp OS ra err.code trên object Error (và errno trên một số API)}. Learn these by heart — they tell you which layer broke and what to do next {Học thuộc — chúng cho biết tầng nào hỏng và làm gì tiếp}.

CodeWhat it means {Ý nghĩa}Typical cause {Nguyên nhân}What to do {Xử lý}
ECONNREFUSEDNothing is listening on that IP:port {Không ai listen IP:port đó}Server down, wrong port, firewall DROP {Server tắt, sai port, firewall DROP}Fix target, health-check upstream, retry with backoff {Sửa đích, health-check upstream, retry backoff}
ECONNRESETPeer closed the TCP connection abruptly {Peer đóng TCP đột ngột}Server crash, idle timeout, proxy kill {Server crash, timeout idle, proxy kill}Reconnect; do not assume partial writes succeeded {Kết nối lại; đừng giả định write một phần thành công}
ETIMEDOUTOperation exceeded the OS/network timeout {Vượt timeout OS/mạng}Host unreachable, routing black hole, overloaded server {Host không tới, routing đen, server quá tải}Shorter connect timeout + retry; check routing/DNS {Timeout connect ngắn hơn + retry; kiểm tra routing/DNS}
EHOSTUNREACHNo route to the host {Không có route tới host}Wrong IP, VPN down, local network issue {Sai IP, VPN tắt, lỗi mạng local}Verify IP/DNS; not fixed by app retry alone {Xác minh IP/DNS; retry app không đủ}
EPIPEWrite to a socket whose peer already closed {Ghi vào socket peer đã đóng}You kept writing after end() / disconnect {Tiếp tục ghi sau end() / disconnect}Attach error handler; stop writing on close {Gắn handler error; dừng ghi khi close}
EADDRINUSEPort already bound by another process {Port đã bị process khác bind}Two servers on same port {Hai server cùng port}Pick another port or stop the conflicting process {Chọn port khác hoặc dừng process xung đột}
ENOTFOUNDDNS lookup failed — hostname has no address {DNS lookup fail — hostname không có địa chỉ}Typo, expired domain, resolver outage (Part 4) {Gõ sai, domain hết hạn, resolver lỗi (Phần 4)}Validate hostname; cache DNS carefully; retry resolver {Kiểm tra hostname; cache DNS cẩn thận; retry resolver}

Key idea {Ý chính}: ECONNREFUSED is “nobody home”; ECONNRESET is “they hung up on you”; ETIMEDOUT is “I waited and gave up” {ECONNREFUSED là “không ai ở nhà”; ECONNRESET là “bên kia cúp máy”; ETIMEDOUT là “chờ quá lâu rồi bỏ cuộc”}.


Always attach an error handler {Luôn gắn handler error}

In Node.js, an error event on a socket with no listener is fatal — it throws and can crash your entire process {Trong Node.js, error event trên socket không có listener là fatal — ném exception và có thể crash cả process}. This catches beginners constantly {Lỗi này bẫy người mới liên tục}.

import { createServer, type Socket } from 'node:net';

function attachSafeHandlers(socket: Socket, label: string): void {
  socket.on('error', (err: NodeJS.ErrnoException) => {
    // Log with context — never let this event go unhandled.
    console.error(`[${label}] socket error:`, err.code ?? err.message);
  });

  socket.on('close', (hadError) => {
    console.log(`[${label}] closed`, hadError ? '(with error)' : '(clean)');
  });
}

const server = createServer((socket) => {
  attachSafeHandlers(socket, 'server-conn');
  socket.write('hello\n');
});

server.on('error', (err: NodeJS.ErrnoException) => {
  // Server-level errors too — e.g. EADDRINUSE on listen().
  console.error('server error:', err.code ?? err.message);
});

server.listen(3000);

The same rule applies to tls.TLSSocket, http.Server, WebSocket, and child_process streams {Quy tắc tương tự cho tls.TLSSocket, http.Server, WebSocket, và stream child_process}: if it extends EventEmitter and emits error, you must listen {nếu kế thừa EventEmitter và emit error, bạn phải listen}. In async code, also handle rejected promises from fetch, connect(), and your own wrappers {Trong code async, cũng xử lý promise reject từ fetch, connect(), và wrapper của bạn}.


Timeouts at every layer {Timeout ở mọi tầng}

“No timeout” means “hang forever” {Không timeout nghĩa là “treo mãi mãi”}. Production code needs three different timeouts {Code production cần ba loại timeout khác nhau}:

1. Connect timeout {Timeout kết nối}

How long you wait for TCP (and TLS) handshake to finish {Chờ bao lâu cho bắt tay TCP (và TLS) hoàn tất}. node:net has no built-in connect timeout — you implement it {node:net không có connect timeout sẵn — bạn tự implement}:

import { Socket } from 'node:net';

export function connectWithTimeout(
  host: string,
  port: number,
  connectMs: number,
): Promise<Socket> {
  return new Promise((resolve, reject) => {
    const socket = new Socket();
    const timer = setTimeout(() => {
      socket.destroy();
      reject(new Error(`connect timeout after ${connectMs}ms`));
    }, connectMs);

    socket.once('connect', () => {
      clearTimeout(timer);
      resolve(socket);
    });

    socket.once('error', (err) => {
      clearTimeout(timer);
      reject(err);
    });

    socket.connect(port, host);
  });
}

// Usage
const socket = await connectWithTimeout('127.0.0.1', 3000, 3_000);

2. Idle / socket timeout {Timeout idle / socket}

How long the connection can sit without receiving data before you assume it is dead {Kết nối có thể không nhận dữ liệu bao lâu trước khi coi là chết}. Essential for long-lived WebSocket and TCP clients (Part 6) {Cần thiết cho WebSocket và TCP client sống lâu (Phần 6)}:

socket.setTimeout(30_000); // 30 s idle

socket.on('timeout', () => {
  console.warn('socket idle timeout — destroying');
  socket.destroy();
});

Combine with application heartbeats (ping/pong frames) so idle timeout only fires on truly dead peers {Kết hợp heartbeat ứng dụng (ping/pong) để idle timeout chỉ kích hoạt khi peer thật sự chết}.

3. Overall request timeout {Timeout toàn bộ request}

A ceiling on the entire operation — connect + TLS + send + wait for response {Trần cho toàn bộ thao tác — connect + TLS + gửi + chờ response}. Node 18+ ships AbortSignal.timeout() {Node 18+ có sẵn AbortSignal.timeout()}:

async function fetchWithDeadline(url: string, totalMs: number): Promise<Response> {
  const response = await fetch(url, {
    signal: AbortSignal.timeout(totalMs),
  });
  return response;
}

try {
  const res = await fetchWithDeadline('https://api.example.com/health', 5_000);
  console.log('status:', res.status);
} catch (err) {
  // DOMException with name 'TimeoutError' when AbortSignal fires.
  console.error('request failed or timed out:', err);
}

For raw sockets, wrap the whole flow in Promise.race against a timer, or use AbortController in your own client library {Với socket thô, bọc toàn flow trong Promise.race với timer, hoặc dùng AbortController trong client library của bạn}. Rule of thumb {Quy tắc ngón tay cái}: connect timeout ≪ idle timeout ≪ overall request timeout {connect timeout ≪ idle timeout ≪ overall request timeout}.


Retries done right {Retry đúng cách}

Blind retries are how a brief outage becomes a multi-hour incident {Retry mù quáng biến sự cố ngắn thành sự cố nhiều giờ}. Three rules:

Only retry idempotent operations {Chỉ retry thao tác idempotent}

An operation is idempotent if doing it twice has the same effect as once {Idempotent nghĩa là làm hai lần có cùng hiệu ứng với một lần}.

Safe to retry {An toàn retry}Dangerous to retry {Nguy hiểm retry}
GET, HEAD, PUT with full bodyPOST payment, POST create order
DNS lookupNon-idempotent RPC without dedup key
TCP/WebSocket reconnect (new session)Retry after partial write without framing (Part 8)

If the server might have already processed your request, use an idempotency key header before retrying {Nếu server có thể đã xử lý request, dùng header idempotency key trước khi retry}.

Exponential backoff with jitter {Backoff lũy thừa có jitter}

When a service is down, every client retrying at the same interval creates a retry storm that keeps it down {Khi service down, mọi client retry cùng nhịp tạo bão retry khiến nó tiếp tục down}. Exponential backoff spaces attempts apart; jitter randomizes the delay so clients do not synchronize {Backoff lũy thừa giãn các lần thử; jitter random hóa delay để client không đồng bộ}.

export interface RetryOptions {
  maxAttempts: number;
  baseDelayMs: number;
  maxDelayMs: number;
  /** Return false to stop retrying this error immediately. */
  shouldRetry?: (err: unknown, attempt: number) => boolean;
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function backoffDelay(attempt: number, base: number, max: number): number {
  const exp = Math.min(max, base * 2 ** (attempt - 1));
  const jitter = Math.random() * exp * 0.3; // up to 30% random spread
  return Math.floor(exp + jitter);
}

export async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  opts: RetryOptions,
): Promise<T> {
  const { maxAttempts, baseDelayMs, maxDelayMs, shouldRetry } = opts;
  let lastErr: unknown;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
      if (shouldRetry && !shouldRetry(err, attempt)) throw err;
      if (attempt === maxAttempts) break;
      const delay = backoffDelay(attempt, baseDelayMs, maxDelayMs);
      console.warn(`attempt ${attempt} failed, retrying in ${delay}ms`, err);
      await sleep(delay);
    }
  }

  throw lastErr;
}

// Example: retry GET on transient network errors only
async function fetchHealth(): Promise<string> {
  return retryWithBackoff(
    async () => {
      const res = await fetch('http://127.0.0.1:3000/health', {
        signal: AbortSignal.timeout(2_000),
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.text();
    },
    {
      maxAttempts: 5,
      baseDelayMs: 200,
      maxDelayMs: 8_000,
      shouldRetry: (err) => {
        const code = (err as NodeJS.ErrnoException).code;
        return code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'ECONNRESET';
      },
    },
  );
}

Circuit breaker (one paragraph) {Circuit breaker (một đoạn)}

A circuit breaker watches failure rate and stops calling a sick dependency for a cooldown period {Circuit breaker theo dõi tỷ lệ lỗi và ngừng gọi dependency đang bệnh trong thời gian cooldown}. After N consecutive failures, the breaker opens — your code fails fast locally instead of hammering a dying server {Sau N lỗi liên tiếp, breaker mở — code fail nhanh tại chỗ thay vì đập server đang chết}. After a timeout it half-opens: one probe request goes through; success closes the circuit (normal retries resume), failure re-opens it {Sau timeout breaker nửa-mở: một request thăm dò; thành công đóng mạch (retry bình thường), thất bại mở lại}. You do not need a heavy library on day one — a counter, a disabledUntil timestamp, and the backoff helper above are enough for most Node services {Không cần thư viện nặng ngay — counter, timestamp disabledUntil, và helper backoff ở trên đủ cho hầu hết service Node}.


A resilient reconnecting client {Client reconnect có khả năng phục hồi}

Long-lived connections — WebSocket chat (Part 6), TCP telemetry, game sessions — will drop {Kết nối sống lâu — chat WebSocket (Phần 6), telemetry TCP, session game — sẽ rớt}. Pattern: on close or error, schedule reconnect with capped exponential backoff, reset backoff on successful open {Pattern: khi close hoặc error, lên lịch reconnect với backoff lũy thừa có trần, reset backoff khi open thành công}.

import WebSocket from 'ws';

interface ReconnectOptions {
  url: string;
  minDelayMs: number;
  maxDelayMs: number;
}

export function createReconnectingWebSocket(opts: ReconnectOptions): WebSocket {
  let attempt = 0;
  let ws: WebSocket;
  let reconnectTimer: ReturnType<typeof setTimeout> | undefined;

  const scheduleReconnect = (): void => {
    attempt++;
    const exp = Math.min(opts.maxDelayMs, opts.minDelayMs * 2 ** (attempt - 1));
    const jitter = Math.random() * exp * 0.2;
    const delay = Math.floor(exp + jitter);
    console.warn(`reconnecting in ${delay}ms (attempt ${attempt})`);
    reconnectTimer = setTimeout(connect, delay);
  };

  const connect = (): void => {
    ws = new WebSocket(opts.url);

    ws.on('open', () => {
      attempt = 0; // success — reset backoff
      console.log('connected');
    });

    ws.on('message', (data) => {
      console.log('←', data.toString());
    });

    ws.on('error', (err) => {
      // MUST exist — prevents process crash.
      console.error('ws error:', err.message);
    });

    ws.on('close', () => {
      console.warn('ws closed — scheduling reconnect');
      scheduleReconnect();
    });
  };

  connect();
  return ws!;
}

The same structure works for raw TCP with net.createConnection() {Cấu trúc tương tự cho TCP thô với net.createConnection()}: track attempt, cap delay, reset on connect, never skip error handler {theo dõi attempt, giới hạn delay, reset khi connect, không bỏ handler error}. On reconnect, re-authenticate and replay state (subscriptions, cursor offsets) — the wire is new even if the URL is the same {Khi reconnect, xác thực lạireplay state (subscription, cursor) — đường truyền mới dù URL giống}.


The debugging toolkit {Bộ công cụ debug}

When something breaks, pick the tool at the layer where the symptom lives {Khi hỏng, chọn công cụ ở tầng có triệu chứng}. Do not reach for Wireshark when the bug is a malformed HTTP header {Đừng dùng Wireshark khi lỗi là HTTP header sai}.

curl · httpie application requests nc (netcat) · openssl s_client raw sockets & TLS tcpdump · Wireshark packets on the wire ss · netstat · ping · traceroute sockets & reachability pick the tool at the layer where the problem lives
Start at the application layer and drill down — curl for HTTP, nc/openssl for sockets, tcpdump when bytes on the wire disagree with your code

Application layer — HTTP & APIs {Tầng ứng dụng — HTTP & API}

# Verbose HTTP — see request, response headers, TLS timing, redirects
curl -v http://127.0.0.1:3000/health
curl -v https://api.example.com/users -H 'Accept: application/json'

# Time breakdown: DNS, connect, TLS, TTFB
curl -w 'dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer}\n' -o /dev/null -s https://example.com

curl -v is the fastest way to answer “is the server returning what I think?” without writing client code {curl -v là cách nhanh nhất trả lời “server có trả đúng như tôi nghĩ?” mà không viết client code}.

Socket & TLS layer — raw bytes {Tầng socket & TLS — byte thô}

# Raw TCP — type bytes, see what the server sends (Part 2)
nc -v 127.0.0.1 3000
echo 'PING' | nc -w 3 127.0.0.1 9000

# TLS handshake + cert chain (Part 7)
openssl s_client -connect example.com:443 -servername example.com </dev/null

openssl s_client shows the certificate chain, cipher, and whether SNI matches {openssl s_client hiện chuỗi certificate, cipher, và SNI có khớp}. Use it when fetch fails with certificate errors but you are unsure why {Dùng khi fetch fail lỗi certificate mà bạn không chắc vì sao}.

Reachability & socket table {Khả năng tới & bảng socket}

# Is the host alive at the IP layer?
ping -c 3 8.8.8.8
traceroute api.example.com

# What is listening? Established connections? (Linux: ss; macOS: netstat)
ss -tlnp
ss -tn state established
netstat -an | grep LISTEN

If ping fails but curl works, the problem is above IP (or ICMP is blocked) {Nếu ping fail nhưng curl ok, vấn đề trên tầng IP (hoặc ICMP bị chặn)}. If nothing is LISTEN on your port, ECONNREFUSED is expected {Nếu không có gì LISTEN trên port, ECONNREFUSED là đúng}.

Packet capture — ground truth {Bắt packet — sự thật cuối cùng}

When application logs and curl disagree with reality, capture packets {Khi log ứng dụng và curl không khớp thực tế, bắt packet}:

# Capture TCP port 3000 on loopback (Linux; macOS: lo0)
sudo tcpdump -i any port 3000 -nn -A

# TLS on 443 — ciphertext on the wire; combine with openssl s_client for keys in dev
sudo tcpdump -i any host api.example.com and port 443 -nn

Reading one tcpdump line {Đọc một dòng tcpdump}:

12:34:56.789012 IP 192.168.1.10.54321 > 93.184.216.34.443: Flags [P.], seq 100, ack 50, win 256, length 42
  • 192.168.1.10.5432193.184.216.34.443 — direction: your client port 54321 to server 443 {hướng: client port 54321 tới server 443}.
  • Flags [P.]Push (application data), . ack field valid {Push (dữ liệu ứng dụng), . trường ack hợp lệ}.
  • seq 100 / ack 50 — TCP sequence numbers for reorder/retransmit debugging {số thứ tự TCP để debug reorder/retransmit}.
  • length 42 — 42 bytes of payload in this segment {42 byte payload trong segment này}.

Wireshark adds decoding for HTTP, TLS, DNS, and WebSocket — same capture, richer view {Wireshark decode HTTP, TLS, DNS, WebSocket — cùng capture, nhìn phong phú hơn}. In Part 7 we encrypted the wire; in captures you will see TLS Application Data records, not plaintext passwords {Ở Phần 7 ta mã hóa đường truyền; trong capture bạn thấy bản ghi TLS Application Data, không phải mật khẩu plaintext}.


Putting it together: a small resilient HTTP client {Gắn lại: HTTP client nhỏ có khả năng phục hồi}

import { retryWithBackoff } from './retry-with-backoff.js';

export async function resilientGet(path: string): Promise<string> {
  return retryWithBackoff(
    async () => {
      const res = await fetch(`http://127.0.0.1:3000${path}`, {
        signal: AbortSignal.timeout(4_000),
      });
      if (res.status >= 500) throw new Error(`server error ${res.status}`);
      if (!res.ok) throw new Error(`client error ${res.status}`);
      return res.text();
    },
    {
      maxAttempts: 4,
      baseDelayMs: 100,
      maxDelayMs: 4_000,
      shouldRetry: (err, attempt) => {
        const code = (err as NodeJS.ErrnoException).code;
        const msg = err instanceof Error ? err.message : '';
        if (msg.includes('server error')) return true;
        return code === 'ECONNREFUSED' || code === 'ETIMEDOUT';
      },
    },
  );
}

Every layer covered: overall timeout on fetch, retry only on transient / 5xx, backoff with jitter inside retryWithBackoff, and underlying sockets still need error handlers on servers you own {Mọi tầng: timeout tổng trên fetch, chỉ retry transient / 5xx, backoff + jitter trong retryWithBackoff, và socket bên dưới vẫn cần handler error trên server bạn sở hữu}.


Mistakes beginners make {Lỗi người mới hay mắc}

  • No error handler on sockets or servers — one ECONNRESET crashes the Node process {Không handler error trên socket/server — một ECONNRESET crash process Node}.
  • Retrying non-idempotent requests — a duplicated POST /charge can double-bill a customer {Retry request không idempotentPOST /charge trùng có thể tính tiền hai lần}.
  • Retry storms without backoff/jitter — clients synchronize and DDoS your own recovering service {Bão retry không backoff/jitter — client đồng bộ và DDoS service đang hồi phục}.
  • No timeouts — a stuck connect() or fetch() holds event-loop work and file descriptors forever {Không timeoutconnect() hoặc fetch() kẹt giữ event-loop và file descriptor mãi}.
  • Swallowing errors silently — empty catch {} blocks hide ENOTFOUND and ECONNREFUSED until users complain {Nuốt lỗi im lặngcatch {} rỗng che ENOTFOUNDECONNREFUSED đến khi user phàn nàn}.

Exercises {Bài tập}

Try each before opening the solution {Thử từng bài trước khi mở lời giải}.

  1. Start a TCP server on port 3000, then connect with nc. Kill the server while nc is connected — observe ECONNRESET on the server if you omit an error handler {Chạy server TCP port 3000, kết nối nc. Tắt server khi nc đang kết nối — quan sát ECONNRESET trên server nếu thiếu handler error}.
  2. Write a retryWithBackoff call that retries fetch('http://127.0.0.1:9/') (port 9 is discard — nothing listens) and log each delay. Confirm delays grow and differ between runs (jitter) {Viết retryWithBackoff gọi fetch('http://127.0.0.1:9/') và log mỗi delay. Xác nhận delay tăng và khác giữa các lần chạy (jitter)}.
  3. Run curl -v http://127.0.0.1:3000/ against your Part 5 HTTP server, then run sudo tcpdump -i lo0 port 3000 -nn -A (macOS) or sudo tcpdump -i any port 3000 -nn -A (Linux) in another terminal. Identify one [P.] line and label source, destination, and length {Chạy curl -v vào server HTTP Phần 5, đồng thời tcpdump ở terminal khác. Tìm một dòng [P.] và ghi source, destination, length}.
Solution {Lời giải}
// Exercise 1 — server without error handler crashes on client reset
import { createServer } from 'node:net';

const server = createServer((socket) => {
  // Missing socket.on('error') → process may throw on ECONNRESET
  socket.on('data', (buf) => socket.write(buf));
});

server.listen(3000, () => console.log('listening :3000'));
// Terminal 2: nc localhost 3000
// Kill server (Ctrl+C) or kill nc abruptly → watch for uncaughtException

Fix: add socket.on('error', () => {}) and server.on('error', () => {}) — the process stays alive {Sửa: thêm socket.on('error')server.on('error') — process sống sót}.

// Exercise 2 — jittered backoff on ECONNREFUSED
import { retryWithBackoff } from './retry-with-backoff.js';

await retryWithBackoff(
  async () => fetch('http://127.0.0.1:9/'),
  { maxAttempts: 4, baseDelayMs: 200, maxDelayMs: 3_000 },
);
// Logs show ~200ms, ~400ms, ~800ms ± jitter — different each run
# Exercise 3
curl -v http://127.0.0.1:3000/
sudo tcpdump -i lo0 port 3000 -nn -A   # macOS loopback
# Example line:
# IP 127.0.0.1.54321 > 127.0.0.1.3000: Flags [P.], seq 1, ack 1, win 6379, length 78
# Source: 127.0.0.1:54321 (curl client)
# Destination: 127.0.0.1:3000 (your server)
# length 78: 78 bytes of HTTP request payload in this segment

Series recap {Tóm tắt series}

You now have the full path from bits on the wire to production-grade behavior {Bạn đã có lộ trình đầy đủ từ bit trên dây đến hành vi production}:

Part 10 ties it together: expect failure, bound every wait with a timeout, retry only what is safe, and debug layer by layer {Phần 10 gắn kết: chờ lỗi, giới hạn mọi chờ bằng timeout, chỉ retry cái an toàn, và debug từng tầng}.


Takeaway {Điều cốt lõi}

The network is not a function that always returns — it is a probabilistic channel {Mạng không phải hàm luôn trả về — nó là kênh xác suất}. Attach error handlers, set connect / idle / request timeouts, retry idempotent work with backoff + jitter, reconnect long-lived clients with capped delay, and when logs are not enough, curl -vnc / openssl s_clientss / pingtcpdump until the layer tells the truth {Gắn handler error, đặt timeout connect / idle / request, retry công việc idempotent với backoff + jitter, reconnect client sống lâu có trần delay, và khi log không đủ, curl -vnc / openssl s_clientss / pingtcpdump đến khi tầng đó nói sự thật}. That mindset is what separates a demo server from software that survives Tuesday afternoon deploys {Tư duy đó tách server demo khỏi phần mềm sống sót qua đợt deploy chiều thứ Ba}.