Per-message database queries to determine channel membership collapse throughput to whatever the database can handle divided by messages per second. At modest scale — 50 messages per second across 10 channels — that is 50 synchronous permission queries in the hot path, turning a real-time feature into a slow polling system. ISO 25010 performance-efficiency and scalability require that read-hot paths use caching rather than per-operation round trips to persistent storage.
Medium because per-message permission queries add a synchronous database round-trip for every broadcast, collapsing throughput at moderate message rates.
Use the WebSocket framework's built-in room tracking — or an explicit in-memory/Redis membership map — as the fan-out source. Channel membership should be updated on subscribe/unsubscribe, not queried on send.
// Subscribe: update the membership cache once
socket.on('subscribe', async (channel: string) => {
if (await checkChannelAccess(socket.userId, channel)) {
socket.join(channel); // Socket.IO maintains membership in-process
}
});
// Send: fan out from the cache — zero DB queries
socket.on('send_message', (data: { channel: string; content: string }) => {
io.to(data.channel).emit('message', {
id: nanoid(),
content: data.content,
userId: socket.userId,
timestamp: Date.now(),
});
});
// Avoid: querying DB on every send
// const members = await db.select().from(channelMembers).where(...);
ID: community-realtime.presence.fanout-cache-not-per-message
Severity: medium
What to look for: Count all message broadcast paths. For each, classify whether the system uses a membership cache or performs per-message database queries. Enumerate the caching mechanisms: framework rooms, Redis sets, in-memory maps.
Pass criteria: The system maintains at least 1 channel membership cache (or uses the WebSocket framework's built-in room/namespace tracking). Messages are fanned out to cached subscribers with 0 per-message permission queries.
Fail criteria: For every message, the system queries a database to determine which users have permission to receive it.
Skip (N/A) when: Never — per-message permission checks would not scale.
Detail on fail: "Every message triggers a database query to determine channel subscribers. This will not scale beyond tens of messages per second."
Remediation: Use membership caching for fan-out:
// Use Socket.IO's built-in room management
socket.on('subscribe', (channel) => {
socket.join(channel); // Cached by Socket.IO
});
// Broadcast is fast because Socket.IO has the membership
socket.on('send_message', (data) => {
io.to(data.channel).emit('message', data);
});
// Avoid this pattern:
socket.on('send_message', async (data) => {
const members = await db.query('SELECT userId FROM channel_members WHERE channel = ?', [data.channel]);
for (const member of members) {
io.to(member.userId).emit('message', data); // Slow!
}
});