The Internet is reliable for a strange reason: its core is not. IP does not promise delivery, order, or even survival for a packet. Reliability comes from a layered design that keeps the network simple, lets routers route around damage, and pushes retransmission and ordering to the endpoints. That split is why the system scales and why it keeps working through loss, congestion, and broken links.
If two machines need to exchange bytes across cities, providers, routers, and cables, lots of things can go wrong. Links fail. Packets are dropped. Routers reboot. Bursts of traffic overflow queues. Paths change mid-transfer. If the network required every router to maintain perfect state for every flow, the system would be brittle and hard to scale.
The TCP/IP design chooses a different split. IP offers a simple packet delivery service: try to move this datagram toward its destination. TCP sits above that and turns many imperfect datagrams into a reliable byte stream using sequence numbers, acknowledgments, timers, and windows.
IP is deliberately modest. A router looks at a destination address, picks a next hop, decrements the packet’s TTL, and forwards it. It does not promise delivery. It does not keep the whole conversation in memory. Each packet is handled independently.
That simplicity is what makes failure resistance possible. If one fiber cut knocks out a path, routing can converge on another path without requiring every existing connection to be rebuilt from scratch. The packet might arrive late, out of order, or not at all, but the network can keep moving.
const graph = {
A: [{to: "B", cost: 1}, {to: "C", cost: 2}],
B: [{to: "A", cost: 1}, {to: "D", cost: 1}, {to: "E", cost: 3}],
C: [{to: "A", cost: 2}, {to: "D", cost: 1}],
D: [{to: "B", cost: 1}, {to: "C", cost: 1}, {to: "F", cost: 1}],
E: [{to: "B", cost: 3}, {to: "F", cost: 1}],
F: [{to: "D", cost: 1}, {to: "E", cost: 1}, {to: "G", cost: 1}],
G: [{to: "F", cost: 1}],
};
function edgeKey(a, b) {
return [a, b].sort().join("-");
}
function shortestPath(graph, start, goal, down = new Set()) {
const dist = new Map([[start, 0]]);
const prev = new Map();
const seen = new Set();
while (true) {
let node = null;
let best = Infinity;
for (const [name, d] of dist) {
if (!seen.has(name) && d < best) {
best = d;
node = name;
}
}
if (!node) break;
if (node === goal) break;
seen.add(node);
for (const {to, cost} of graph[node]) {
if (down.has(edgeKey(node, to))) continue;
const next = best + cost;
if (next < (dist.get(to) ?? Infinity)) {
dist.set(to, next);
prev.set(to, node);
}
}
}
if (!dist.has(goal)) return null;
const path = [];
for (let at = goal; at; at = prev.get(at)) path.push(at);
path.reverse();
return {path, cost: dist.get(goal)};
}
function sendPacket(ttl, path) {
for (let i = 1; i < path.length; i++) {
if (ttl === 0) return {ok: false, stoppedAt: path[i]};
ttl--;
}
return {ok: true, ttlLeft: ttl};
}
const healthy = shortestPath(graph, "A", "G");
console.log("Healthy:", healthy.path.join(" → "), "cost", healthy.cost);
const damaged = shortestPath(graph, "A", "G", new Set([edgeKey("D", "F")]));
console.log("Link D-F down:", damaged.path.join(" → "), "cost", damaged.cost);
const packet = sendPacket(8, damaged.path);
console.log("TTL result:", packet);
TCP assumes the network may drop, duplicate, damage, or reorder packets. It detects corruption, discards bad segments, and recovers at the endpoints with acknowledgments and retransmission. Every byte belongs to a sequence number range. The receiver acknowledges the next byte it still needs. If an ACK never arrives, the sender retransmits. If packets arrive out of order, the receiver can buffer them but still keep asking for the missing gap.
This is why the Internet can survive random failures without the core becoming complicated. Routers forward packets. TCP endpoints notice missing data and repair it themselves.
class Receiver {
constructor() {
this.expected = 1;
this.buffer = new Map();
this.delivered = [];
}
receive(segment) {
if (segment.seq < this.expected) return this.expected;
this.buffer.set(segment.seq, segment.data);
while (this.buffer.has(this.expected)) {
this.delivered.push(this.buffer.get(this.expected));
this.buffer.delete(this.expected);
this.expected++;
}
return this.expected;
}
}
class Sender {
constructor(chunks) {
this.chunks = chunks;
this.unacked = new Map();
this.nextSeq = 1;
}
sendNext() {
if (this.nextSeq > this.chunks.length) return null;
const seg = {seq: this.nextSeq, data: this.chunks[this.nextSeq - 1]};
this.unacked.set(seg.seq, seg);
this.nextSeq++;
return seg;
}
ack(n) {
for (const seq of [...this.unacked.keys()]) {
if (seq < n) this.unacked.delete(seq);
}
}
oldestUnacked() {
return this.unacked.get(Math.min(...this.unacked.keys()));
}
}
const sender = new Sender(["A", "B", "C"]);
const receiver = new Receiver();
const s1 = sender.sendNext();
const s2 = sender.sendNext();
const s3 = sender.sendNext();
console.log("send", s1, s2, s3);
let ack = receiver.receive(s1);
console.log("recv seq1 -> ACK", ack);
sender.ack(ack);
console.log("seq2 lost in network");
ack = receiver.receive(s3);
console.log("recv seq3 out of order -> duplicate ACK", ack);
const retry = sender.oldestUnacked();
console.log("timeout -> retransmit", retry);
ack = receiver.receive(retry);
console.log("recv retry -> ACK", ack);
sender.ack(ack);
console.log("delivered:", receiver.delivered.join(""));
Reliability is not enough. A fast sender can still overwhelm a slow receiver or flood the network. TCP therefore has two separate limits. The receive window says how much the receiver can buffer. The congestion window says how much data the sender believes the network can currently handle. The sender may only have min(rwnd, cwnd) bytes in flight.
This is another reason the architecture survives at scale: endpoints adapt to both machine capacity and path capacity without requiring routers to understand every application.
cwnd. If the receiver is slow, rwnd shrinks. Either way, the sender backs off.TCP/IP is not reliable because every component is perfect. It is reliable because imperfections are expected and isolated. IP gives the network a narrow job: move packets hop by hop. TCP gives the endpoints the richer job: detect missing data, reorder it, pace transmission, and retry when necessary.
That split creates three important engineering advantages.
Forwarding per packet is cheaper and more failure-tolerant than forcing every router to hold precise per-flow conversation state.
Because packets are independent, routing can adapt around damaged links while TCP continues repairing any loss at the ends.
The sender and receiver are the only places that can truly know whether the application’s bytes arrived in the right order.
The code below is not a full TCP implementation, but it captures the core contract. The sender chops a message into segments, tracks which ones are unacknowledged, retransmits after loss, and advances only when cumulative ACKs arrive. The receiver buffers out-of-order data and acknowledges the next gap.
class LossyLink {
constructor(dropFirst = []) {
this.dropFirst = new Set(dropFirst);
this.dropped = new Set();
}
transmit(segment) {
const key = segment.seq;
if (this.dropFirst.has(key) && !this.dropped.has(key)) {
this.dropped.add(key);
return null;
}
return {...segment};
}
}
class Receiver {
constructor() {
this.expected = 1;
this.buffer = new Map();
this.parts = [];
}
receive(segment) {
if (!segment) return this.expected;
if (segment.seq < this.expected) return this.expected;
this.buffer.set(segment.seq, segment.data);
while (this.buffer.has(this.expected)) {
this.parts.push(this.buffer.get(this.expected));
this.buffer.delete(this.expected);
this.expected++;
}
return this.expected;
}
text() {
return this.parts.join("");
}
}
class Sender {
constructor(message, chunkSize = 3, windowSize = 3) {
this.chunks = [];
for (let i = 0; i < message.length; i += chunkSize) {
this.chunks.push(message.slice(i, i + chunkSize));
}
this.windowSize = windowSize;
this.base = 1;
this.nextSeq = 1;
this.unacked = new Map();
}
hasData() {
return this.base <= this.chunks.length || this.unacked.size > 0;
}
sendable() {
return this.nextSeq < this.base + this.windowSize && this.nextSeq <= this.chunks.length;
}
makeSegment() {
const seg = {seq: this.nextSeq, data: this.chunks[this.nextSeq - 1]};
this.unacked.set(seg.seq, seg);
this.nextSeq++;
return seg;
}
acknowledge(ack) {
for (const seq of [...this.unacked.keys()]) {
if (seq < ack) this.unacked.delete(seq);
}
this.base = ack;
}
timeoutSegments() {
return [...this.unacked.values()].sort((a, b) => a.seq - b.seq);
}
}
function transfer(message) {
const sender = new Sender(message, 3, 3);
const receiver = new Receiver();
const link = new LossyLink([2, 4]);
const log = [];
while (sender.hasData()) {
let sentSomething = false;
while (sender.sendable()) {
const seg = sender.makeSegment();
sentSomething = true;
const arrived = link.transmit(seg);
log.push(arrived ? `send seq=${seg.seq} "${seg.data}"` : `drop seq=${seg.seq} "${seg.data}"`);
if (arrived) {
const ack = receiver.receive(arrived);
log.push(` receiver ACK ${ack}`);
sender.acknowledge(ack);
}
}
if (!sentSomething && sender.unacked.size) {
for (const seg of sender.timeoutSegments()) {
if (!sender.unacked.has(seg.seq)) continue;
const arrived = link.transmit(seg);
log.push(`timeout -> retransmit seq=${seg.seq}`);
if (arrived) {
const ack = receiver.receive(arrived);
log.push(` receiver ACK ${ack}`);
sender.acknowledge(ack);
}
}
}
}
log.push(`result="${receiver.text()}"`);
return log;
}
for (const line of transfer("PACKETS MAKE ORDER FROM CHAOS")) {
console.log(line);
}