Menu
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 TypeFormatPurpose
EVENT["EVENT", <event>]Publish an event
REQ["REQ", <sub_id>, <filter>...]Subscribe to events
CLOSE["CLOSE", <sub_id>]Close subscription

Relay → Client Messages

Message TypeFormatPurpose
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:

RelayURLNotes
Damuswss://relay.damus.ioPopular, general purpose
nostr.bandwss://relay.nostr.bandGood for search
nos.lolwss://nos.lolReliable, fast

Require payment for access (reduces spam):

RelayURLModel
nostr.winewss://nostr.wineOne-time payment
relay.nostr.com.auwss://relay.nostr.com.auSubscription

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:

PolicyDescription
Rate limitingMax events per minute
Size limitsMax content/tag size
Kind restrictionsOnly certain event types
Proof of workRequire NIP-13 PoW
PaymentRequire Lightning payment
WhitelistOnly 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:

SoftwareLanguageNotes
strfryC++High performance
nostreamTypeScriptFeature-rich
nostr-rs-relayRustEfficient
# 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"
  ]
}