Building Real-Time Features with Ably
A practical guide to adding real-time communication to web and mobile applications using Ably's pub/sub infrastructure.
Real-time features — live location tracking, instant messaging, collaborative editing — have shifted from "nice to have" to baseline expectations. We've built real-time systems for ride-sharing platforms, social apps, and collaborative tools. Here's how we approach it with Ably.
Why Ably over rolling your own
You can absolutely run your own WebSocket server with Socket.io. We've done it. For a single-server deployment with a few hundred concurrent users, it works fine.
The problems start when you scale:
- Multiple server instances need shared state (Redis pub/sub, sticky sessions)
- Mobile clients disconnect and reconnect constantly (cellular handoffs, app backgrounding)
- Message ordering and delivery guarantees require careful engineering
- Presence (who's online) needs distributed consensus
Ably handles all of this as managed infrastructure. We publish messages, subscribe to channels, and let their global edge network handle the hard distributed systems problems.
Architecture pattern
Our standard real-time architecture looks like this:
Client (React/React Native)
↕ Ably SDK (WebSocket)
↕ Ably Channel
↕ Ably SDK (WebSocket)
Server (Express/Next.js)
↕ Database (PostgreSQL)
The key insight: Ably is a message bus, not a database. Every real-time event should also be persisted server-side. When a user sends a message, we:
- Client publishes to Ably channel (instant delivery to other clients)
- Server receives the same message via Ably subscription
- Server persists to database
- If the server-side processing fails, we can replay from the database
This gives us both instant delivery and data durability.
Token authentication
Never put Ably API keys in client code. We use token authentication where the client requests a short-lived token from our API:
// Server: token endpoint
app.get("/api/ably/token", authenticate, async (req, res) => {
const ably = new Ably.Rest(process.env.ABLY_API_KEY);
const token = await ably.auth.createTokenRequest({
clientId: req.user.id,
capability: {
[`ride:${req.user.activeRideId}`]: ["publish", "subscribe", "presence"],
"notifications:*": ["subscribe"],
},
});
res.json(token);
});
The capability object is powerful — we scope each token to exactly the channels and operations that user needs. A passenger in a ride-sharing app gets publish/subscribe access to their specific ride channel, nothing else.
Channel design for ride-sharing
For our ride-sharing platform, we use a channel-per-ride pattern:
ride:{rideId} → location updates, status changes
ride:{rideId}:chat → in-ride messaging
driver:{driverId} → new ride requests (driver only)
Location updates are the highest-frequency messages. We throttle client-side to one update per second and use Ably's publish with no history retention — we only care about the latest position:
// Driver app: publish location
const channel = ably.channels.get(`ride:${rideId}`);
watchPosition((position) => {
channel.publish("location", {
lat: position.coords.latitude,
lng: position.coords.longitude,
heading: position.coords.heading,
timestamp: Date.now(),
});
});
// Passenger app: subscribe to location
const channel = ably.channels.get(`ride:${rideId}`);
channel.subscribe("location", (message) => {
updateDriverMarker(message.data);
});
Presence for online status
Ably's presence feature solves the "who's online" problem without us building anything:
const channel = ably.channels.get(`ride:${rideId}`);
// Enter presence when component mounts
await channel.presence.enter({
role: "passenger",
name: user.name,
});
// Watch for presence changes
channel.presence.subscribe("enter", (member) => {
console.log(`${member.data.role} joined`);
});
channel.presence.subscribe("leave", (member) => {
console.log(`${member.data.role} left`);
});
This handles disconnection detection automatically. If a driver's phone loses signal, the passenger sees them go offline within seconds — no polling, no heartbeat logic on our side.
React Native considerations
Mobile adds complexity. The Ably React Native SDK handles most of it, but we've learned a few things:
Background handling: When the app backgrounds on iOS, WebSocket connections are suspended. We handle this by re-fetching the latest state from our API when the app foregrounds, then re-subscribing to Ably channels. The gap is usually under a second.
Connection recovery: Ably's SDK automatically reconnects with message continuity. Messages published while disconnected are delivered when the connection resumes (within the channel's history TTL). This is critical for ride-sharing — a 30-second cellular dead zone shouldn't lose location updates.
Battery optimization: We reduce location update frequency when the app is backgrounded and increase it when foregrounded. The Ably connection itself is lightweight — a single WebSocket handles all channels.
Cost considerations
Ably charges per message and per connection. For a ride-sharing app:
- Location updates: ~60 messages/minute per active ride (1/sec from driver)
- Chat messages: ~5-10 per ride
- Status updates: ~5 per ride lifecycle
A typical 15-minute ride generates roughly 900 messages. At Ably's pricing, this is fractions of a cent per ride. The engineering time saved by not running WebSocket infrastructure dwarfs the service cost.
When to build your own
Ably is the right choice for most real-time features. We'd consider building our own WebSocket infrastructure when:
- Message volume is extremely high (millions per second) and cost becomes significant
- Ultra-low latency is required (sub-10ms, gaming-type scenarios)
- The real-time logic is tightly coupled to server-side state machines
For the vast majority of applications — chat, notifications, live updates, collaborative features — a managed pub/sub service like Ably is the pragmatic choice.