---
title: Connection states
description: How a connection moves from handshake to play, and how the library tracks it.
---

A Minecraft connection is a small state machine. The same packet ID means different things in different states, so both peers must agree on where they are. mc-protocol tracks this for you and switches automatically when it sees the lifecycle packets below.

```diagram
+-----------+   Intension(1)   +--------+
| handshake | ---------------> | status |
+-----------+                  +--------+
      |
      | Intension(2 or 3)
      v
+-----------+  LoginAcknowledged  +--------+  FinishConfiguration  +------+
|   login   | ------------------> | config | --------------------> | play |
+-----------+                     +--------+                       +------+
                                       ^                              |
                                       |  StartConfiguration /        |
                                       +--- ConfigurationAcknowledged +
```

## The five states

| State | Purpose | Packets |
| --- | --- | --- |
| `handshake` | Declares protocol version, target host, and where to go next. Also includes the legacy server-list ping. | [2 packets](/packets/handshake) |
| `status` | Server list ping: description, player count, latency. No login. | [4 packets](/packets/status) |
| `login` | Username exchange, optional encryption, compression setup. | [11 packets](/packets/login) |
| `config` | Registries, tags, feature flags, resource packs. Everything the client needs before it can render a world. | [30 packets](/packets/config) |
| `play` | The game. Chunks, entities, movement, chat, inventory. | [210 packets](/packets/play) |

## Two directions, tracked separately

Each `Client` has an `incomingState` and an `outgoingState`. Most of the time they match, but during the play → config round trip they briefly differ: the server sends `StartConfiguration` and switches its **outgoing** state to `config`, while its **incoming** state stays `play` until the client answers with `ConfigurationAcknowledged`.

```ts
client.on("stateChange", ({ direction, from, to }) => {
  console.log(`${direction}: ${from} -> ${to}`);
});
```

## Transitions the library performs

These happen inside `Client` whenever the packet is sent or received, on either side of the connection:

| Packet | Effect |
| --- | --- |
| `Intension` | `nextState` 1 → `status`; 2 or 3 → `login`. Also picks the packet tables for `protocolVersion`; see [Versions](/docs/versions). |
| `Hello` (serverbound) | Records `username` and `uuid` on `client.session` from either the write names (`username` / `playerUUID`) or the parsed names (`name` / `uuid`). |
| `LoginFinished` | Records the game profile on `client.session`. |
| `LoginCompression` | Enables zlib compression at the given threshold for both directions. Emits `compression`. |
| `LoginAcknowledged` | → `config`. Sets `client.loggedIn = true` and emits `login`. |
| `FinishConfiguration` (serverbound) | → `play`. Sets `client.joined = true` and emits `playerJoin` the first time. |
| `StartConfiguration` | Server's outgoing / client's incoming → `config`. |
| `ConfigurationAcknowledged` | Server's incoming / client's outgoing → `config`. `AcknowledgeConfiguration` is accepted as the same packet name. |

You never call a "set state" method. Send or receive the right packet and the state follows.

## Packet lookup depends on state

`sendPacket("KeepAlive", …)` resolves the name against the **current outgoing state**. `KeepAlive` exists in both `config` and `play` with different IDs; the library picks the right one. If the name does not exist in the current state you get:

```
Error: Unknown packet: play/clientbound/StatusResponse
```

The string tells you which state and bound it searched, which is usually enough to spot a sequencing mistake.
