---
title: Streams, cancellation, and 0-RTT
description: Closing versus cancelling a stream, and the two ways to send data before a handshake finishes.
icon: gauge-high
---

A `Conn` carries many streams. Getting their lifecycle right is most of what
separates a protocol that works from one that hangs or wastes bandwidth. This
page covers the parts that are easy to get wrong.

Upstream covers the same ground for Rust in
[Using QUIC](https://docs.iroh.computer/protocols/using-quic).

## Closing is not cancelling

Two different operations, routinely confused:

| Call | Meaning |
| --- | --- |
| `Stream.Close()` | finish sending — the peer reads EOF after buffered data |
| `Stream.CancelWrite(code)` | abandon sending — discard buffered data, tell the peer |
| `Stream.CancelRead(code)` | stop receiving — tell the peer to stop sending |

`Close` is graceful and belongs at the end of a successful exchange. It closes
only the *send* side of a bidirectional stream, which is what lets the peer's
`io.Copy` or `io.ReadAll` return. Forgetting it is the most common way to hang
a request/response protocol — see [Build an echo protocol](first-app).

`CancelWrite` and `CancelRead` are the abort path. They exist because QUIC can
stop a transfer that is no longer wanted:

```go
s, err := conn.OpenStreamSync(ctx)
// ... decide the response is stale or the user navigated away
s.CancelRead(1)   // stop the peer sending more
s.CancelWrite(1)  // discard anything we have queued
```

The reason to reach for these rather than just closing the connection: on a
multiplexed connection, a stream you no longer care about still consumes
bandwidth and can delay the streams you do care about. Cancelling reclaims
that capacity without disturbing the other streams. For anything real-time —
media, interactive UI, speculative prefetch — this is the difference between a
responsive protocol and one that falls behind under load.

Both take an application error code, an opaque `uint64` your protocol defines.
The unidirectional halves have the matching subset: `SendStream.CancelWrite`
and `ReceiveStream.CancelRead`.

## 0-RTT: sending before the handshake completes

Normally `Endpoint.Connect` blocks until the peer is authenticated. Two APIs
let you send earlier, and they are not symmetric.

### Client side — `ConnectEarly` and `Into0RTT`

```go
c, err := ep.ConnectEarly(ctx, addr, alpn)   // returns immediately
if err != nil {
	return err
}
conn, ok := c.Into0RTT()   // ok reports whether 0-RTT is actually available
if !ok {
	conn, err = c.Connection(ctx)  // fall back to the verified path
}
```

`Into0RTT` returns `ok = false` when there is no usable session ticket for
this peer — the first connection to a peer can never be 0-RTT. Always write
the fallback.

Those tickets come from `iroh.SessionCache`, an LRU of TLS 1.3 session tickets
bucketed by TLS server name. Because iroh derives a unique server name from
each peer's endpoint ID, tickets for different peers cannot collide and a
resumption always targets the identity you meant. You get this by default;
there is nothing to wire up.

A `Connecting` is **not safe for concurrent use and may be consumed only
once**: after `Into0RTT` succeeds or `Connection` returns, do not touch it
again.

### Server side — `Accepting.Into0RTT`

The accept side has the mirror image: an `Accepting` whose handshake may still
be in flight, with `Into0RTT()` to get a connection that can read early data
and `Connection(ctx)` to wait for the verified one.

### The safety rule

Early data arrives **before the peer is authenticated** and can be replayed by
an attacker who captured it. So handle it only if both hold:

- the operation is **idempotent** — replaying it changes nothing; and
- it does not depend on the peer's identity, because `RemoteID` is not yet
  proven.

A read is usually fine. A write, a payment, or anything authorizing on
identity is not. When you need the guarantee, wait:

```go
<-conn.HandshakeComplete()   // now RemoteID is proven
```

After the fact, `conn.Used0RTT()` reports whether the connection actually used
early data, which is what you check before trusting anything received on it.

## Next steps

- [Endpoints and connections](endpoints-and-connections) — the rest of the
  connection API.
- [Observing and rejecting connections](endpoint-hooks) — admission control,
  including the pre-handshake filter.
- [Security and privacy](security-and-privacy) — the wider threat model.
