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 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.0means “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.xto172.31.x.x, and192.168.x.xare 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.
| Port | Usually |
|---|---|
| 22 | SSH |
| 53 | DNS |
| 80 / 443 | HTTP / HTTPS |
| 3000 | Local dev servers such as next dev |
| 5432 | Postgres |
| 6543 | Supabase connection pooler (transaction mode) |
| 6379 | Redis |
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.
| TCP | UDP | |
|---|---|---|
| Setup | Handshake first: one round trip | None. Just send. |
| Order | Bytes arrive in the order sent | Packets can arrive in any order |
| Loss | Lost data is sent again | A lost packet stays lost |
| Shape | One long stream of bytes | Separate messages (datagrams) |
| Used by | HTTP/1.1, HTTP/2, SSH, Postgres | DNS, video calls, games, QUIC (HTTP/3) |
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.
| Record | Holds | Example |
|---|---|---|
| A | An IPv4 address | api.example.com → 198.51.100.7 |
| AAAA | An IPv6 address | api.example.com → 2001:db8::7 |
| CNAME | “Use this other name instead” | www → cname.vercel-dns.com |
| MX | The mail server for the domain | example.com → mx.mailhost.net |
| TXT | Free text: domain proofs, email rules (SPF) | "v=spf1 include:_spf.example.net ~all" |
| NS | Which servers hold the records | example.com → ns1.dnshost.net |
Try it: dig +trace api.example.com prints every hop. dig +short api.example.com prints only the answer.
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.
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.
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.
| Status | Means | Common ones |
|---|---|---|
| 2xx | It worked | 200 OK · 201 Created · 204 No Content |
| 3xx | Look somewhere else | 301 Moved Permanently · 304 Not Modified (use your cached copy) |
| 4xx | The request is wrong | 400 Bad Request · 401 not logged in · 403 not allowed · 404 Not Found · 409 Conflict · 429 Too Many Requests |
| 5xx | The server failed | 500 Internal Server Error · 502 Bad Gateway · 503 Unavailable · 504 Gateway Timeout |
| Method | Use | Safe | Idempotent |
|---|---|---|---|
| GET | Read | yes | yes |
| PUT | Replace with exactly this | no | yes |
| DELETE | Remove | no | yes |
| POST | Create, or run an action | no | no |
| PATCH | Change some fields | no | not 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.
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.
What someone on the same café Wi-Fi sees
Everything is readable: the page, the cookie that keeps you logged in, and the password. Anyone on the path can also change it.
Still visible: the IP, the port, the site name (SNI), and the size and timing of each message. Hidden: the path, the headers, the cookie and the body.
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.
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.
The two trust lists
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.
“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.
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
Listen there, connect from here. The bastion listens on port 8080. Each connection to it travels back through SSH to your laptop, which connects to localhost:3000. Use it to show a dev server to someone, or to receive a webhook. Your laptop needs no open ports, because it dialed out.
ssh -N -R 8080:localhost:3000 ada@bastion.example.com
# the bastion needs "GatewayPorts yes" in sshd_config,
# or it listens on its own 127.0.0.1 only
Listen here, connect anywhere. Your laptop runs a SOCKS proxy on port 1080. Each app that uses it says where it wants to go, and the bastion dials that place. One tunnel covers a whole network, for every app that can use a SOCKS proxy.
ssh -N -D 1080 ada@bastion.example.com
# then:
curl --socks5-hostname localhost:1080 http://grafana.internal:3000
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.
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| Flag | Means |
|---|---|
-L | Listen on a local port. Forward to a host the server can reach. |
-R | Listen on a server port. Forward back to a host your laptop can reach. |
-D | Run a SOCKS proxy on a local port. |
-N | Do not open a shell. Only hold the tunnel. |
-f | Go to the background after login. |
-J host | Jump through this host first (ProxyJump). |
-o ServerAliveInterval=30 | Send 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?
| Tool | What goes through | Who sets it up | Typical use |
|---|---|---|---|
ssh -L | One port to one destination | You, one tunnel at a time | Reach one private database |
ssh -D | Any TCP connection from apps that use SOCKS | You | Browse internal web tools |
| VPN | All traffic from every app, TCP and UDP | An admin, once | Work inside a private network |
| Reverse proxy | Requests coming in to your servers | The site owner | TLS, caching, load balancing, one public address |
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.
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}
| Style | Format | Transport | Where you meet it |
|---|---|---|---|
Supabase rpc() | JSON | HTTP | Calling a Postgres function from the app |
| gRPC | Protocol Buffers (binary) | HTTP/2 | Calls between back-end services, with generated clients |
| JSON-RPC 2.0 | JSON | Any: HTTP, stdio, WebSocket | MCP servers, language servers (LSP) |
| tRPC, Server Actions | JSON | HTTP | TypeScript 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?
| Goal | REST: things and verbs | RPC: named actions |
|---|---|---|
| Read a user | GET /users/42 | POST /rpc/getUser {"id":42} |
| Rename | PATCH /users/42 {"name":"Ada"} | POST /rpc/renameUser {"id":42,"name":"Ada"} |
| Send a reset email | POST /users/42/password-resets | POST /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.
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 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; }:
08is field 1, type “number”. The byte is(field << 3) | type, so(1 << 3) | 0 = 8.2Ais the value 42.12is field 2, type “length then bytes”:(2 << 3) | 2 = 18, which is12in hex.03says 3 bytes follow.41 64 61is “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
bigintloses digits inJSON.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.stringifydrops fields that areundefined. 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.
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.
| Delivery | How | Risk |
|---|---|---|
| At most once | Send once. Never retry. | Work can be lost |
| At least once | Retry until a reply comes | Work can happen twice |
| Exactly once | Not 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.
| Way | Good | Bad | Use for |
|---|---|---|---|
| Polling | Simple. Works with any server. | Late, and most requests return nothing | Data that changes every few minutes |
| Long polling | Near real time over plain HTTP | One new request per message | Old stacks, a fallback |
| SSE | One HTTP stream. The browser reconnects by itself (EventSource). | Server to client only | Live feeds, streaming AI replies token by token |
| WebSocket | Both ways, small cost per message | The server must hold many open connections | Chat, collaboration, Supabase Realtime |
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 | |
|---|---|---|---|
| Memory | Separate | Shared | Shared, one thread |
| Truly at once? | Yes | Yes, on many cores | No, tasks take turns |
| Cost to start | High | Medium | Very low |
| Main danger | Slow to share data | Race conditions; needs locks | One slow, blocking step stops everything |
| Examples | Postgres: one process per connection | Java servers, Go goroutines | Node.js, browser JavaScript, Python asyncio |
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
- console.log('A');
- setTimeout(() => console.log('B'), 0);
- Promise.resolve().then(() => console.log('C'));
- console.log('D');
Console
Call stack
Microtasks
Tasks
The whole script runs as one task. Guess the output, then press Next.
- Code after
awaitruns 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.
SELECT tickets_leftreads 1. Enough, so sell.SELECT tickets_leftreads 1. Enough, so sell.UPDATE ... SET tickets_left = 0soldUPDATE ... SET tickets_left = 0sold a ticket that does not existTwo tickets sold, one existed. B decided on a value that was already old. This is a lost update.
BEGIN; SELECT tickets_left ... FOR UPDATEreads 1 and locks the rowBEGIN; SELECT tickets_left ... FOR UPDATEwaits for A's lockUPDATE ... SET tickets_left = 0; COMMITsold. Lock released.(the SELECT returns)reads 0: sold out, no saleOne ticket sold. B waited, then read the real value. The lock turns read, decide and write into one step.
UPDATE events SET tickets_left = tickets_left - 1 WHERE id = 7 AND tickets_left > 01 row changed: soldUPDATE events SET tickets_left = tickets_left - 1 WHERE id = 7 AND tickets_left > 00 rows changed: sold outOne ticket sold. The check (tickets_left > 0) and the write are one statement, so nothing can run between them. Use this when the rule fits in one statement.
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 -Dspeaks 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.