Deepen PracticeOrdered learning track

Stream Module Mental Model

Learn NGINX In Action - Part 083

Stream module mental model untuk memahami NGINX sebagai Layer 4 proxy/load balancer, berbeda dari HTTP reverse proxy karena tidak melihat URI, header, cookie, method, atau response code.

12 min read2360 words
PrevNext
Lesson 83105 lesson track58–86 Deepen Practice
#nginx#stream#tcp#udp+5 more

Part 083 — Stream Module Mental Model

Di fase HTTP, NGINX bisa berpikir dalam request, header, URI, method, status code, cache, CORS, auth, dan content.
Di fase stream, NGINX berpikir dalam connection, bytes, session, upstream, timeout, dan socket lifecycle.

Ini pergeseran mental model yang besar.

Banyak engineer salah memakai stream module karena membawa intuisi HTTP ke Layer 4. Mereka ingin routing berdasarkan path, membaca JWT, menambah header, mengubah body, atau melihat response code. Itu bukan domain stream. Pada stream, NGINX tidak memproses HTTP message. NGINX hanya menerima koneksi TCP atau datagram UDP, memilih upstream, lalu mem-proxy aliran data.

Dokumentasi resmi NGINX menyebut ngx_stream_core_module tersedia sejak 1.9.0 dan tidak dibangun secara default ketika build from source; module ini harus diaktifkan dengan --with-stream. ngx_stream_proxy_module memungkinkan proxy data stream melalui TCP, UDP, dan UNIX-domain socket. Artinya, saat menggunakan binary distro/container, selalu verifikasi apakah module stream tersedia sebelum mendesain arsitektur di atasnya.

nginx -V 2>&1 | tr ' ' '\n' | grep -- '--with-stream'

Kalau output kosong, jangan lanjut mendesain stream proxy dulu. Binary NGINX tersebut mungkin tidak membawa stream module.


1. Apa sebenarnya stream?

stream adalah subsystem NGINX untuk proxy/load balancing Layer 4.

Bentuk paling minimal:

stream {
    upstream postgres_backends {
        server 10.10.1.11:5432;
        server 10.10.1.12:5432;
    }

    server {
        listen 5432;
        proxy_pass postgres_backends;
    }
}

Di sini tidak ada:

  • location
  • server_name HTTP virtual host
  • proxy_set_header
  • $uri
  • $request_method
  • $status HTTP
  • Cache-Control
  • CORS
  • HTTP auth
  • response body transformation

Yang ada adalah:

  • listener socket
  • upstream group
  • TCP/UDP session
  • connection timeout
  • upstream timeout
  • SSL/TLS termination atau passthrough, bila dikonfigurasi
  • PROXY protocol, bila dibutuhkan
  • stream variables
  • stream access control
  • stream log

Mental model sederhananya:

NGINX tidak bertanya: “request ini /api/users atau /assets/app.js?”
NGINX bertanya: “connection/datagram ini masuk listener mana, harus diarahkan ke upstream mana, dan berapa lama boleh hidup?”


2. HTTP proxy vs stream proxy

AspekHTTP subsystemStream subsystem
LayerL7/applicationL4/transport-ish
Unit kerjaHTTP request/responseTCP session atau UDP datagram/session approximation
Routing umumhost, URI, method, header, cookielistener port, SNI, client IP, PROXY protocol, variables tertentu
Bisa baca path?YaTidak
Bisa set HTTP header?YaTidak
Bisa cache response?YaTidak
Bisa rate-limit HTTP request?YaBukan dengan limit_req; harus pakai stream primitives/OS/network controls
Cocok untukAPI, web app, static, gRPC HTTP/2PostgreSQL, MySQL, Redis, LDAP, DNS, syslog, TLS passthrough, custom TCP

Kalimat praktisnya:

Kalau keputusan routing membutuhkan HTTP semantics, gunakan http.
Kalau keputusan routing cukup berdasarkan connection-level semantics, gunakan stream.


3. Di mana stream diletakkan?

stream adalah top-level context, sejajar dengan events dan http.

worker_processes auto;

events {
    worker_connections 4096;
}

http {
    # HTTP reverse proxy, cache, static, API gateway, TLS termination HTTP
}

stream {
    # TCP/UDP proxy/load balancing
}

Ini penting karena directive HTTP tidak otomatis tersedia di stream context.

Contoh salah:

stream {
    server {
        listen 5432;

        # Salah: ini directive HTTP, bukan stream.
        location / {
            proxy_pass postgres_backends;
        }
    }
}

Contoh benar:

stream {
    upstream postgres_backends {
        server 10.10.1.11:5432;
        server 10.10.1.12:5432;
    }

    server {
        listen 5432;
        proxy_pass postgres_backends;
    }
}

4. Kapan stream dipakai?

Gunakan stream ketika NGINX harus menjadi proxy/load balancer untuk protocol yang bukan HTTP atau ketika NGINX tidak boleh decrypt/memahami HTTP.

Contoh umum:

Use caseProtocolKenapa stream?
PostgreSQL edge/internal LBTCP 5432Protocol bukan HTTP
MySQL LBTCP 3306Protocol bukan HTTP
Redis proxyTCP 6379Protocol bukan HTTP
LDAP/LDAPS proxyTCP 389/636Protocol bukan HTTP
DNS proxy/load balancingUDP/TCP 53DNS bisa UDP dan TCP
Syslog fan-in/proxyUDP/TCP 514Non-HTTP telemetry
TLS passthroughTCP 443NGINX tidak terminate TLS, hanya route berdasarkan SNI bila perlu
MQTTTCP 1883/8883Non-HTTP long-lived connection
Custom binary protocolTCPTidak ada HTTP semantics

Jangan pakai stream hanya karena “lebih cepat”. Untuk HTTP traffic, subsystem HTTP memberi observability, header control, cache, security policy, retry semantics, dan tooling yang jauh lebih tepat.


5. Stream tidak otomatis berarti “transparent proxy”

Salah satu kesalahpahaman besar: karena stream berada di Layer 4, engineer mengira upstream otomatis melihat IP client asli.

Tidak selalu.

Secara default, backend melihat koneksi dari IP NGINX, bukan IP client asli.

Dari sisi DB, peer address adalah 10.0.0.5.

Kalau backend harus tahu IP asli, opsi yang tersedia bergantung pada protocol/backend:

  1. PROXY protocol bila backend mendukungnya.
  2. Transparent proxying dengan OS/network routing khusus, jauh lebih kompleks.
  3. Logging IP asli di NGINX saja, bukan di backend.
  4. Protocol-level identity dari aplikasi, misalnya database user/client certificate, bukan source IP.

Jangan mengandalkan source IP backend untuk audit user kalau NGINX berada di tengah dan tidak ada mekanisme preservasi identitas.


6. Listener adalah routing boundary pertama

Dalam stream, listener address/port sangat penting.

stream {
    server {
        listen 5432;
        proxy_pass postgres_primary_pool;
    }

    server {
        listen 6380;
        proxy_pass redis_pool;
    }
}

Satu listener biasanya merepresentasikan satu service contract.

Pada HTTP, satu port 443 bisa melayani banyak host/path. Pada stream, satu port sering lebih dekat dengan satu protocol/service. Bisa ada SNI-based routing untuk TLS passthrough, tetapi itu tetap terbatas pada informasi TLS ClientHello, bukan HTTP request.


7. Stream server lifecycle

Untuk TCP, lifecycle sederhananya:

NGINX tidak tahu “request selesai” kecuali protocol-level close/timeout terjadi. Ini berbeda dari HTTP keepalive, di mana satu TCP connection dapat membawa banyak HTTP request/response dan NGINX memahami boundaries-nya.

Implikasi production:

  • Long-lived TCP connection bisa menahan slot upstream lama.
  • Load balancing biasanya terjadi saat connection dibuat, bukan setiap query/database command.
  • Jika client mempertahankan connection berjam-jam, rebalancing tidak otomatis meratakan existing sessions.
  • Timeout dan keepalive policy harus mengikuti protocol.

8. TCP stream vs UDP stream

TCP dan UDP diproxy oleh subsystem stream, tapi mental modelnya berbeda.

AspekTCPUDP
KoneksiStateful connectionDatagram, connectionless
Load balancing unitConnectionDatagram/session approximation
BackpressureAda via TCP flow controlTidak seperti TCP
Failure visibilityConnect/read/write timeoutLebih ambigu
Cocok untukDB, Redis, LDAP, MQTT, TLS passthroughDNS, syslog, RADIUS
RisikoLong-lived connection skewStateless loss, retry client-side

TCP proxying biasanya lebih mudah dipahami karena ada connection lifecycle. UDP proxying lebih tricky karena tidak ada connection natural; NGINX harus membuat session-like association untuk meneruskan response upstream ke client.

Part 085 nanti akan fokus khusus UDP.


9. Upstream group di stream

stream punya upstream module sendiri: ngx_stream_upstream_module.

Contoh:

stream {
    upstream mysql_pool {
        least_conn;

        server 10.0.10.11:3306 max_fails=2 fail_timeout=10s;
        server 10.0.10.12:3306 max_fails=2 fail_timeout=10s;
    }

    server {
        listen 3306;
        proxy_pass mysql_pool;
    }
}

Konsepnya mirip HTTP upstream, tetapi semantics-nya mengikuti session/connection, bukan HTTP request.

Load balancing method umum:

  • default round-robin
  • least_conn
  • hash ... consistent
  • random pada versi/module yang mendukung
  • Plus/commercial features tertentu seperti active health checks pada boundary berbeda

Jangan menganggap semua fitur HTTP upstream identik dengan stream upstream. Nama directive bisa mirip, tapi context dan behavior tidak selalu sama.


10. Passive failure di stream

Pada TCP, failure yang bisa dideteksi NGINX biasanya berupa:

  • gagal connect ke upstream
  • upstream menutup koneksi terlalu cepat
  • timeout saat connect/read/write
  • network reset

Contoh:

stream {
    upstream redis_pool {
        server 10.0.20.11:6379 max_fails=2 fail_timeout=10s;
        server 10.0.20.12:6379 max_fails=2 fail_timeout=10s;
    }

    server {
        listen 6379;
        proxy_connect_timeout 1s;
        proxy_timeout 30s;
        proxy_pass redis_pool;
    }
}

Namun NGINX tidak tahu apakah Redis command tertentu gagal secara semantic, apakah SQL query lambat karena lock, atau apakah LDAP bind ditolak. Itu application-level failure.

Invariant:

Stream passive health melihat transport-level failure, bukan business correctness.


11. Timeout adalah safety boundary utama

Directive penting:

proxy_connect_timeout 1s;
proxy_timeout 30s;

Makna praktis:

  • proxy_connect_timeout: berapa lama NGINX menunggu koneksi ke upstream terbentuk.
  • proxy_timeout: timeout antara dua operasi read/write berturut-turut pada client atau upstream.

Jangan copy timeout dari HTTP API ke database stream proxy. Database connection bisa idle lama karena pooling. Redis pub/sub bisa long-lived. MQTT bisa persistent. LDAP bind/search mungkin pendek. DNS UDP harus sangat pendek.

Timeout harus mengikuti protocol contract.

Contoh matrix awal:

ServiceConnect timeoutSession timeoutCatatan
PostgreSQL OLTP1s1h atau sesuai poolPastikan pool app punya idle timeout selaras
MySQL OLTP1s1h atau sesuai poolHindari NGINX menutup koneksi lebih cepat dari pool expectation
Redis cache500ms-1s30s-5mTergantung command/pool model
LDAP bind/search1s30s-2mBiasanya tidak perlu long-lived sangat lama
MQTT1s-3spanjangPerlu heartbeat/keepalive protocol-aware
DNS UDPpendekpendekBahas di Part 085

Angka di atas bukan template final. Itu starting point untuk reasoning.


12. TLS di stream: terminate, passthrough, atau inspect SNI

Stream bisa dipakai untuk beberapa pola TLS:

12.1 TLS passthrough

NGINX tidak decrypt traffic.

stream {
    upstream tls_backend {
        server 10.0.30.10:443;
    }

    server {
        listen 443;
        proxy_pass tls_backend;
    }
}

Client melakukan TLS handshake langsung dengan backend. NGINX hanya meneruskan bytes.

Kelebihan:

  • end-to-end TLS sampai backend
  • backend memegang certificate/private key
  • NGINX tidak melihat plaintext

Kekurangan:

  • tidak bisa HTTP routing berdasarkan path/header
  • tidak bisa inject header
  • observability terbatas
  • policy L7 tidak bisa dilakukan di NGINX

12.2 TLS termination di stream

NGINX terminate TLS, tetapi tidak masuk HTTP subsystem.

stream {
    server {
        listen 636 ssl;
        ssl_certificate     /etc/nginx/certs/ldap.example.com.fullchain.pem;
        ssl_certificate_key /etc/nginx/certs/ldap.example.com.key;

        proxy_pass ldap_plain_backend;
    }
}

Cocok untuk protocol TLS non-HTTP seperti LDAPS atau custom TLS protocol.

12.3 SNI-based routing

Dengan preread, NGINX dapat membaca TLS ClientHello untuk mengambil SNI tanpa terminate TLS.

stream {
    map $ssl_preread_server_name $backend {
        db1.example.com db1_pool;
        db2.example.com db2_pool;
        default        default_tls_pool;
    }

    upstream db1_pool { server 10.0.40.11:443; }
    upstream db2_pool { server 10.0.40.12:443; }
    upstream default_tls_pool { server 10.0.40.99:443; }

    server {
        listen 443;
        ssl_preread on;
        proxy_pass $backend;
    }
}

Ini bukan membaca HTTP Host header. Ini hanya membaca TLS ClientHello SNI.


13. Stream observability harus dirancang sejak awal

HTTP log default tidak membantu stream.

Gunakan log_format stream:

stream {
    log_format stream_json escape=json
      '{'
        '"ts":"$time_iso8601",'
        '"remote_addr":"$remote_addr",'
        '"protocol":"$protocol",'
        '"status":"$status",'
        '"bytes_sent":$bytes_sent,'
        '"bytes_received":$bytes_received,'
        '"session_time":$session_time,'
        '"upstream_addr":"$upstream_addr",'
        '"upstream_bytes_sent":"$upstream_bytes_sent",'
        '"upstream_bytes_received":"$upstream_bytes_received",'
        '"upstream_connect_time":"$upstream_connect_time",'
        '"upstream_session_time":"$upstream_session_time"'
      '}';

    access_log /var/log/nginx/stream-access.log stream_json;
}

Yang perlu dibaca:

FieldMakna operasional
$remote_addrIP peer yang connect ke NGINX; bisa LB upstream, bukan end-user
$protocolTCP atau UDP
$statusStatus stream session, bukan HTTP status
$session_timeDurasi session client-NGINX
$upstream_addrUpstream yang dipilih
$upstream_connect_timeWaktu connect ke upstream
$upstream_session_timeDurasi session upstream
$bytes_sent/receivedVolume traffic

Untuk stream, RCA sering dimulai dari:

  1. Apakah client bisa connect ke NGINX?
  2. Apakah NGINX bisa connect ke upstream?
  3. Apakah session ditutup client, upstream, atau timeout?
  4. Apakah distribusi upstream seimbang?
  5. Apakah ada long-lived session skew?
  6. Apakah backend melihat IP yang benar?
  7. Apakah protocol membutuhkan L7 awareness yang NGINX stream tidak punya?

14. Stream access control

Stream punya access control sendiri.

Contoh allowlist sederhana:

stream {
    server {
        listen 5432;

        allow 10.0.0.0/8;
        allow 192.168.0.0/16;
        deny all;

        proxy_pass postgres_pool;
    }
}

Namun ingat Real IP boundary. Jika NGINX berada di belakang load balancer, $remote_addr mungkin IP load balancer. Untuk menerima IP asli di stream, gunakan PROXY protocol bila upstream LB mengirimkannya.

stream {
    server {
        listen 5432 proxy_protocol;
        proxy_pass postgres_pool;
    }
}

PROXY protocol akan dibahas lebih detail di Part 087.


15. Stream bukan pengganti protocol-aware proxy

NGINX stream adalah generic TCP/UDP proxy. Ia tidak otomatis memahami protocol seperti PostgreSQL, MySQL, Redis, Kafka, MQTT, DNS, atau FTP.

Konsekuensi:

  • Tidak bisa route SQL read/write berdasarkan query.
  • Tidak bisa paham Redis cluster redirection.
  • Tidak bisa inspect Kafka topic.
  • Tidak bisa split MySQL read/write secara semantic.
  • Tidak bisa tahu LDAP bind berhasil/gagal sebagai health signal.
  • Tidak cocok untuk protocol yang membuka dynamic secondary connection kecuali dipahami network path-nya.

Kalau butuh protocol semantics, pertimbangkan:

  • proxy khusus protocol tersebut
  • service mesh/L4 LB dengan metadata memadai
  • database-native proxy
  • driver/client-level load balancing
  • application-level routing

NGINX stream kuat untuk generic L4 forwarding, TLS passthrough, dan simple L4 load balancing. Jangan memaksanya menjadi protocol-aware broker.


16. Failure model stream

Top production failure modes:

FailureCommon root causeMitigation
All clients fail connectwrong listener, firewall, module missingnginx -T, ss -ltnp, security group test
Only some upstreams failpassive failure, backend down, DNS stalelog $upstream_addr, test direct upstream
Backend overloadedconnection skew, no max_conns, long sessionsleast_conn, pool sizing, backend limits
Client IP lostNGINX source IP seen by backendPROXY protocol or app identity
TLS SNI route wrongclient did not send SNI, map default badlog $ssl_preread_server_name
Reload does not move trafficold TCP sessions continuedesign drain/connection expiry at app/protocol level
Auth not enforcedassumed HTTP auth works in streamenforce with network/mTLS/protocol auth

17. Minimal production skeleton

# /etc/nginx/nginx.conf
worker_processes auto;

error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

events {
    worker_connections 8192;
}

http {
    # Existing HTTP config omitted.
}

stream {
    log_format stream_json escape=json
      '{'
        '"ts":"$time_iso8601",'
        '"remote_addr":"$remote_addr",'
        '"protocol":"$protocol",'
        '"status":"$status",'
        '"bytes_sent":$bytes_sent,'
        '"bytes_received":$bytes_received,'
        '"session_time":$session_time,'
        '"upstream_addr":"$upstream_addr",'
        '"upstream_connect_time":"$upstream_connect_time",'
        '"upstream_session_time":"$upstream_session_time"'
      '}';

    access_log /var/log/nginx/stream-access.log stream_json;

    include /etc/nginx/stream.d/*.conf;
}

Example service config:

# /etc/nginx/stream.d/redis.conf
upstream redis_cache_pool {
    least_conn;
    zone redis_cache_pool 64k;

    server 10.20.0.11:6379 max_fails=2 fail_timeout=10s;
    server 10.20.0.12:6379 max_fails=2 fail_timeout=10s;
}

server {
    listen 6379;

    proxy_connect_timeout 1s;
    proxy_timeout 5m;

    proxy_pass redis_cache_pool;
}

Review checklist:

  • Is stream module present?
  • Is this traffic truly non-HTTP or intentionally opaque TLS?
  • Does the backend require original client IP?
  • Does backend support PROXY protocol if needed?
  • Are timeouts appropriate for the protocol?
  • Are logs sufficient for RCA?
  • Are failure and reconnect behaviors understood by clients?
  • Are existing sessions expected to survive reload?
  • Is access controlled at network/mTLS/protocol level?

18. Smoke testing stream config

Validate config:

nginx -t
nginx -T | sed -n '/^stream {/,/^}/p'

Check listener:

ss -ltnp | grep ':5432'

TCP connect test:

nc -vz nginx.example.internal 5432

TLS passthrough SNI test:

openssl s_client \
  -connect nginx.example.com:443 \
  -servername db1.example.com \
  -brief

PostgreSQL-level test:

psql "host=nginx.example.internal port=5432 user=app dbname=appdb sslmode=require" -c 'select 1'

Redis-level test:

redis-cli -h nginx.example.internal -p 6379 PING

Remember: nc only proves TCP connect. It does not prove protocol correctness.


19. Design heuristics

Use this as a decision table:

RequirementRecommended subsystem
Route by URI/pathhttp
Add/strip HTTP headershttp
Cache responsehttp
Terminate HTTPS and apply API policyhttp
Pass raw TLS to backendstream
Proxy PostgreSQL/MySQL/Redisstream or protocol-specific proxy
Load balance DNS UDPstream
Preserve client IP to backendstream + PROXY protocol if backend supports it
Route TLS by SNI without decryptingstream + ssl_preread
Need database read/write splittingUsually not generic NGINX stream
Need per-query SQL observabilityProtocol-aware/database layer, not NGINX stream

20. Mental model summary

NGINX stream is best understood as a programmable socket-level traffic gate.

It is excellent when:

  • protocol is not HTTP
  • routing is listener/SNI/IP based
  • L4 load balancing is enough
  • TLS passthrough is required
  • generic TCP/UDP proxying solves the problem

It is dangerous when:

  • you need HTTP semantics but choose stream for simplicity
  • you assume source IP is preserved
  • you assume NGINX understands database/application correctness
  • you ignore long-lived connection skew
  • you copy HTTP timeout/retry/security assumptions into stream

Core invariant:

In stream, NGINX does not manage requests. It manages sessions and bytes.

That one sentence prevents most bad designs.


References

Lesson Recap

You just completed lesson 83 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.