Chapter 4
Relays & the wire protocol
Watch REQ, EVENT, EOSE and friends fly between clients and relays.
Packets incoming! Grab a seat, we're going to eavesdrop on a relay. Totally legal, I checked.
You have a
Relays are dumb (and that’s a compliment)
Picture a relay as a community notice board in a café. Anyone can pin a card on it, anyone can walk up and read the cards, and the café owner decides house rules: no ads, members only, cards taken down after a month. That’s it. The notice board doesn’t know your friends, doesn’t rank your feed, and doesn’t talk to the notice board in the café across the street.
Your
This split is on purpose:
- Relays stay simple, so they’re cheap to run. Thousands of people run one, from big public relays to a tiny one in someone’s closet.
- Clients hold the intelligence, so you choose the algorithm, not a company.
- Nothing depends on one relay. If a board disappears, your assistant just reads another one.
That’s where Nostr’s
comes from.censorship resistance Censorship resistance: The property that no single company or server can silence you or erase your identity.
A relay can refuse your note, lose it, or hide it. What it can’t do is edit it or post in your name: every event carries your signature, and clients verify it on their own device (chapter 3). The worst a relay can do is stay silent, and that’s why we use more than one.
Eavesdropping on the wire
Clients and relays talk over a
Pick a conversation below, then press play, step through it with the arrows, or drag the scrubber. Click any packet type in the legend to jump straight to it. Keep an eye on the client’s notebook: that’s all the bookkeeping relays don’t do.
Your client wants Alice's two latest notes. It asks three relays at once and merges the answers.
A sequence diagram: your client on the left, relays on the right. Each arrow is one JSON message on a WebSocket. Step through it, play it, or drag the scrubber.
Ready. Press play or step forward to send the first message.
Text version
| # | From | To | Message |
|---|---|---|---|
| 1 | Your client | Relay Alpha | Your client opens subscription "feed" on Relay Alpha: Alice's notes, newest 2. |
| 2 | Your client | Relay Beta | Same REQ to Relay Beta. Asking several relays means one slow or dead relay can't blank your feed. |
| 3 | Your client | Relay Gamma | And to Relay Gamma. Relays never talk to each other, so the client does the asking. |
| 4 | Relay Alpha | Your client | Alpha answers with Alice's newest note, wrapped in an EVENT tagged with the subscription id. |
| 5 | Relay Beta | Your client | Beta sends the very same note. Same id, so your client shows it once. Deduplication is the client's job. |
| 6 | Relay Alpha | Your client | Alpha sends the second note. Your client verifies every signature itself: relays are never trusted. |
| 7 | Relay Alpha | Your client | EOSE from Alpha: that's all it had stored. Time to stop the loading spinner for Alpha. |
| 8 | Relay Beta | Your client | EOSE from Beta. The subscription stays open, so brand-new notes would still stream in. |
| 9 | Relay Gamma | Your client | Gamma refuses with CLOSED and a "rate-limited:" reason. No drama: two relays already answered. |
| 10 | Your client | Relay Alpha | Done reading. Your client sends CLOSE so Alpha stops sending updates. |
| 11 | Your client | Relay Beta | And CLOSE to Beta. The WebSocket stays open for the next REQ. |
The whole vocabulary fits on a sticky note
NIP-01 defines the core language. Your client says three things:
| Client says | Meaning |
|---|---|
REQ |
“Send me events matching these |
EVENT |
“Please store this signed event.” (Publishing.) |
CLOSE |
“I’m done with that subscription.” |
The relay answers with five:
| Relay says | Meaning |
|---|---|
EVENT |
“Here’s an event for your subscription called feed.” |
EOSE |
“That’s everything I had stored.” |
OK |
“About that event you sent: stored (true) or refused (false), and here’s why.” |
CLOSED |
“I’m ending your subscription,” with a reason. |
NOTICE |
A human-readable message, like a note taped to the door. |
And there’s one extra pair for logging in, from AUTH.
The relay sends a random challenge; your client signs it with your key and sends it back. No
password ever crosses the wire, only a signature. (There’s also COUNT from
NIP-45, for “how many?” questions, but
you rarely see it.)
Under the hood: the exact frames Show details
Every frame is a JSON array sent as one WebSocket text message.
["REQ", "feed", {"authors": ["e550c6e9…"], "kinds": [1], "limit": 2}]
["EVENT", "feed", {"id": "…", "pubkey": "…", "created_at": 1735686600, "kind": 1, "tags": [], "content": "…", "sig": "…"}]
["EOSE", "feed"]
["CLOSE", "feed"]
["EVENT", {"id": "…", "kind": 1, "…": "…"}]
["OK", "<event id>", true, ""]
["OK", "<event id>", false, "pow: difficulty 16 or more is required"]
["CLOSED", "feed", "rate-limited: slow down, too many requests"]
["NOTICE", "heads up: this relay restarts at 03:00 UTC"]Rules worth knowing, straight from NIP-01:
- The subscription id is any non-empty string up to 64 characters, chosen by the client and
scoped to one connection. Sending a new
REQwith an id that’s already open replaces that subscription. - A
REQmay carry several filters; they’re combined with OR.limitonly applies to the first, stored batch. Withlimit: 0the relay skips stored events entirely and just streams new ones. - After
EOSE, the relay keeps sending matching events in real time until youCLOSE, it sendsCLOSED, or the socket drops. - A relay must answer every published
EVENTwithOK, and must sendCLOSEDwhen it refuses aREQ. OKandCLOSEDmessages start with a machine-readable prefix:duplicate:,pow:,blocked:,rate-limited:,invalid:,restricted:,mute:orerror:. NIP-42 addsauth-required:. Note that["OK", id, true, "duplicate: …"]is a success: the relay already had it.
Under the hood: logging in with AUTH (NIP-42) Show details
The relay may send a challenge at any time, typically right after you connect or just before it refuses something:
["AUTH", "ostrich-7f3a9c2e"]
["CLOSED", "dms", "auth-required: gift wraps are only served to their recipient"]The client answers with a signed, never published event of kind 22242, tagging the relay URL
and the challenge. Relays check the signature, the challenge, the URL, and that created_at is
recent (roughly within 10 minutes):
["AUTH", {"kind": 22242, "tags": [["relay", "wss://relay.delta.example"], ["challenge", "ostrich-7f3a9c2e"]], "content": "", "…": "id, pubkey, created_at, sig"}]
["OK", "<auth event id>", true, ""]If you’re logged in but your key still isn’t allowed, the relay uses the restricted: prefix
instead. Paid relays and relays serving private
Under the hood: talking to relays with nostr-tools Show details
SimplePool manages one WebSocket per relay and fans your requests out to all of them, deduplicating
events by id for you:
import { SimplePool } from "nostr-tools/pool";
import { finalizeEvent, generateSecretKey } from "nostr-tools/pure";
const relays = ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.primal.net"];
const pool = new SimplePool();
// REQ to every relay; onevent fires once per unique event, oneose when all relays sent EOSE.
const sub = pool.subscribeMany(relays, { kinds: [1], limit: 3 }, {
onevent: (event) => console.log(event.pubkey.slice(0, 8), event.content),
oneose: () => sub.close(), // sends CLOSE to each relay
});
// Publishing = one EVENT per relay; each promise settles with that relay's OK.
const event = finalizeEvent(
{ kind: 1, created_at: Math.floor(Date.now() / 1000), tags: [], content: "hello relays" },
generateSecretKey(),
);
const results = await Promise.allSettled(pool.publish(relays, event));
console.log(results.map((r) => r.status)); // e.g. ["fulfilled", "fulfilled", "rejected"]For a single connection there’s Relay.connect(url) from nostr-tools/relay, with relay.subscribe,
relay.publish and relay.auth(sign) for NIP-42. Under the hood it’s exactly the frames you saw above.
Bonus: a relay can describe itself. Fetch its URL over HTTPS with the header
Accept: application/nostr+json and you get a JSON document with its name, contact, supported NIPs
and limits (
Don’t put all your notes in one relay
Because relays don’t copy events between each other, your client usually sends every note to
several relays at once. If one goes offline, gets overloaded, or decides it doesn’t like you,
your followers can still find your note on the others. Some relays are free, some are
Try it: choose where to publish, then start knocking relays offline.
Don't put all your notes in one relay
Pick which relays your note is published to, then knock relays offline. Can your followers still find it?
- Relay Alpha online · has a copy
- Relay Beta online · has a copy
- Relay Gamma online · no copy
- Relay Delta (paid) online · no copy
Your note is still findable. Copies still reachable: 2 of 2
How does Bob’s client know which relays to check for your notes? You publish a small list of
“where I write” relays, and his client goes there. That’s the
OK false isn’t an error in the protocol: it’s a relay exercising its right to choose. One relay
might demand
Listen to the real thing
Everything above was scripted. This panel sends a real REQ and shows every frame exactly as it
arrives. By default it talks to our practice relays; flip Live mode in the header and it talks to
real public relays instead. Don’t worry: it only ever reads. We never publish anything.
Real frames from real relays
Practice relaysSend one REQ (latest 3 text notes) and watch the raw JSON come back. Read-only: we never publish.
You're on practice relays. Turn on Live mode in the header to talk to real ones.
Idle.
- No frames yet. Press "Send REQ".
Notice the CLOSE frames at the end: once every relay has said EOSE, the panel politely hangs up.
A real client would often keep the subscription open to receive new notes as they’re posted.
Quick check
Takeaways
- A relay is a simple server that stores signed events and hands them out when asked. Relays don't talk to each other.
- Clients do the clever work: they pick relays, merge and deduplicate answers, and verify every signature themselves.
- The whole conversation is a handful of JSON arrays over a WebSocket: REQ, EVENT, EOSE, CLOSE, OK, NOTICE, CLOSED and AUTH.
- Publishing the same event to several relays keeps it findable when one relay goes down or kicks you out.
- A relay can refuse or forget your events, but it can never change them or fake new ones in your name.