That initial simplicity can be deceiving. Under the hood, Firebase’s Realtime Database is essentially a cloud-hosted JSON document store that clients listen to for changes. All message exchanges have to be written to and read from the database, there’s no direct client-to-client messaging channel. This means every single chat message hits the disk (incurring latency and cost) before reaching the recipient. For a small user base this isn’t a big deal, but at scale it introduces unnecessary overhead and tight coupling between the messaging logic and the storage layer. reference

More importantly, Firebase wasn’t built specifically for chat, so it lacks many real-time communication niceties out-of-the-box. For example, tracking user presence (who’s 🟢online/⚪offline) is something you might expect in any chat; yet Firestore doesn’t support presence natively at all. You have to bolt on a separate system (like a parallel Realtime Database and some Functions) just to know when users connect or disconnect. Similarly, implementing things like read receipts, typing indicators, or efficient conversation history retrieval requires building custom logic atop Firebase’s general-purpose APIs.

On the scaling front, Firebase’s simple architecture also has hard limits. A single instance of the Firebase Realtime Database caps out around 200,000 simultaneous connections for WebSocket clients. Beyond that, you’re forced to shard your data across multiple database instances. Splitting your user base across multiple Firebase databases is no trivial task; it’s a heavy operational burden that you have to manage yourself. Hitting these limits might not be an immediate concern for every app, but if you aspire to significant scale, it’s something you’ll need to carefully consider.

Even beyond performance and scalability, adopting Firebase for chat introduces significant privacy, security, and compliance concerns. Since all your messages—including private conversations, presence updates, and read-receipt events—live on Google’s infrastructure, you’ll need to carefully craft complex Firebase Security Rules to enforce who can read or write specific data paths. Meeting regulatory requirements like GDPR, CCPA, and similar data protection mandates becomes daunting, with several specific pain points emerging:

  • Chat apps frequently require fine-grained permissions eg, restricting access to conversation threads to only their participants. Implementing these detailed security rules in Firebase can be challenging, error-prone, and difficult to audit. A minor misconfiguration might unintentionally expose private messages or lock legitimate users out entirely. Given Firebase’s scattered, hierarchical JSON data structure, reliably enforcing these nuanced permissions becomes even more questionable, complicating both security and compliance efforts.

  • Under GDPR, users can request deletion of their personal data. With Firebase, deleting scattered chat histories across multiple JSON branches and ensuring that caches, backups, and audit logs are thoroughly purged - turns into a significant and ongoing operational challenge.

Consider a scenario where users can delete messages just for themselves (without removing them from other participants’ histories), a common feature in modern platforms. When you introduce group chats (Multi-User Chats, or MUCs), the complexity skyrockets.

In short, using Firebase for chat can carry you through a prototype and maybe a modest launch, but you risk slamming into walls as your app gains traction. It’s a fantastic building block for many applications, but real-time chat at scale happens to highlight where Firebase is weakest: fine-grained control and purpose-built realtime features.

Nostalgia Trip. ICQ/IRC to Jabber

Thinking about chat scalability and reliability takes me back to the early days of instant messaging. If you’re old enough to remember the dial-up era, names like ICQ, MSN and AIM might ring a bell. Those were the wild west of proprietary chat protocols – each platform was its own island. Then came Jabber some 26 years ago, an open-source messaging system that introduced the world to what we now know as XMPP (Extensible Messaging and Presence Protocol).

I was first introduced to Jabber by my friend BC who managed a Jabber server on default.co.yu, which I extensively used for years. In the early days, our clients of choice were Psi and Adium on macOS, real dinosaurs of Jabber client implementations. Amazingly, these classic apps still work flawlessly with newest XMPP servers, which speaks volumes about the timeless design and foresight behind the protocol.

For a while, it felt like we might get a unified, federated chat ecosystem. I remember when Google Talk adopted XMPP; suddenly you could chat from Gmail with friends on other XMPP services, no siloed app needed. It wasn’t just about instant messaging; it was about escaping the closed ecosystems of propriatery messeging systems. We saw a world of federated chat servers (much like email) where anyone could host their own community, yet chat across server boundaries - in real-time. BC’s default.co.yu became our small digital clubhouse, a tight-knit community that expanded beyond our immediate circle as we federated with other Jabber servers around the globe.

Of course, that utopia didn’t fully materialize. Mainstream networks closed off (Google eventually dropped XMPP when it moved to Hangouts, and Facebook shut down its XMPP gateway), and mobile messaging veered toward closed platforms. But XMPP never went away. In fact, it quietly became the backbone for some massive services.

Fun fact: the early version of WhatsApp famously ran on an adapted XMPP stack (built on the open-source ejabberd server). XMPP is also found in places you might not expect. For example, it powered chat and notifications in certain gaming networks (Nintendo, Epic among others) and consoles, and it’s used in many enterprise chat systems.

Fast-forward to today, and that open, decentralized vision is still alive and kicking—even if it’s slightly overshadowed by proprietary messaging giants. Every time I log into a modern XMPP server like MongooseIM, I’m reminded of why openness matters. It’s incredible to see old-school clients like Psi and Adium still working perfectly alongside modern mobile and web apps. That’s because the core XMPP protocol, the heart of Jabber, was built from day one with openness and extensibility in mind.

Over the years, XMPP influenced not just messaging apps, but how we think about open communication itself. Companies and products have come and gone—Google Talk itself being a notable casualty—but Jabber lives on, evolving gracefully without ever losing sight of its original values or breaking compatibility.

What’s appealing about XMPP today is that it offers a ready-made foundation for features we’d otherwise have to build from scratch. It standardizes how to do presence, how to send typing notifications, how to handle offline messages, how to do group chats, and so on. All the stuff we expect a chat app to handle. Instead of reinventing those wheels (or bolting them awkwardly onto a system like Firebase), you can leverage XMPP’s decades of refinement. And unlike a proprietary service, XMPP is open and self-hostable. That’s a huge win for anyone worried about vendor lock-in or compliance requirements. It feels ironic that in 2025, an open protocol from the early 2000s is ticking boxes that some modern “serverless” solutions can’t tick without extra help.

That’s the beauty of open standards done right — they’re resilient, inclusive, and outlive the companies and apps built upon them.
Thanks, BC, for running default.co.yu for all those years.

I highly recommend reading The State of Mobile XMPP in 2016 if in doubt.

You could roll your own

Building your own chat server with raw WebSockets (or using a lightweight framework like Socket.io, SignalR, etc.) gives you maximum control. You can design the message protocols (JSON, Protocol Buffers, custom binary) to fit your exact needs, integrate domain-specific logic directly on message events, and optimize everything for your particular workload. There’s a certain appeal to the DIY approach – no external dependencies, and potentially very high efficiency for your use case. Many game developers, for instance, build custom real-time messaging backends tuned to their game’s mechanics and scale requirements. If you have the expertise, you can create a system that does exactly what you need and nothing more, which can be both fun and rewarding.

The downside is that you’re taking on all the responsibility. Designing a robust, secure, and scalable chat system from scratch is hard. Think about what a production-ready messaging service entails: auth, session, presence, delivering messages in order (and maybe with ack), reconnection and offline scenarios, storing message history, supporting group chats, perhaps end-to-end encryption, and more. In practice, that means you often end up reinventing functionality that protocols like XMPP or Matrix already solved years ago. Moreover, scaling a WebSocket service is non-trivial. Once you go beyond one server, you need to coordinate state across servers (which user is connected to which node, where to route a given message, etc.), usually via a shared database or message broker. Load balancing WebSocket connections might require sticky sessions or a custom routing layer. All of this adds engineering complexity. Unless real-time chat is the core product you’re offering, rolling your own is a labor of love (or folly) that can distract from building the product’s unique value.

Practical Example

If all of this sounds great in theory but you’re wondering what using XMPP actually looks like in code, let’s take a look at a minimal working example using stanza.js, a modern XMPP library for the web. This example connects to a public XMPP server and joins a group chat:

import { createClient } from 'stanza'

const client = createClient({
  jid: 'cvele@default.co.yu',
  password: 'users-jwt-token',
  transports: ['websocket'],
  wsURL: 'wss://default.co.yu/ws',
})

client.on('session:started', async () => {
  console.log('Connected and authenticated')

  client.sendPresence({
    status: 'Ready to chat',
    show: 'chat', // away | dnd | xa | chat | null (or implement custom)
  })

  client.joinRoom('room@conference.default.co.yu', 'cvele')

  client.sendMessage({
    to: 'room@conference.default.co.yu',
    type: 'groupchat',
    body: 'Edesi!',
  })
})

client.on('message', (msg) => {
  if (msg.body) {
    console.log(`[${msg.from}] ${msg.body}`)
  }
})

client.on('presence', (pres) => {
  const from = pres.from
  const show = pres.show || 'available'
  const status = pres.status || ''

  console.log(`${from} is now ${show}${status}`)
})

client.connect()

This compact example shows how approachable XMPP development can be. You’re not locked into one ecosystem or tool but are building on a protocol designed from day one to be interoperable, extensible, and user-controlled. We already supprased all those Firebase chat tutorials, notice we bundled presence ?

Want to extend it with typing indicators, read receipts, or message archiving? There’s a spec for that. Literally.

XMPP for the Real World, MongooseIM

I’d like to talk about MongooseIM specifically, because it’s my go-to XMPP server last two projects. MongooseIM is an open-source, enterprise-grade XMPP server that was born out of the ejabberd codebase (both are written in Erlang). It was designed with large-scale deployments in mind, a single MongooseIM node can handle as many as 2.5 million concurrent users on suitable hardware, and with a cluster of nodes the devs have tested configurations with 10+ million users.

But beyond raw numbers, what I appreciate is the richness of features. MongooseIM comes with modules for everything you’d expect: group chat (XEP-0045 MUC), message archiving (XEP-0313 MAM), offline message queuing, vCard user profiles, and even newer conveniences like Message Carbons (which automatically sync your messages to all your logged-in devices) and built-in push notifications support for mobile devices. In essence, it provides the building blocks to support the kind of messaging experience users expect in 2025, but in a self-hosted package.

One feature that really highlights MongooseIM’s approach is the Inbox extension. When you open a chat app these days, you typically see an inbox view, a scrolling list of recent conversations, each with a contact name, a snippet of the last message, and perhaps a badge count of unread messages. Historically, implementing that UI with plain XMPP was a bit of a chore: the client would have to fetch the roster (contact list), pull message archives for each chat, track unread counts, etc., and then mash that data together. MongooseIM simplifies this by tracking conversations on the server side. The mod_inbox module keeps a live index of each user’s active conversations, including the latest message and an unread count for each thread. This means your client can fetch one “inbox summary” from the server to get all that info in one go, instead of making dozens of queries. It’s basically an inbox-style conversation view baked into the server.

Scalability and reliability are strong suits of MongooseIM (and XMPP servers in general). Because it’s built on the Erlang/OTP platform, it inherits the famous fault-tolerance and concurrency strengths of that ecosystem. You can run MongooseIM in a clustered mode across multiple machines; new nodes will linearly increase the number of users you can handle (up until some practical limit where you might re-architect the cluster). In one public benchmark, a single MongooseIM server was able to handle about 2.5 million simultaneous connections, exchanging ~45,000 messages per second, without hitting a hard limit. In another test, a cluster of 15 MongooseIM nodes handled over 1.2 million users with proportional throughput, and the team was confident they could reach many millions more with a bit of tuning.

The point is: it’s unlikely your app will outgrow what a well-deployed XMPP server can handle. And if it does, you’ll probably have the resources to tackle that (since at 10 million users, you’re probably doing quite well!). Equally important, you have ownership of the stack – you can deploy on your own cloud or on-prem, scale up or out as needed, and you’re not at the mercy of a third-party platform’s pricing or downtime. If something goes awry, you have the insight (logs, metrics) and the control to fix it.

Now, I won’t claim that adopting MongooseIM (or any XMPP server) is all sunshine and roses. The learning curve can be a bit steep at the start. The documentation is extensive but can be overwhelming when you’re new to XMPP (there’s a lot of terminology to absorb), and getting things configured just right might take a few trial-and-error cycles. If you have very unique messaging requirements, you might even find yourself writing a custom server-side plugin in Erlang/Elixir to extend MongooseIM – and not every team has an Erlang expert on hand.

However, in my experience these hurdles are far outweighed by the benefits. Once it’s up and running, MongooseIM tends to be remarkably stable and low-maintenance. It’s designed to handle unreliable networks and high churn (users connecting/disconnecting frequently), which are exactly the conditions of the real world. We’ve had deployments running for months without a hiccup – something I’d be hesitant to say about a homegrown Node.js WebSocket server, for example. The effort you invest in learning the platform pays off when you don’t have to constantly patch around the limitations of a generic solution. In short, MongooseIM lets you spend more time building your app’s unique features, because the messaging engine has your back.

The key takeaway is that you don’t need to invent a chat engine from scratch to deliver a great messaging experience. Protocols like XMPP are real chat engines – ones that have powered giant platforms and been hardened over many years. By building on top of them, you get scalability, reliability, and rich features by design, not as an afterthought. So if you’re serious about your app’s chat feature, consider stepping off the Firebase hamster wheel and standing on the shoulders of these giants. You’ll spend less time worrying about how to make chat work, and more time delivering what makes your application special. In the long run, your future self (and your users) will thank you.