Managed real-time services: patterns, pitfalls and fixes
By day 95 of our Kafka and WebSocket journey, you've likely built custom real-time infrastructure and felt the operational weight. Managed real-time services promise to shoulder that burden—connection scaling, message delivery, presence tracking—but only if you understand when to use them and where they'll cost you. This article distills patterns and pitfalls we've seen across production e-commerce, SaaS, and trading platforms, so you can make the boring choice that actually works.
Connection scaling handled for you
The most seductive promise of managed real-time platforms is horizontal scaling without thought. Services like Firebase Realtime Database, Pusher, Ably, or AWS AppSync handle the hard parts: socket pooling, connection multiplexing, node failover, and load balancing across regions. You never wake up at 3 AM because a new campaign drove connection count past your single-box limit.
Compare this to running your own WebSocket server. You build connection pooling logic, track session state, implement reconnection logic, monitor per-instance memory, and script graceful shutdowns. It works—but it's work, and it compounds as you add regions or scale to thousands of concurrent users.
Where managed services shine:
- Sudden traffic spikes. A flash sale or viral moment scales from 100 to 50,000 connections in seconds. Managed services auto-scale; your custom server fleet might not keep up.
- Geographic distribution. Ably and Pusher maintain edge nodes globally. Latency to Tokyo drops from 300ms to 50ms without code changes.
- Operational simplicity. No capacity planning meetings, no node failure playbooks, no custom monitoring dashboards.
A typical e-commerce checkout flow might use Pusher like this:
// On your Node.js backend
import Pusher from 'pusher';
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_KEY,
secret: process.env.PUSHER_SECRET,
cluster: 'us2'
});
app.post('/orders', authenticateUser, async (req, res) => {
const order = await createOrder(req.user, req.body);
// Notify all clients watching this user's order stream
await pusher.trigger(`user-${req.user.id}`, 'order-created', {
orderId: order.id,
total: order.total,
status: 'pending'
});
res.json({ orderId: order.id });
});
On the browser, you subscribe once and forget about reconnection logic—Pusher's client library handles it. This is the boring, reliable choice for that use case.
However, scaling also means cost scales. That's the trade-off we'll explore next.
Presence and history often included
Managed platforms bundle features you'd otherwise build yourself. Presence (who's online right now) and message history (what did I miss?) are common examples.
Presence is particularly useful in collaborative tools and live support. Instead of custom Redis sets tracking active users, you declare presence on connect and the platform maintains it:
// Browser client
const channel = pusher.subscribe('chat-room-42');
channel.bind('pusher:member_added', (member) => {
console.log(`${member.info.name} joined (${member.user_id})`);
});
channel.bind('pusher:member_removed', (member) => {
console.log(`${member.info.name} left`);
});
// Initial presence state
channel.members.each((member) => {
renderUserAvatar(member.info.name);
});
On your backend, you trigger events knowing the platform maintains member lists:
await pusher.trigger('chat-room-42', 'new-message', {
userId: user.id,
text: req.body.message,
timestamp: Date.now()
});
History is similarly baked in. Some platforms (like Ably with the rewind parameter) keep the last N messages per channel, so a disconnected client can catch up without hitting your database:
// Browser: reconnect and automatically fetch last 100 messages
const channel = ably.channels.get('news-feed', {
params: { rewind: '100' }
});
This is convenient for news feeds, live trading tickers, and chat applications. But there's a catch: history is usually limited (last 24 or 72 hours, often for a fee), and it's not durable across platform failures in the way your PostgreSQL backup would be. For compliance-heavy domains like financial trading or healthcare, you still want authoritative history in your own database.
The pattern: use managed presence and short-term history for UX polish, but never rely on it for compliance or audit trails.
Pricing per connection and message
This is where managed services reveal their teeth. Most charge by concurrent connections and/or message volume. Pusher bills per peak concurrent connections per month; Firebase charges per GB downloaded; Ably charges per million messages. It sounds reasonable until your trading desk has 500 power users who each hold 50 open subscriptions, or your gaming backend broadcasts a leaderboard update to 100,000 players every second.
Let's do the math. Pusher's standard tier caps at 100 concurrent connections free, then $49/month for 1,000, $499/month for 10,000, and $4,990/month for 100,000. A single production outage where clients reconnect in a thundering herd and linger in a reconnection loop can spike your concurrent count and bill you mid-month.
Message volume is similarly brutal at scale. A messaging platform broadcasting to 10,000 users 10 times per second is 100,000 messages/second—roughly 8.6 billion per day. At Ably's $0.10 per million, that's $860/day or $25,800/month, before peak multipliers.
A financial trading firm we worked with initially loved Firebase Realtime for price ticks until their bill jumped from $200 to $12,000 in a single market-open spike. They didn't have budget authority to approve that increase, so they migrated to self-hosted Kafka + WebSocket servers within weeks.
This doesn't mean avoid managed services—it means understand your traffic pattern first:
- Are you broadcasting or point-to-point? (Broadcast is cheaper on Kafka; managed services charge per connection and message.)
- Do you have traffic spikes or steady-state? (Managed is better for spikes; custom infrastructure is cheaper for predictable load.)
- Is message history required? (If not, managed services are overkill.)
Build a cost model. For a SaaS application with 5,000 concurrent users and 10 messages/second per user, that's 50,000 messages/second. At scale, your own Kafka cluster and WebSocket servers might cost $2,000–$5,000/month in infrastructure; Ably would cost $50,000+. The boring choice is worth measuring.
Keep business logic in your backend
The seductive trap is letting the managed real-time service become your source of truth. Developers new to these platforms often push validation, authorization, and state transitions into channel bindings or client-side event handlers. This breaks down fast.
Consider a ride-hailing app broadcasting driver locations. Tempting pattern:
// DON'T DO THIS
const channel = ably.channels.get(`driver-${driverId}`);
channel.publish('location-update', {
latitude: geoData.lat,
longitude: geoData.lng,
heading: geoData.heading
});
The problems:
- No validation. A malicious app sends
{ latitude: 'injection', longitude: 999 }. - No authorization checks. Any authenticated user can publish to any driver channel.
- Race conditions. Two location updates arrive out of order and you display the older one as current.
- No audit trail. You can't explain to regulators why a driver's location jumped 50 miles.
Correct pattern: send all events to your backend first, validate and authorize there, then broadcast via the managed service:
// Browser app sends location to YOUR backend
fetch('/api/driver/location', {
method: 'POST',
body: JSON.stringify({ latitude, longitude, heading }),
headers: { 'Authorization': `Bearer ${token}` }
});
// Your backend on Node.js
app.post('/api/driver/location', authenticate, async (req, res) => {
const { latitude, longitude, heading } = req.body;
// Validate
if (!isValidCoordinate(latitude, longitude)) {
return res.status(400).json({ error: 'Invalid location' });
}
// Authorize: only drivers can update their own location
const driver = await Driver.findById(req.user.id);
if (!driver || driver.status !== 'active') {
return res.status(403).json({ error: 'Not authorized' });
}
// Update database (source of truth)
await driver.update({ latitude, longitude, heading, updatedAt: Date.now() });
// Broadcast to consumers
await ably.channels.get('driver-updates').publish('location', {
driverId: driver.id,
latitude,
longitude,
heading,
timestamp: Date.now()
});
res.json({ ok: true });
});
Now you have:
- Validation and authorization on every update
- A single source of truth in your database
- Auditability
- Ability to apply business rules (rate limiting, geofencing, fraud detection)
The managed service is a delivery mechanism, not a data store. Keep that separation clear.
Common mistakes
Mistake 1: Treating managed history as your database. You query the managed service's message history for reporting. Then they prune it after 72 hours and you've lost data. Store durable data in PostgreSQL or MongoDB; use managed history only for recent UX.
Mistake 2: Not budgeting for reconnection storms. A deployment or network hiccup causes 50,000 clients to reconnect within 5 seconds. Your peak concurrent connection count spikes, and so does your bill. Implement exponential backoff and jitter in your client reconnection logic.
Mistake 3: Publishing sensitive data to public channels. A developer publishes user payment data or API keys to a channel thinking "it's encrypted in transit." A malicious actor subscribes and harvests credentials. Always encrypt sensitive payloads, and never send what shouldn't be broadcast.
Mistake 4: Over-subscribing clients. A React component subscribes to a channel on mount without unsubscribing on unmount. After navigating the app 50 times, the client has 50 subscriptions open, draining battery and contributing to your peak connection count. Use cleanup functions religiously.
Mistake 5: Mixing real-time and request-response patterns carelessly. You broadcast an order update via the managed service, but the client fetches inventory via REST immediately after, and races cause a stale read. Coordinate your patterns: if real-time matters, use it end-to-end.
Key takeaways
- Managed real-time services excel at operational simplicity and handling traffic spikes; use them when you don't want to maintain custom WebSocket infrastructure.
- Presence and history are convenient but limited; never rely on them for compliance or audit trails. Keep your database as the source of truth.
- Model your costs before committing. Message-based billing scales poorly for high-volume broadcast scenarios; self-hosted Kafka may be cheaper and more flexible.
- Keep all business logic—validation, authorization, state transitions—in your backend. Use the managed service only as a delivery channel for events you've already validated and approved.
- Plan for reconnection storms and over-subscription. Implement proper client cleanup, exponential backoff, and per-user subscription limits.
- The boring choice is right: buy the service for scaling and ops simplicity if it fits your cost model, but never let it replace your backend or database.






Comments
Loading comments…