Skip to main content
友田 陽大
TCP/IP・ネットワーク
TCP/IP
TCP
UDP
ネットワーク
アーキテクチャ設計
パフォーマンス

The difference between TCP and UDP and when to use each: understand it via the RFCs and choose with QUIC/HTTP3 in view

An explanation of whether to use TCP or UDP, with a comparison and decision flow faithful to IETF primary sources (RFC 9293, 768). It organizes the differences by reliability, ordering, boundaries, overhead, and implementation cost, and shows judgment axes usable for production technology selection — with concrete examples of HTTP/DB/gRPC, DNS, real-time voice/games, and QUIC(HTTP3), plus Node.js code.

Published
Reading time
10 min read
Author
友田 陽大
Share

"Is this TCP? Or UDP?" — it's not a question you ask every time you add one API. Most Web/APIs are fine over HTTP (= TCP), and there's no need to agonize. But when you put real-time voice on it, build game synchronization, hit DNS, or judge whether to ride on HTTP/3 — choosing "TCP, vaguely" in those situations gets you stuck on latency. Conversely, starting to roll your own reliability with "UDP because it seems fast" lands you in the swamp of re-implementing a degraded TCP.

This article accurately grasps the difference between TCP and UDP with IETF primary sources, and shapes it so you can use "which one, and why" as a decision flow. For the details of the mechanism, see how TCP works, and for the big picture, the complete TCP/IP guide.

Rules for this article: the specs are based on TCP = RFC 9293 (August 2022), UDP = RFC 768 (1980), QUIC = RFC 9000 (2021), HTTP/3 = RFC 9114 (2022). The code is arranged to run with the Node.js standard library. Confirm the latest specs at rfc-editor.org.


1. Grasp it on one page: the essential difference between TCP and UDP

The difference between the two isn't "how many features" but a difference in design philosophy: who takes on the responsibility of reliability.

AspectTCP (RFC 9293)UDP (RFC 768)
ConnectionConnection-oriented (three-way handshake)Connectionless (send right away)
Delivery guaranteeYes (recover with ACK + retransmit)No (fire and forget)
Ordering guaranteeYes (order with sequence numbers)No (can be reordered)
De-duplicationYesNo
Data boundaryNot kept (byte stream)Kept (1 send = 1 datagram)
Flow controlYes (receive window)No
Congestion controlYes (mandatory, AIMD)No (the app's responsibility)
HeaderMinimum 20 bytes8 bytes
Speed characteristicFixed cost of establishment RTT + HoLBMinimal overhead, low latency
Representative usesHTTP/HTTPS, DB, SSH, gRPC, mailDNS, voice/video, games, QUIC, syslog

Condensed into one row of this table: TCP chooses "correctness," and UDP chooses "speed and freedom of control," by default.

1.1 The difference in "data boundary" is easy to overlook

Delivery guarantee gets all the attention, but what produces bugs in practice is the boundary.

  • TCP is a byte stream: write("AB") and write("CD") might arrive at the other side as a single "ABCD". The message delimiter must be implemented by the app itself (length prefix, etc.).
  • UDP is message-oriented: one send is received at the other side as exactly one message (if it arrives), keeping the boundary.

This is why there are situations where you feel "UDP is easier to handle." But it's a trade-off with the price of no guarantee of arrival.


2. Why choose UDP — there are domains where "retransmission is harmful"

The intuition "if there's no guarantee, isn't TCP fine?" overlooks one important fact. In the real-time domain, dropping lost old data and moving on is more correct than retransmitting and waiting for it.

2.1 Real-time voice/video

Even if a voice frame from 20ms ago is retransmitted now, the playback position has long passed and it's useless. TCP dutifully retransmits, and moreover holds up even the newer subsequent frames with HoLB — this worsens perceived quality as dropouts and delay. With UDP (+ an app-layer jitter buffer or FEC), you can "give up the dropped frame and prioritize the latest." This is why WebRTC builds on UDP.

2.2 DNS — a small query that finishes in one round trip

A DNS query is "a small question → a small answer." Paying TCP's three-way handshake (1-RTT establishment) every time here isn't worth it. With UDP it completes in one round trip (it falls back to TCP if the response is large/truncated). Not having enough conversation volume to justify paying the establishment cost is the typical UDP fit.

2.3 State synchronization in online games

"The latest coordinates" is always more correct than "the player coordinates from 100ms ago." A complete in-order history is unnecessary, and the latest-ness is the lifeline. The standard is a design that puts your own lightweight reliability (e.g., ACKing only important events) on UDP.

A common judgment axis: if "freshness (latest-ness) > completeness (not dropping anything)" holds, UDP becomes a candidate. Conversely, if "even one missing byte is a problem" or "it breaks down if ordering collapses," it's TCP.


3. Decision flow — how to actually choose

A practical flow to apply from the top when in doubt.

Q1. 確実な配送と順序が必須か?(1バイトの欠落・順序崩れも許されない)
    ├─ YES → TCP。さらに「アプリ意味」が要るなら HTTP/gRPC(=TCP の上)。     → 終了
    └─ NO  → Q2 へ

Q2. 低遅延・最新性が最優先で、損失をアプリで許容/補償できるか?
    ├─ NO  → 判断保留なら TCP を既定に。 → 終了
    └─ YES → Q3 へ

Q3. 「UDP だが信頼性・暗号・多重化が欲しい」か?
    ├─ YES → QUIC(HTTP/3)に乗る。生UDPで信頼性を自作しない。            → 終了
    └─ NO  → Q4 へ

Q4. 純粋に最小オーバーヘッドの片方向/小往復通信か?(音声/映像/ゲーム/DNS/メトリクス)
    └─ YES → UDP + アプリ層で必要最小限の補償(FEC・選択的ACK・ジッタバッファ)。

The most important guideline: if in doubt, TCP (or HTTPS/gRPC) is the default. UDP is an optimization for when "freshness > completeness" is clear and you can design the handling of loss — it's not the default. And if you want "UDP's speed × reliability," riding on QUIC rather than rolling your own is the 2026 realistic solution.


4. The difference in code: the same "echo" in TCP and UDP

Let's drop the abstraction into the concrete. Writing the same function in both, the difference in design philosophy appears directly in the code.

4.1 TCP: establish a connection, delimit boundaries yourself

import net from "node:net";

// TCP はバイトストリーム。改行などで「メッセージ境界」を自分で復元する必要がある
const server = net.createServer((socket) => {
  let buffer = "";
  socket.setEncoding("utf8");
  socket.on("data", (chunk: string) => {
    buffer += chunk;
    let nl: number;
    // 改行区切りでフレーミング(write と data は1対1でないため必須)
    while ((nl = buffer.indexOf("\n")) >= 0) {
      const line = buffer.slice(0, nl);
      buffer = buffer.slice(nl + 1);
      socket.write(`${line}\n`); // echo
    }
  });
});
server.listen(9000);

4.2 UDP: no connection, with boundaries, no guarantee of arrival

import dgram from "node:dgram";

// UDP は 1 send = 1 message。境界は保たれるが、順序・到達・重複は保証されない
const server = dgram.createSocket("udp4");
server.on("message", (msg: Buffer, rinfo) => {
  // フレーミング不要(メッセージ指向)。ただし「届かなかった message」は永遠に来ない
  server.send(msg, rinfo.port, rinfo.address);
});
server.bind(9001);

The philosophy appears in the code: TCP pays the cost of "connection management + framing" to obtain completeness. UDP doesn't pay that, and in exchange, if you need reliability you have to design it yourself.

4.3 If you were to add "minimal reliability" over UDP

A minimal design for when you want to reliably deliver only important messages over UDP (knowing how troublesome this is leads to the judgment to choose QUIC).

import dgram from "node:dgram";

/** 重要メッセージにシーケンス番号を付け、ACK が来るまで指数バックオフで再送する最小実装 */
class ReliableUdpSender {
  private seq = 0;
  private readonly pending = new Map<number, NodeJS.Timeout>();

  constructor(
    private readonly socket: dgram.Socket,
    private readonly port: number,
    private readonly host: string,
  ) {
    // 受信側からの ACK(seq) を受けて、対応する再送タイマーを止める
    socket.on("message", (msg) => {
      if (msg[0] === 0x06 /* ACK */) this.clear(msg.readUInt32BE(1));
    });
  }

  send(payload: Buffer, attempt = 0): void {
    const seq = attempt === 0 ? this.seq++ : this.lastSeq;
    this.lastSeq = seq;
    const frame = Buffer.concat([Buffer.from([0x01]), u32(seq), payload]);
    this.socket.send(frame, this.port, this.host);
    // ACK が来なければ指数バックオフで再送(=TCP の RTO/再送を手で再発明している)
    const timer = setTimeout(() => this.send(payload, attempt + 1), 200 * 2 ** attempt);
    this.pending.set(seq, timer);
  }

  private lastSeq = 0;
  private clear(seq: number): void {
    const t = this.pending.get(seq);
    if (t) clearTimeout(t), this.pending.delete(seq);
  }
}
const u32 = (n: number): Buffer => { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; };

This is merely re-inventing a degraded version of TCP's retransmission mechanism by hand. Add ordering, congestion control, flow control, and even encryption here, and you end up building TCP or QUIC after all. So the answer to "I want reliability over UDP" is almost always QUIC.


5. The third option: QUIC / HTTP3 — "UDP but reliable"

To overcome TCP's structural weaknesses seen in how TCP workshead-of-line blocking and the fixed cost of establishment RTT — the IETF rebuilt the transport layer. That's QUIC (RFC 9000).

  • Implemented over UDP: avoids the constraints of the OS's TCP stack and middleboxes, and can evolve in user space.
  • Per-stream-independent reliability: multiplexes multiple streams, and loss in one stream doesn't stop the others (resolving TCP's HoLB).
  • Encryption is a premise (TLS 1.3 integrated): fuses the handshake and crypto key exchange, establishing a connection in 0–1 RTT.
  • Connection migration: can maintain the connection even when the IP changes (Wi-Fi ↔ mobile).

HTTP/3 (RFC 9114) is HTTP built on this QUIC. In other words, "going to HTTP/3" means swapping the transport from TCP to QUIC(UDP). You can remove TCP's fixed cost and HoLB with almost no change to the app code — this is QUIC's practical value.

The reality of selection: implementing reliability over UDP yourself is (as in §4.3) re-inventing TCP/QUIC. If you want "low latency × reliability × encryption," the correct answer is to ride on a QUIC library / HTTP3 support. Raw UDP is only for when minimal overhead is truly needed (voice/video/games/metrics) or under constraints where QUIC can't be used.


6. Correcting common misconceptions

  • "UDP is fast" = half correct. It's advantageous in latency for lacking establishment RTT and HoLB, but UDP that doesn't do congestion control can scatter packets during congestion and even worsen the whole. "Fast" means "light," not "always high-performance."
  • "Avoid TCP because it's heavy" = wrong in many cases. Establishment cost can be amortized with Keep-Alive and connection pools, and modern CUBIC/BBR are high-performance. First use TCP correctly to the fullest.
  • "UDP makes firewalls easier" = often the opposite. Being connectionless, care is needed for compatibility with stateful FWs and NAT (e.g., keep-alives are needed due to NAT mapping timeouts).
  • "Going to HTTP/3 always makes it faster" = conditional. The effect is large on high-loss, high-latency, mobile while the difference can be small in low-latency wired environments. Measure and judge.

7. Conclusion: the choice in one sentence

If you need reliable delivery and ordering, TCP (this if in doubt, or HTTPS/gRPC). If low latency and "freshness > completeness" holds, UDP. And if you want "UDP's speed × reliability," don't roll your own; ride on QUIC/HTTP3.

  • TCP = have the network build reliability. UDP = make reliability the app's responsibility (or don't need it).
  • UDP's strengths are low latency, message boundaries, and freedom of control. Its weakness is zero guarantees (rolling your own is a thorny path).
  • QUIC is a third option that standardized taking the best of both. HTTP/3 is its application.
  • The default is TCP, UDP is an optimization, and QUIC is the realistic solution for reliable low latency.

Protocol choice is an upstream decision that simultaneously affects latency, perceived quality, and implementation cost. Choosing it grounded in the mechanism (TCP/IP big picture / TCP internals) pays off later.


I (Yudai Tomoda) select TCP/UDP/QUIC from requirements in backends for real-time streaming, payments, and mobile integration, designing for both low latency and reliability. "Latency/dropouts in real-time communication," "should we migrate to HTTP/3," "I want to build a custom protocol over UDP but I'm scared of reliability" — I'll untangle these protocol-selection and implementation problems together with you, based on measurement and primary sources. Feel free to consult me.

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

I can take on the implementation from this article as an engagement

Investigating network/low-level production incidents & designing for reliability

“ECONNRESET won't stop,” “TIME_WAIT is exhausting ports,” “lots of retransmits and unstable latency,” “afraid of double processing in payments/inventory,” “which of TCP/UDP/QUIC to choose” — I pin down the cause of these TCP/IP, low-level issues from ss/tcpdump observation and cure them with connection pooling, timeout budgets, and idempotency. With the payment-reliability experience that achieved zero double charges in production, I design backends that don't fall over, are traceable, and are correct.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading