A field guide, from the wire to one JavaScript thread

How Machines Talk

Every request your app makes passes through the same few layers. This guide draws each one. Click a step in a diagram to see it alone, or step through it. Where you see a toggle, switch modes and watch what changes.

One request, every layer

IP packetfinds the machine TCP segmentfinds the program, keeps bytes in order TLS recordkeeps it private and unchanged HTTP requestsays what you want RPC calladd(2, 3) IP headerto 198.51.100.7 TCP headerto port 443 TLS headertype, length HTTP headPOST /rest/v1/rpc/add Body{"a":2,"b":3} TLS tagtamper seal every router on the path can read these encrypted: only the two ends can read this
One call to supabase.rpc('add', {a: 2, b: 3}) as it leaves your laptop. Each layer wraps the one inside it and adds its own header. Click a layer name to jump to its section.
Contents
Part 1

The wire

How bytes find a machine, then a program, then a safe path between them.

01IPv4 RFC 791Private ranges RFC 1918

Addresses and ports

An IP address finds a machine. A port finds one program on that machine. So 198.51.100.7:443 means “the program on port 443 of machine 198.51.100.7”.

Your laptop private IP 192.168.1.20 browser port 51234 Home router NAT swaps in its public 203.0.113.7 from :51234 to :443 Server 198.51.100.7 :22 sshd remote shell :443 nginx web server :5432 postgres listens on 127.0.0.1 from outside: no answer
The browser reaches nginx on port 443. Postgres runs on the same server, but it listens only on 127.0.0.1, so only programs on that server can reach it. Remember this picture for the tunnels section.

Your side of a connection gets a port too. The operating system picks a free one at random. This is an ephemeral port. One connection is four numbers: your IP, your port, their IP, their port. A program holds its end of a connection as a socket.

Three kinds of address to know

  • 127.0.0.1, also called localhost or loopback, is the machine itself. Traffic to it never leaves the machine. A program that listens on it can only be reached from the same machine.
  • 0.0.0.0 means “every network this machine is on”. A program that listens on it accepts connections from anywhere that can reach the machine.
  • 10.x.x.x, 172.16.x.x to 172.31.x.x, and 192.168.x.x are private. They work inside a home or cloud network, never on the public internet. Your router uses NAT (network address translation) to swap your private address for its one public address on the way out. NAT is also why nobody on the internet can start a connection to your laptop.
PortUsually
22SSH
53DNS
80 / 443HTTP / HTTPS
3000Local dev servers such as next dev
5432Postgres
6543Supabase connection pooler (transaction mode)
6379Redis

Try it: lsof -iTCP -sTCP:LISTEN -n -P lists every program that listens on your Mac, with its address and port.

02TCP RFC 9293UDP RFC 768

TCP and UDP

IP alone moves single packets with no promises. A packet can arrive late, out of order, twice, or not at all. TCP and UDP sit on top of IP and make different trade-offs.

Client Server :443 1 SYN · seq=1000 2 SYN-ACK · seq=5000 ack=1001 3 ACK · ack=5001 1 roundtrip 4 data · bytes 1001 to 1100 5 bytes 1101 to 1200lost timer ran out6 resend · bytes 1101 to 1200 7 ACK · ack=1201
TCPUDP
SetupHandshake first: one round tripNone. Just send.
OrderBytes arrive in the order sentPackets can arrive in any order
LossLost data is sent againA lost packet stays lost
ShapeOne long stream of bytesSeparate messages (datagrams)
Used byHTTP/1.1, HTTP/2, SSH, PostgresDNS, video calls, games, QUIC (HTTP/3)
Gotcha

Head-of-line blocking. TCP delivers bytes in order. If one packet is lost, every byte behind it waits for the resend, even bytes that belong to a different request. HTTP/3 moved to QUIC, which runs on UDP and keeps each stream separate. A loss then stalls only its own stream.

03RFC 1034 and 1035Port 53

DNS

People use names. Packets need IP addresses. DNS (the Domain Name System) turns one into the other. It is a tree of servers, and each server only knows the next step down.

Your laptopbrowser and OS Resolver1.1.1.1 Roota.root-servers.net .com serversgtld-servers.net Authoritativens1.dnshost.net 1 api.example.com? 2 api.example.com? 3 ask .com servers 4 api.example.com? 5 ask ns1.dnshost.net 6 api.example.com? 7 A 198.51.100.7 · TTL 300 8 198.51.100.7keeps the answer for 300 s
RecordHoldsExample
AAn IPv4 addressapi.example.com → 198.51.100.7
AAAAAn IPv6 addressapi.example.com → 2001:db8::7
CNAME“Use this other name instead”www → cname.vercel-dns.com
MXThe mail server for the domainexample.com → mx.mailhost.net
TXTFree text: domain proofs, email rules (SPF)"v=spf1 include:_spf.example.net ~all"
NSWhich servers hold the recordsexample.com → ns1.dnshost.net

Try it: dig +trace api.example.com prints every hop. dig +short api.example.com prints only the answer.

Gotcha

DNS changes are slow on purpose. Old answers stay in caches until their TTL runs out. Lower the TTL a day before you move a site. Raise it again after the move.

04HTTP semantics RFC 9110Ports 80 and 443

HTTP

HTTP is the language of requests and replies on the web. HTTP/1.1 is plain text, so you can read it line by line. HTTP/2 and HTTP/3 carry the same meaning in a binary form.

Request
POST /rest/v1/rpc/add HTTP/1.1Method, path, version. POST means “do something”. Host: xyz.supabase.coWhich site. One IP can serve many sites. Authorization: Bearer eyJhbGciOi...Who you are. A token, sent with every request. Content-Type: application/jsonHow to read the body. Content-Length: 13Body size in bytes. An empty line ends the headers. {"a":2,"b":3}The body: the arguments.
Response
HTTP/1.1 200 OKStatus code. 2xx means it worked. Content-Type: application/jsonHow to read the body. Cache-Control: no-storeRules for caches along the way. End of headers. 5The body: the return value of add(2, 3).

HTTP is stateless. The server forgets you after each reply. Cookies and tokens bring “who you are” back with every request.

StatusMeansCommon ones
2xxIt worked200 OK · 201 Created · 204 No Content
3xxLook somewhere else301 Moved Permanently · 304 Not Modified (use your cached copy)
4xxThe request is wrong400 Bad Request · 401 not logged in · 403 not allowed · 404 Not Found · 409 Conflict · 429 Too Many Requests
5xxThe server failed500 Internal Server Error · 502 Bad Gateway · 503 Unavailable · 504 Gateway Timeout
MethodUseSafeIdempotent
GETReadyesyes
PUTReplace with exactly thisnoyes
DELETERemovenoyes
POSTCreate, or run an actionnono
PATCHChange some fieldsnonot always

Safe means the request changes nothing on the server. Idempotent means that if you send it twice, the effect is the same as if you sent it once. This matters a lot when a request times out and you want to retry. See section 11.

Gotcha

502 and 504 come from a proxy in front of your app. 502: your app sent back something broken, or closed the connection. 504: your app did not answer in time. Look in your app's logs first, not the proxy's.

05TLS 1.3 RFC 8446Port 443

TLS and HTTPS

HTTPS is HTTP inside TLS. TLS gives three things. Privacy: nobody on the path can read the data. Integrity: nobody can change it without being caught. Identity: you are talking to the real api.example.com.

Client api.example.com :443 1 ClientHello · key share · SNI: api.example.com 2 ServerHello · key share · certificate · signature 1 roundtrip 3 check the certificatetrusted CA? valid dates? right name? 4 both sides compute the same secret key 5 Finished · encrypted request encrypted response

What someone on the same café Wi-Fi sees

198.51.100.7 port 80 · 412 bytes POST /login HTTP/1.1 Host: shop.example.com Cookie: session=8f2c41d9e07a Content-Type: application/x-www-form-urlencoded email=ada%40example.com&password=hunter2

Everything is readable: the page, the cookie that keeps you logged in, and the password. Anyone on the path can also change it.

Encryption alone is not enough. If you encrypt to the wrong party, you hand your secrets to them. The certificate chain links the site's key to a root CA that ships with your operating system or browser. That is how the browser knows the key belongs to the name.

Part 2

Getting inside

How you log in to a machine far away, and how you reach the machines behind it.

06RFC 4251 to 4254Port 22

SSH

SSH (Secure Shell) gives you an encrypted connection to a remote machine. You use it for a shell, to copy files (scp, rsync), for git push, and for tunnels. It makes two trust checks, one in each direction. Your laptop checks the server. Then the server checks you.

Your laptop Server sshd :22 1 TCP connect to port 22 2 host key SHA256:Qm4x...r9Tc is this key in known_hosts? 3 key exchange: same session key on both sides 4 log in as ada with key id_ed25519.pub listed in authorized_keys? 5 key is allowed: send a signature sign with the private key 6 signature 7 valid: shell opens

The two trust lists

Your laptop~/.ssh/ id_ed25519private key. Never leaves here. id_ed25519.pubpublic key. Safe to share. known_hostshost keys of servers you trust configshort names and settings Server/etc/ssh and ~ada/.ssh ssh_host_ed25519_keyserver private key authorized_keyspublic keys that may log in ssh_host_ed25519_key.pubhost key, sent to every client sshd_configserver settings ssh-copy-id adds it you accept it on first connect
Each side keeps a list of the other side's public keys. Your public key goes into the server's authorized_keys. The server's public host key goes into your known_hosts. Private keys (red) never move.

A key pair is two linked keys. The private key makes signatures. The public key can check them, but it cannot make them. So you can give your public key to every server and keep the private key on your laptop.

ssh-keygen -t ed25519 -C "ada@laptop"   # make a key pair
ssh-copy-id ada@server.example.com       # add your public key to the server
ssh -v ada@server.example.com            # connect and print every step

~/.ssh/config saves typing. With this file, ssh db-box hops through the bastion by itself:

Host bastion
  HostName bastion.example.com
  User ada
  IdentityFile ~/.ssh/id_ed25519

Host db-box
  HostName 10.0.2.15        # private IP, only the bastion can reach it
  User ada
  ProxyJump bastion

A bastion (or jump host) is the one machine that accepts SSH from the internet. Everything else stays private behind it.

Gotcha

“REMOTE HOST IDENTIFICATION HAS CHANGED” means the server sent a different host key from the one in known_hosts. Often the server was rebuilt. Sometimes someone is in the middle. Confirm the new fingerprint with the server's owner, then run ssh-keygen -R hostname to remove the old entry.

07Port forwarding RFC 4254 section 7SOCKS5 RFC 1928

SSH tunnels

One SSH connection can carry other connections inside it. This is a tunnel. Use it to reach a port that you cannot reach directly, such as a database inside a private network.

In all three modes, your laptop dials the SSH server. The modes differ only in which end listens and which end makes the last hop.

private network · port 5432 is closed to the internet SSH · port 22 you dial out Your laptop bastion bastion.example.com db.internal no public IP psql listens on:5433 sshd opens a newconnection TCP to :5432 :5432 postgres

Listen here, connect from there. Your laptop listens on port 5433. Each connection to it travels through SSH. The bastion then opens a new connection to db.internal:5432. Postgres sees a client that comes from the bastion, which it allows.

ssh -N -L 5433:db.internal:5432 ada@bastion.example.com
# then, in another terminal:
psql -h localhost -p 5433 -U app app

How to read the spec

-L 5433:db.internal:5432 reads as: listen on 5433 here, then send to db.internal:5432 as the far end sees it. -R 8080:localhost:3000 is the mirror image: listen on 8080 there, send to localhost:3000 as your laptop sees it.

Gotcha

In -L, the far end resolves the destination. So -L 5433:localhost:5432 reaches Postgres on the bastion itself, not on your laptop. And with --socks5-hostname, the bastion also does the DNS lookup, so internal names work.

Build an -L command

ssh -N -L 5433:db.internal:5432 ada@bastion.example.com
# then point your app at localhost:5433

FlagMeans
-LListen on a local port. Forward to a host the server can reach.
-RListen on a server port. Forward back to a host your laptop can reach.
-DRun a SOCKS proxy on a local port.
-NDo not open a shell. Only hold the tunnel.
-fGo to the background after login.
-J hostJump through this host first (ProxyJump).
-o ServerAliveInterval=30Send a keep-alive every 30 s, so idle tunnels do not drop.

08Forward proxyReverse proxyVPN

Proxies and VPNs

A proxy is a middleman that makes connections for someone else. The important question is: for whom?

Forward proxy works for the clients example: ssh -D, office proxy your apps forward proxy websites sites see the proxy's IP Reverse proxy works for the servers example: nginx, Cloudflare, Vercel users reverse proxy app servers users see one address VPN joins you to a network example: WireGuard, Tailscale encrypted laptop VPN gateway private network db wiki ci every app, every port
The same middleman shape, three jobs. A forward proxy hides the clients. A reverse proxy hides the servers. A VPN makes your laptop act as if it were plugged into the far network.
ToolWhat goes throughWho sets it upTypical use
ssh -LOne port to one destinationYou, one tunnel at a timeReach one private database
ssh -DAny TCP connection from apps that use SOCKSYouBrowse internal web tools
VPNAll traffic from every app, TCP and UDPAn admin, onceWork inside a private network
Reverse proxyRequests coming in to your serversThe site ownerTLS, caching, load balancing, one public address
Part 3

Programs calling programs

How one program asks another to do work, how the data travels, and what to do when the network fails in the middle.

09Remote procedure callIdea first written down in RFC 707

RPC

RPC (remote procedure call) lets your code call a function that runs on another machine, as if it were a local function. A piece of code called a stub hides the network. A library gives you the stub, or a tool generates it.

network Your codeapp.ts Client stubsupabase-js Server stubPostgREST The functionin Postgres 1 add(2, 3) {"a":2,"b":3}2 3 POST /rpc/add parse JSON 4 add(2, 3) 5 returns 5 6 200 OK · 5 7 5

The same call in code. You write two pieces, and the stubs do the rest:

-- in Postgres
create function add(a int, b int) returns int
language sql as $$ select a + b $$;

// in your app
const { data } = await supabase.rpc('add', { a: 2, b: 3 })   // data === 5

# on the wire
POST https://xyz.supabase.co/rest/v1/rpc/add
{"a":2,"b":3}
StyleFormatTransportWhere you meet it
Supabase rpc()JSONHTTPCalling a Postgres function from the app
gRPCProtocol Buffers (binary)HTTP/2Calls between back-end services, with generated clients
JSON-RPC 2.0JSONAny: HTTP, stdio, WebSocketMCP servers, language servers (LSP)
tRPC, Server ActionsJSONHTTPTypeScript apps, Next.js

A gRPC service is defined in a .proto file. A tool generates the client and server stubs in each language from it:

service Calc {
  rpc Add (AddRequest) returns (AddReply);
}
message AddRequest { int32 a = 1; int32 b = 2; }
message AddReply   { int32 sum = 1; }

RPC or REST?

GoalREST: things and verbsRPC: named actions
Read a userGET /users/42POST /rpc/getUser {"id":42}
RenamePATCH /users/42 {"name":"Ada"}POST /rpc/renameUser {"id":42,"name":"Ada"}
Send a reset emailPOST /users/42/password-resetsPOST /rpc/sendPasswordReset {"id":42}

REST fits data that looks like records. RPC fits actions that do not map to one record. Many APIs mix both. Supabase does: tables are REST, functions are RPC.

Gotcha

A remote call is not a local call. A local call takes nanoseconds and never fails halfway. A remote call takes milliseconds, about a million times longer, and it can fail in the middle. Two habits follow. Do not call RPC in a loop when one batch call can do the job. And decide what happens on a timeout (section 11).

10JSON RFC 8259Protocol Buffers

Serialization

Memory holds objects. Networks carry bytes. Serialization turns an object into bytes. Deserialization turns the bytes back into an object. Both ends must agree on the format. Here is the same user, { id: 42, name: "Ada" }, in two formats.

JSON · text22 bytes
{"id":42,"name":"Ada"}
field namesvaluespunctuation
Protobuf · binary7 bytes
082A1203416461
field number and typelengthvalues

JSON repeats the field names, as text, in every message. Protobuf sends only field numbers and values, packed tight. The cost: both sides need the same schema file to make sense of the bytes.

How to read the protobuf bytes, with the schema message User { int32 id = 1; string name = 2; }:

  • 08 is field 1, type “number”. The byte is (field << 3) | type, so (1 << 3) | 0 = 8.
  • 2A is the value 42.
  • 12 is field 2, type “length then bytes”: (2 << 3) | 2 = 18, which is 12 in hex.
  • 03 says 3 bytes follow. 41 64 61 is “Ada” in ASCII.

Traps that bite in practice

  • Big integers. A JavaScript number is exact only up to 253 − 1 (9,007,199,254,740,991). A larger Postgres bigint loses digits in JSON.parse. Send big IDs as strings.
  • Dates. JSON has no date type. Send ISO 8601 strings with a time zone, such as "2026-10-22T14:05:00Z".
  • Missing fields. JSON.stringify drops fields that are undefined. So {a: undefined} becomes {}, which is not the same as {"a": null}.
  • Schema changes. Protobuf survives new fields if you never reuse a field number. A JSON reader breaks if you rename a key that it expects.

11Idempotent methods RFC 9110 section 9.2.2

Retries and idempotency

A call times out. Did the server do the work? The client cannot know. Maybe the request was lost. Maybe the server did the work and the reply was lost. From the client's side, both look the same.

Client Payment API POST /charge $20 charge the card (1st) 200 OK reply lost no reply in 10 s: retry POST /charge $20 (retry) charge the card (2nd) 200 OK Result: card charged twice, $40

Switch modes to see the one box that changes. The client behaves the same both times. Only the server's memory of the key is different.

DeliveryHowRisk
At most onceSend once. Never retry.Work can be lost
At least onceRetry until a reply comesWork can happen twice
Exactly onceNot possible over an unreliable network by itself. Get the same effect with at-least-once delivery plus an idempotent receiver.None, if the key check is atomic

To make the receiver idempotent, the client makes one unique key per operation, and sends the same key on every retry. The server stores the key with the result. A unique constraint does the hard part:

insert into charges (idempotency_key, amount_cents)
values ('7f3a...', 2000)
on conflict (idempotency_key) do nothing
returning id;
-- no row back means: already done. Look up the saved result and return it.

Back off between retries. Wait longer after each failure (1 s, 2 s, 4 s) and add a random part, called jitter. Without jitter, every client that failed at the same moment retries at the same moment, and they all hit the server together again.

12WebSocket RFC 6455Server-sent events: HTML standard

Push and pull

HTTP is request, then reply. The server cannot speak first. When the server has news, there are four common ways to get it to the client. The dashed green lines show when new data appears on the server.

new data 2 more Pollingask every 2 s 1.7 s late up to 1.9 s late Long pollingserver holds each request SSEone stream, server to you WebSocketone socket, both ways 0 s2 s4 s6 s8 s10 s
Blue bars are open requests or connections. Green dots are data from the server. Blue dots on the WebSocket line are messages from the client. Polling wastes most requests and is always late. The other three deliver at once.
WayGoodBadUse for
PollingSimple. Works with any server.Late, and most requests return nothingData that changes every few minutes
Long pollingNear real time over plain HTTPOne new request per messageOld stacks, a fallback
SSEOne HTTP stream. The browser reconnects by itself (EventSource).Server to client onlyLive feeds, streaming AI replies token by token
WebSocketBoth ways, small cost per messageThe server must hold many open connectionsChat, collaboration, Supabase Realtime
Part 4

Inside one machine

How one program does many jobs at once without breaking its own data.

13Concurrency models

Processes, threads, async

A program that must do many jobs at once can split the work in three ways. First, two words. Concurrency means many jobs are in progress at the same time. Parallelism means many jobs run at the same instant, on different CPU cores. Async gives you concurrency on one core. Threads and processes can give you both.

Processes Threads Async, one thread process 4120 ownmemory 1 thread pipe or socket process 4121 ownmemory 1 thread process 4200 shared memory thread 1core 1 thread 2core 2 thread 3core 3 process 4300 the one thread A B C A C B A waiting on network or disk A B C one task runs at a time a task gives way at each await the OS does the waiting
Processes are walled off from each other. Threads share one memory, so they are fast to share data and easy to break. Async runs one task at a time on one thread. A task gives up the thread each time it waits.
ProcessesThreadsAsync
MemorySeparateSharedShared, one thread
Truly at once?YesYes, on many coresNo, tasks take turns
Cost to startHighMediumVery low
Main dangerSlow to share dataRace conditions; needs locksOne slow, blocking step stops everything
ExamplesPostgres: one process per connectionJava servers, Go goroutinesNode.js, browser JavaScript, Python asyncio
Gotcha

Why Postgres needs a connection pooler. Postgres starts one process for each connection. 500 connections means 500 processes, each with its own memory. A pooler such as PgBouncer or Supavisor lets thousands of app requests share a few dozen real connections.

14Event loops: HTML standard

The event loop

JavaScript runs your code on one thread. The event loop decides what runs next. It keeps two waiting lines. Microtasks are promise callbacks and the code after an await. Tasks are timers, network events and clicks. The rule: run one task to the end, then run every microtask, then take the next task.

What does this script print? Guess first, then step through it.

script.js

  1. console.log('A');
  2. setTimeout(() => console.log('B'), 0);
  3. Promise.resolve().then(() => console.log('C'));
  4. console.log('D');

Console

Call stack
    Microtasks
      Tasks

        The whole script runs as one task. Guess the output, then press Next.

        • Code after await runs later, as a microtask. Other code can run and change your data in between.
        • A long synchronous loop blocks everything. No clicks, no timers and no network replies are handled until it ends. In a server, every user waits.

        15PostgreSQL explicit lockingSELECT ... FOR UPDATE

        Race conditions and locks

        A race condition happens when the result depends on who goes first. The classic form is check, then act: read a value, decide, then write. Between the read and the write, someone else can change the value. Here, two workers sell the last ticket.

        Time
        Worker A
        tickets_left
        Worker B
        t1
        SELECT tickets_leftreads 1. Enough, so sell.
        1
        t2
        1
        SELECT tickets_leftreads 1. Enough, so sell.
        t3
        UPDATE ... SET tickets_left = 0sold
        0
        t4
        0
        UPDATE ... SET tickets_left = 0sold a ticket that does not exist

        Two tickets sold, one existed. B decided on a value that was already old. This is a lost update.

        Queues: take a job without waiting

        Workers that pull jobs from a table should not wait on each other. SKIP LOCKED makes each worker skip rows that another worker holds. Each worker gets a different job, and nobody waits.

        select id from jobs
        where status = 'queued'
        order by created_at
        limit 1
        for update skip locked;

        The same bug on one JavaScript thread

        Async code has no threads to fight, but await opens a gap between check and act:

        // Two callers can both pass the check before either one finishes.
        if (!cache[id]) {
          cache[id] = await loadUser(id);
        }
        
        // Fix: store the promise at once, with no await between check and write.
        cache[id] ??= loadUser(id);
        return cache[id];

        ··Reference

        Glossary

        Backoff
        Waiting longer after each failed retry, often with a random part (jitter).
        Bandwidth
        How many bytes per second a link can carry.
        Bastion
        Also called a jump host. The one machine that accepts SSH from the internet. You hop through it to reach private machines.
        Certificate authority (CA)
        A trusted party that signs certificates. Your operating system ships a list of root CAs.
        Connection pool
        A set of open database connections that many requests share.
        Ephemeral port
        The random port your side of a connection gets.
        Fingerprint
        A short hash of a public key, used to compare keys by eye.
        Handshake
        The first messages of a connection, where both sides agree on settings.
        Idempotent
        Doing it twice has the same effect as doing it once.
        Latency
        How long one message takes to arrive. Round-trip time (RTT) is there and back.
        Lock
        A marker that says “mine for now”. Others wait, or skip.
        Loopback
        127.0.0.1, the machine's address for itself.
        Microtask
        A callback that runs as soon as the current task ends, before the next task.
        NAT
        Network address translation. A router swaps private addresses for its one public address.
        Port forwarding
        Sending connections that arrive at one port on to another address. SSH tunnels are one kind.
        Serialization
        Turning an object in memory into bytes, and back.
        Socket
        A program's end of a connection: an address, a port and a protocol.
        SOCKS
        A simple proxy protocol. The app tells the proxy where to connect. ssh -D speaks it.
        Stub
        Code that makes a remote call look like a local one.
        TTL
        Time to live. How long a cached answer stays valid.

        All IP addresses come from the ranges set aside for documentation (RFC 5737). Names, keys and fingerprints are examples.