# Minecraft Java Edition Protocol — Overview > **Scope:** Java Edition only. Protocol details change with each release; version-specific differences are called out explicitly. "Latest" means 1.21.x as of mid-2026 unless noted. > > **Primary sources:** > - `node-minecraft-protocol` v1.66.2 (`/tmp/mcproto-refs/node-minecraft-protocol/`) > - minecraft.wiki/w/Java_Edition_protocol (formerly wiki.vg) — [Packet format](https://minecraft.wiki/w/Java_Edition_protocol) / [Data types](https://minecraft.wiki/w/Java_Edition_protocol/Data_types) --- ## 1. Transport - **TCP**, no UDP (voice-chat plugins add their own UDP layer out-of-band). - Default port: **25565** (configurable). - All data is **big-endian** except VarInt/VarLong, which use little-endian 7-bit groups (see `01-data-types.md`). - **Encryption** is applied after the Login handshake (online-mode servers). The cipher is AES-128-CFB8, using the shared secret as both key and IV. Encryption wraps the raw TCP byte stream; packet framing runs on top of the encrypted layer. - Source: `node-minecraft-protocol/src/transforms/encryption.js:6-9` (`aes-128-cfb8`, key=IV=shared secret). --- ## 2. Packet Framing Every packet on the wire is length-prefixed. There are two modes: **uncompressed** (before the server sends `Set Compression`) and **compressed** (after). ### 2.1 Uncompressed packet format | Field | Type | Notes | |------------|---------|------------------------------------------------| | Length | VarInt | Byte count of everything that follows (Packet ID + Data). | | Packet ID | VarInt | State- and direction-scoped identifier. | | Data | bytes | Payload; structure defined per packet. | The `Framer` prepends the length: ```js // node-minecraft-protocol/src/transforms/framing.js:16-21 const varIntSize = sizeOfVarInt(chunk.length) const buffer = Buffer.alloc(varIntSize + chunk.length) writeVarInt(chunk.length, buffer, 0) chunk.copy(buffer, varIntSize) ``` The `Splitter` reads length, then slices exactly that many bytes — buffering partial reads across TCP segments (framing.js:48-73). ### 2.2 Compressed packet format Enabled by the serverbound `Set Compression` packet (Login state). After that, **every** subsequent packet uses: | Field | Type | Notes | |----------------|---------|-----------------------------------------------------------------------------| | Packet Length | VarInt | Byte count of `Data Length` field + compressed payload. | | Data Length | VarInt | Uncompressed size of (Packet ID + Data). **0 = not compressed.** | | Packet ID | VarInt | } zlib-deflated together when `Data Length > 0` | | Data | bytes | } | **Threshold rule:** if `len(Packet ID + Data) >= compressionThreshold`, compress and set `Data Length` to the uncompressed size. Otherwise send uncompressed with `Data Length = 0`. ```js // node-minecraft-protocol/src/transforms/compression.js:22-40 if (chunk.length >= this.compressionThreshold) { const newChunk = zlib.deflateSync(chunk) // prepend VarInt(chunk.length), then compressed bytes } else { // prepend VarInt(0), then raw bytes } ``` **Historical note:** Compression was added in **1.8** (protocol 47). Per `server/login.js:179`, node-minecraft-protocol gates compression on `protocolVersion >= 27` (snapshot 14w28a), which predates the 1.8 release — whole-protocol compression replaced earlier per-packet compression in that snapshot. ### 2.3 Packet size limits - Uncompressed maximum: **2²¹ − 1 = 2,097,151 bytes** (the `Length` VarInt may not exceed 3 bytes on the wire). — [minecraft.wiki](https://minecraft.wiki/w/Java_Edition_protocol) - Serverbound compressed: uncompressed (Packet ID + Data) must be ≤ 2²³ bytes (8,388,608). - Plugin message unrecognised-channel data: vanilla client caps at 1,048,576 bytes. --- ## 3. Connection State Machine The protocol is divided into **states**. Each state has its own independent set of packet IDs. Transitions are triggered by specific packets. ```js // node-minecraft-protocol/src/states.js:3-8 const states = { HANDSHAKING: 'handshaking', STATUS: 'status', LOGIN: 'login', CONFIGURATION: 'configuration', // added in 1.20.2 (protocol 764) PLAY: 'play' } ``` ### State diagram ```mermaid stateDiagram-v2 [*] --> Handshaking : "TCP connect" Handshaking --> Status : "Handshake (nextState=1)" Handshaking --> Login : "Handshake (nextState=2)" Handshaking --> Login : "Handshake (nextState=3, Transfer 1.20.5+)" Status --> [*] : "Status Response + Ping/Pong" Login --> Configuration : "Login Success → Login Acknowledged (1.20.2+)" Login --> Play : "Login Success (pre-1.20.2)" Configuration --> Play : "Finish Configuration (both sides)" Play --> Configuration : "Start Configuration (server-initiated, 1.20.2+)" Play --> [*] : "Disconnect" ``` ### 3.1 Handshaking Initial state on connect. Client sends exactly one packet: - **Handshake** (0x00, serverbound): carries `protocolVersion`, `serverHost`, `serverPort`, and `nextState` (1 = Status, 2 = Login, 3 = Transfer). After this packet the server transitions immediately to the indicated next state. ```js // node-minecraft-protocol/src/server/handshake.js:34-38 if (packet.nextState === 1) { client.state = states.STATUS } else if (packet.nextState === 2) { client.state = states.LOGIN } ``` `nextState = 3` (Transfer) was added in **1.20.5** for server-transfer support. ### 3.2 Status Server-list ping. Two round-trips: 1. Client → **Status Request** (0x00) 2. Server → **Status Response** (0x00) — JSON payload with MOTD, player count, favicon 3. Client → **Ping Request** (0x01) — 8-byte timestamp 4. Server → **Pong Response** (0x01) — echoes timestamp Connection is then closed by the client. The server does not transition to another state. ### 3.3 Login Authentication and encryption negotiation. Typical online-mode sequence: 1. C→S: `Login Start` (username, UUID, optional profile-key signature) 2. S→C: `Encryption Request` (server ID, RSA public key, verify token) 3. C→S: `Encryption Response` (RSA-encrypted shared secret + verify token) 4. *(Both sides enable AES-128-CFB8 encryption)* 5. Server calls Mojang session-server to verify join 6. S→C: `Set Compression` (threshold; gates compressed mode) — added in **1.8** / snapshot 14w28a 7. S→C: `Login Success` (UUID, username, properties) 8. C→S: `Login Acknowledged` — triggers transition to Configuration (1.20.2+) ```js // node-minecraft-protocol/src/server/login.js:179 if (client.protocolVersion >= 27) { client.write('compress', { threshold: 256 }) client.compressionThreshold = 256 } ``` ```js // node-minecraft-protocol/src/server/login.js:189-193 if (client.supportFeature('hasConfigurationState')) { client.once('login_acknowledged', onClientLoginAck) } else { client.state = states.PLAY } ``` ### 3.4 Configuration (1.20.2+, protocol 764+) Introduced in **1.20.2**. Entered after Login Acknowledged; used for: - Registry codec data (dimension types, biomes, etc.) - Feature flags - Known packs negotiation - Server brand plugin channel Both client and server exchange `Finish Configuration` to exit this state. ```js // node-minecraft-protocol/src/server/login.js:224-238 (onClientLoginAck) client.state = states.CONFIGURATION // ... send registry_data, then finish_configuration client.once('finish_configuration', () => { client.state = states.PLAY }) ``` The server can re-enter Configuration from Play at any time by sending `Start Configuration` (serverbound: `Configuration Acknowledged` from client acknowledges the switch back). ```js // node-minecraft-protocol/src/client/play.js:43-44 client.on('start_configuration', () => enterConfigState()) // ... if (client.state === states.PLAY) { client.write('configuration_acknowledged', {}) } ``` ### 3.5 Play Normal gameplay state. The bulk of all packets are Play-state packets. This state persists until disconnect or a server-initiated re-entry into Configuration. --- ## 4. Packet ID Scoping Packet IDs are **not globally unique**. They are scoped by **both state and direction**: - **State**: Handshaking / Status / Login / Configuration / Play - **Direction**: serverbound (client → server) or clientbound (server → client) A packet ID of `0x00` in Handshaking-serverbound (Handshake packet) is completely unrelated to `0x00` in Status-clientbound (Status Response), and so on. You need all three pieces — state, direction, ID — to unambiguously identify a packet. The serializer key in node-minecraft-protocol reflects this: ```js // node-minecraft-protocol/src/transforms/serializer.js:47-51 function createSerializer ({ state = states.HANDSHAKING, isServer = false, version, ... }) { return new Serializer( createProtocol(state, !isServer ? 'toServer' : 'toClient', version, ...), 'packet' ) } ``` Concrete packet ID assignments live in `minecraft-data` (a separate package) and change between protocol versions. Always consult the version-specific protocol data, not hard-coded constants. --- ## 5. Encryption Layer Position The pipeline (innermost to outermost on send) is: ``` Packet (ID + payload) → Compression (optional, zlib deflate) → Framing (VarInt length prefix) → Encryption (AES-128-CFB8, wraps the byte stream after framing) → TCP ``` Framing runs before encryption so the receiver can split frames while decrypting. Both directions use the same shared secret but independent cipher/decipher instances. Source: `node-minecraft-protocol/src/transforms/encryption.js` — AES-128-CFB8 with `key = IV = sharedSecret`. --- ## 6. Legacy Server List Ping Clients predating the current framing (pre-1.7) send a `0xFE` byte as the first byte of the TCP stream. node-minecraft-protocol detects this and shims it into the modern VarInt-framed pipeline: ```js // node-minecraft-protocol/src/transforms/framing.js:36-43 if (this.recognizeLegacyPing && this.buffer[0] === LEGACY_PING_PACKET_ID) { // Prefix a VarInt-encoded packet ID so the deserializer can handle it } ``` This is a compatibility shim only; the modern protocol framing is always VarInt-prefixed as described in §2. --- *See `01-data-types.md` for wire type encodings (VarInt, Position, NBT, etc.).*