ready-bell.com:3137 · UDP
Better asynchronous communication.
Ready Bell makes asynchronous workflows more responsive. A single UDP packet
to ready-bell.com:3137 announces that an operation has completed, allowing
clients to check for results before their next scheduled poll.
Click Listen. Click Notify. Watch Ready appear.
Try it in your own terminals
-
Open two terminals and start Netcat (
nc) in UDP mode in both:nc -u ready-bell.com 3137
- In terminal 1, enter:
- In terminal 2, enter:
- Watch for
Readyin terminal 1.
How it works
A Listener sends Listen <uuid> <seconds>. Later, a Notifier sends
Notify <uuid> when that UUID's result is ready. Ready Bell sends a
single Ready <uuid> packet to every Listener currently waiting on it.
Protocol reference
Ready Bell speaks UTF-8 plain-text messages over UDP. Each datagram carries exactly one command or response.
| Initiator | Message | Response | Meaning |
|---|---|---|---|
| Listener client → Server | Listen <uuid> <seconds> |
Registered <uuid> <ip> <port> <seconds> |
Subscribe to a UUID for up to <seconds> seconds. The subscription is removed when that time expires. <ip> and <port> are the UDP source address observed by the server. |
| Notifier client → Server | Notify <uuid> |
Notified <uuid> <n> |
Notify all currently subscribed Listener clients. <n> is the number of notification packets sent. |
| Server → Listener client | Ready <uuid> |
— | Notification sent to every Listener subscribed to the UUID, once per Notify. |
| Any client → Server | Invalid command | Error: <reason> |
The command could not be parsed or validated. |
Multiple Listener clients can subscribe to the same UUID — a Notify sends
Ready to all of them. Ready Bell runs over UDP and provides no delivery
guarantees: commands, responses, and notifications may be lost, duplicated, or
reordered. A Notified response only confirms the server attempted to send
the notification, not that it arrived — keep your regular re-check schedule running as
the fallback, and treat Ready purely as a hint to check sooner.
Code examples
Every example below talks to the live service at ready-bell.com:3137 —
nothing to run or host yourself.
- Run
node listener.jsin one terminal - Run
node notifier.jsin a second terminal
import dgram from "dgram";
const socket = dgram.createSocket("udp4");
const uuid = "3fa85f64-5717-4562-b3fc-2c963f66afa6";
socket.on("message", msg => console.log(msg.toString().trim()));
socket.send(`Listen ${uuid} 30`, 3137, "ready-bell.com");
import dgram from "dgram";
const socket = dgram.createSocket("udp4");
const uuid = "3fa85f64-5717-4562-b3fc-2c963f66afa6";
socket.once("message", msg => (console.log(msg.toString().trim()), socket.close()));
socket.send(`Notify ${uuid}`, 3137, "ready-bell.com");
- Run
python listener.pyin one terminal - Run
python notifier.pyin a second terminal
import socket
uuid = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
addr = ("ready-bell.com", 3137)
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto(f"Listen {uuid} 30".encode(), addr)
while True:
data, _ = s.recvfrom(512)
print(data.decode().strip())
import socket
uuid = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
addr = ("ready-bell.com", 3137)
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto(f"Notify {uuid}".encode(), addr)
data, _ = s.recvfrom(512)
print(data.decode().strip())
- Run
java listener.javain one terminal - Run
java notifier.javain a second terminal
import java.net.*;
void main() throws Exception {
var uuid = "3fa85f64-5717-4562-b3fc-2c963f66afa6";
var address = new InetSocketAddress("ready-bell.com", 3137);
try (var socket = new DatagramSocket()) {
var request = ("Listen " + uuid + " 30").getBytes();
socket.send(new DatagramPacket(request, request.length, address));
var packet = new DatagramPacket(new byte[512], 512);
while (true) {
socket.receive(packet);
System.out.println(new String(packet.getData(), 0, packet.getLength()).trim());
}
}
}
import java.net.*;
void main() throws Exception {
var uuid = "3fa85f64-5717-4562-b3fc-2c963f66afa6";
var address = new InetSocketAddress("ready-bell.com", 3137);
try (var socket = new DatagramSocket()) {
var bytes = ("Notify " + uuid).getBytes();
socket.send(new DatagramPacket(bytes, bytes.length, address));
var packet = new DatagramPacket(new byte[512], 512);
socket.receive(packet);
System.out.println(new String(packet.getData(), 0, packet.getLength()));
}
}
Drop-in integration for a Quarkus service, taken from the
ready-bell-demo project.
Incoming Ready notifications are fired as CDI events — handle them with
@Observes ReadyBellEvent.
package readybell;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.Executor;
import io.quarkus.logging.Log;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event;
import jakarta.inject.Inject;
@ApplicationScoped
public class ReadyBellService {
private static final String REMOTE_HOST = "ready-bell.com";
private static final int REMOTE_PORT = 3137;
@Inject
Executor executor;
@Inject
Event<ReadyBellEvent> readyEvent;
private DatagramSocket socket;
@PostConstruct
public void init() throws Exception {
socket = new DatagramSocket();
executor.execute(this::listenLoop);
Log.infof("ReadyBellService bound to port: %s", socket.getLocalPort());
}
@PreDestroy
public void destroy() {
if (socket != null && !socket.isClosed()) {
socket.close();
Log.info("ReadyBellService UDP socket closed.");
}
}
public boolean sendListen(UUID uuid, int seconds) {
return send("Listen " + uuid + " " + seconds);
}
public boolean sendNotify(UUID uuid) {
return send("Notify " + uuid);
}
private boolean send(String message) {
InetSocketAddress target = new InetSocketAddress(REMOTE_HOST, REMOTE_PORT);
if (target.isUnresolved()) {
Log.debugf("Failed to resolve %s, dropping message: %s", REMOTE_HOST, message);
return false;
}
try {
var bytes = message.getBytes(StandardCharsets.UTF_8);
socket.send(new DatagramPacket(bytes, bytes.length, target));
Log.debugf("Sent UDP packet: %s", message);
return true;
} catch (IOException e) {
Log.warnf(e, "Failed to send UDP packet: %s", message);
return false;
}
}
private void listenLoop() {
var buffer = new byte[512];
var packet = new DatagramPacket(buffer, buffer.length);
while (!socket.isClosed() && !Thread.currentThread().isInterrupted()) {
try {
socket.receive(packet);
var message = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8).trim();
Log.debugf("Received UDP packet: %s", message);
if (message.startsWith("Ready ")) {
UUID uuid = UUID.fromString(message.substring("Ready ".length()).trim());
readyEvent.fire(new ReadyBellEvent(uuid));
}
} catch (SocketException e) {
Log.infof("UDP socket closed, stopping listener.");
break;
} catch (Exception e) {
Log.errorf(e, "UDP reader error: %s", e.getMessage());
}
}
}
}
package readybell;
import java.util.UUID;
public record ReadyBellEvent(UUID uuid) {
}
An asynchronous API made of two REST requests — POST starts a job,
GET reads its status — wrapped in an async function:
const result = await callAsyncApi("Alice", 7);
Between checks it waits on readyBell.wait(), which returns as soon as
Ready arrives.
See the detailed explanation in An Async API in One Function.
- Put both files in one directory
- Run
node call-async-api.js Alice 7(Node.js 22 or newer)
import dgram from "dgram";
const BACKLOG_TTL_MS = 60000;
export class ReadyBell {
waiters = new Map();
backlog = new Map();
socket = dgram.createSocket("udp4");
constructor() {
this.socket.on("message", msg => this.onMessage(msg.toString().trim()));
this.socket.unref();
}
sendListen(uuid, seconds) {
this.evictStale();
this.socket.send(`Listen ${uuid} ${seconds}`, 3137, "ready-bell.com");
}
sendNotify(uuid) {
this.socket.send(`Notify ${uuid}`, 3137, "ready-bell.com");
}
wait(uuid, ms) {
if (this.backlog.delete(uuid)) return Promise.resolve();
this.sendListen(uuid, 2 * ms / 1000);
return new Promise(resolve => {
const wakeUp = () => {
clearTimeout(timer);
this.waiters.delete(uuid);
resolve();
};
const timer = setTimeout(wakeUp, ms);
this.waiters.set(uuid, wakeUp);
});
}
onMessage(text) {
console.log(text);
const [command, uuid] = text.split(" ");
if (command !== "Ready") return;
this.evictStale();
const wakeUp = this.waiters.get(uuid);
if (wakeUp) wakeUp();
else this.backlog.set(uuid, Date.now() + BACKLOG_TTL_MS);
}
evictStale() {
for (const [uuid, expires] of this.backlog) {
if (expires < Date.now()) this.backlog.delete(uuid);
}
}
}
// Usage: node call-async-api.js [name] [dbg-delay-sec]
import { ReadyBell } from "./ready-bell.js";
const API = "https://demo.ready-bell.com/hello-async";
const POLL_INTERVAL_MS = 15000;
const TIMEOUT_MS = 60000;
const readyBell = new ReadyBell();
const [name = "Alice", delaySec = "4"] = process.argv.slice(2);
const started = performance.now();
const result = await callAsyncApi(name, Number(delaySec));
console.log(result);
console.log(`callAsyncApi took ${((performance.now() - started) / 1000).toFixed(3)} s`);
async function callAsyncApi(name, delaySec) {
const uuid = crypto.randomUUID();
const deadline = Date.now() + TIMEOUT_MS;
readyBell.sendListen(uuid, 30);
await submitAsyncJob(uuid, name, delaySec);
while (Date.now() < deadline) {
await readyBell.wait(uuid, POLL_INTERVAL_MS);
const job = await fetchFinishedJob(uuid).catch(err => console.log(err.message));
if (job) return job;
}
throw new Error(`Timed out after ${TIMEOUT_MS} ms`);
}
function submitAsyncJob(uuid, name, debugDelay) {
return fetch(`${API}/${uuid}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: { name, "dbg-delay-sec": debugDelay } }),
}).then(res => {
if (!res.ok) throw new Error(`POST failed: HTTP ${res.status}`);
});
}
function fetchFinishedJob(uuid) {
return fetch(`${API}/${uuid}`).then(res =>
res.ok ? res.json() : Promise.reject(new Error(`GET failed: HTTP ${res.status}`))
).then(job =>
job.status === "IN_PROGRESS" ? Promise.reject(new Error("Job IN_PROGRESS")) : job
);
}