Introduction
What you send through Action Cable matters just as much as how you send it. Poorly structured payloads waste bandwidth, leak sensitive data, and create fragile client-side code. This lesson covers how to design clean, efficient broadcast payloads that serve your frontend well.
Key Concepts
- Payload Serialization: The process of converting Ruby objects into JSON for transmission over WebSockets. Action Cable automatically serializes hashes to JSON.
- Selective Serialization: Including only the fields the client needs, rather than dumping entire model attributes.
- Payload Schema: A consistent structure for broadcast messages that includes a type identifier and the relevant data.
Real World Context
Broadcasting user.as_json sends every column — including encrypted_password, email, and created_at — to every connected client. Besides the security risk, it wastes bandwidth and forces the client to sift through irrelevant fields. Production applications define explicit payload shapes, much like API serializers for HTTP endpoints.
Deep Dive
A well-structured broadcast payload has a clear schema:
ruby# Good: explicit, typed payload ChatChannel.broadcast_to(room, { type: "new_message", data: { id: message.id, body: message.body, user: { id: message.user.id, username: message.user.username, avatar_url: message.user.avatar_url }, created_at: message.created_at.iso8601 } })
Compare this with a lazy approach:
ruby# Bad: leaks data, wastes bandwidth ChatChannel.broadcast_to(room, message.as_json(include: :user))
For reusable payload construction, create a broadcast builder or use a serializer:
ruby# app/builders/message_broadcast_builder.rb class MessageBroadcastBuilder def self.call(message) { type: "new_message", data: { id: message.id, body: message.body, user: { id: message.user.id, username: message.user.username, avatar_url: message.user.avatar_url }, created_at: message.created_at.iso8601 } } end end # Usage ChatChannel.broadcast_to(room, MessageBroadcastBuilder.call(message))
This pattern keeps payload definitions in one place, making it easy to update the structure when the frontend needs change.
When working with timestamps, always use ISO 8601 format:
ruby# Good: unambiguous, timezone-aware created_at: message.created_at.iso8601 # Output: "2026-03-06T14:30:00Z" # Bad: locale-dependent, ambiguous created_at: message.created_at.to_s # Output: "2026-03-06 14:30:00 +0000"
Common Pitfalls
- Using
as_jsonorto_jsonwithout filtering — These methods serialize all model attributes by default, including sensitive fields. Always specify which fields to include. - Inconsistent payload shapes — If some broadcasts include a
typefield and others do not, client-side routing logic breaks. Establish a convention and follow it everywhere. - Sending timestamps in local time — Local time formats vary by server locale and are ambiguous. Always use
iso8601for timestamps in broadcasts.
Best Practices
- Create broadcast builder objects — Centralize payload construction in dedicated classes. This ensures consistency and makes changes easy.
- Always include a
typefield — Every broadcast should identify its purpose so the client can route it appropriately. - Use ISO 8601 for all timestamps — It is unambiguous, timezone-aware, and natively parsed by JavaScript's
Dateconstructor.
Summary
- Never broadcast raw ActiveRecord objects — serialize only the fields the client needs.
- Structure every payload with a
typefield and adataobject for consistency. - Use broadcast builder objects to centralize and standardize payload construction.
- Always format timestamps as ISO 8601 strings.
- Treat broadcast payloads with the same care as API responses — they are a public contract with your frontend.
Code Examples
class MessageBroadcastBuilder
def self.call(message)
{
type: "new_message",
data: {
id: message.id,
body: message.body,
user: {
id: message.user.id,
username: message.user.username
},
created_at: message.created_at.iso8601
}
}
end
end