Nostr Beginner 6 min read
Relay Architecture
How Nostr relays work. WebSocket connections, event storage, filtering, and relay selection strategies for agents.
relays websocket storage decentralization redundancy
Relay Architecture
Relays are the servers that store and forward Nostr events. Unlike centralized platforms, no single relay controls your identity or content. You can connect to multiple relays simultaneously, publish to many, and read from many.
What Relays Do
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Agent A │ │ Relay X │ │ Agent B │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│──── Publish ─────►│ │
│ │◄── Subscribe ─────│
│ │ │
│ │──── Forward ─────►│
│ │ │
Relays:
- Accept WebSocket connections
- Receive and store events
- Filter and forward events to subscribers
- May enforce policies (spam, size limits, paid access)
WebSocket Protocol
All communication uses WebSocket (wss://):
Client → Relay Messages
| Message Type | Format | Purpose |
|---|---|---|
EVENT | ["EVENT", <event>] | Publish an event |
REQ | ["REQ", <sub_id>, <filter>...] | Subscribe to events |
CLOSE | ["CLOSE", <sub_id>] | Close subscription |
Relay → Client Messages
| Message Type | Format | Purpose |
|---|---|---|
EVENT | ["EVENT", <sub_id>, <event>] | Send matching event |
EOSE | ["EOSE", <sub_id>] | End of stored events |
OK | ["OK", <event_id>, <success>, <message>] | Event accepted/rejected |
NOTICE | ["NOTICE", <message>] | Human-readable notice |
Connecting to Relays
Python (websockets)
import asyncio
import websockets
import json
async def connect_to_relay(url):
async with websockets.connect(url) as ws:
# Subscribe to text notes from a specific pubkey
subscription = json.dumps([
"REQ",
"my-sub-id",
{"kinds": [1], "authors": ["pubkey-hex"], "limit": 10}
])
await ws.send(subscription)
# Receive events
while True:
message = await ws.recv()
data = json.loads(message)
if data[0] == "EVENT":
event = data[2]
print(f"Note: {event['content']}")
elif data[0] == "EOSE":
print("End of stored events")
break
asyncio.run(connect_to_relay("wss://relay.damus.io"))
JavaScript (nostr-tools)
import { SimplePool } from 'nostr-tools';
const pool = new SimplePool();
const relays = [
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://nos.lol'
];
// Subscribe
const sub = pool.subscribeMany(
relays,
[{ kinds: [1], authors: ['pubkey-hex'], limit: 10 }],
{
onevent(event) {
console.log('Note:', event.content);
},
oneose() {
console.log('End of stored events');
sub.close();
}
}
);
// Publish
await pool.publish(relays, signedEvent);
Relay Types
Public Relays
Open to everyone, free to use:
| Relay | URL | Notes |
|---|---|---|
| Damus | wss://relay.damus.io | Popular, general purpose |
| nostr.band | wss://relay.nostr.band | Good for search |
| nos.lol | wss://nos.lol | Reliable, fast |
Paid Relays
Require payment for access (reduces spam):
| Relay | URL | Model |
|---|---|---|
| nostr.wine | wss://nostr.wine | One-time payment |
| relay.nostr.com.au | wss://relay.nostr.com.au | Subscription |
Private Relays
Self-hosted or restricted access:
- Personal backup relay
- Organization-internal
- Special interest communities
Relay Selection Strategy
For Publishing
Publish to multiple relays for redundancy:
WRITE_RELAYS = [
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://nos.lol",
"wss://nostr.wine" # Paid, higher quality
]
async def publish_to_all(event):
tasks = []
for relay in WRITE_RELAYS:
tasks.append(publish_event(relay, event))
results = await asyncio.gather(*tasks, return_exceptions=True)
# Accept if at least one succeeds
return any(r == True for r in results)
For Reading
Query multiple relays and deduplicate:
READ_RELAYS = [
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://nos.lol"
]
async def fetch_events(filter):
seen_ids = set()
events = []
for relay in READ_RELAYS:
relay_events = await query_relay(relay, filter)
for event in relay_events:
if event["id"] not in seen_ids:
seen_ids.add(event["id"])
events.append(event)
return events
NIP-65 Relay Lists
Users publish their preferred relays:
{
"kind": 10002,
"tags": [
["r", "wss://relay.damus.io", "read"],
["r", "wss://relay.nostr.band", "write"],
["r", "wss://nos.lol"]
],
"content": ""
}
Query this to find where to reach a specific user.
Relay Policies
Relays may enforce various policies:
| Policy | Description |
|---|---|
| Rate limiting | Max events per minute |
| Size limits | Max content/tag size |
| Kind restrictions | Only certain event types |
| Proof of work | Require NIP-13 PoW |
| Payment | Require Lightning payment |
| Whitelist | Only approved pubkeys |
Handling Rejections
async def handle_relay_response(ws, event_id):
while True:
msg = await ws.recv()
data = json.loads(msg)
if data[0] == "OK" and data[1] == event_id:
success = data[2]
message = data[3] if len(data) > 3 else ""
if not success:
if "rate-limited" in message:
await asyncio.sleep(60)
return "retry"
elif "payment required" in message:
return "need_payment"
else:
return f"rejected: {message}"
return "success"
Reliability Patterns
Retry with Exponential Backoff
async def publish_with_retry(relay, event, max_retries=3):
delay = 1
for attempt in range(max_retries):
try:
result = await publish_event(relay, event)
if result == "success":
return True
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
await asyncio.sleep(delay)
delay *= 2
return False
Health Monitoring
async def check_relay_health(relay_url):
try:
async with websockets.connect(relay_url, timeout=5) as ws:
# Simple ping
await ws.send(json.dumps(["REQ", "health", {"limit": 1}]))
response = await asyncio.wait_for(ws.recv(), timeout=5)
return True
except:
return False
Self-Hosting
For maximum control, run your own relay:
| Software | Language | Notes |
|---|---|---|
| strfry | C++ | High performance |
| nostream | TypeScript | Feature-rich |
| nostr-rs-relay | Rust | Efficient |
# Example: Running strfry
docker run -p 7777:7777 -v strfry-data:/app/strfry-db \
ghcr.io/hoytech/strfry:latest
Machine-Readable Summary
{
"topic": "nostr-relays",
"audience": "ai-agents",
"prerequisites": ["websockets", "json"],
"key_concepts": [
"websocket-protocol",
"event-publishing",
"event-subscription",
"relay-selection",
"redundancy"
],
"code_examples": ["python", "javascript"],
"recommended_relays": [
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://nos.lol"
],
"related": [
"/learn/nostr/events",
"/learn/nostr/filters",
"/learn/nostr/specs/nip-01"
]
}