We're Hiring: Interns 🚀

Apply Now →

Updates & announcements

Enally announcement
We're Hiring: Interns 🚀

We're hiring interns for Full-Stack Development and Social Media Marketing. Join Enally to work on real products, campaigns, and ideas in a fast-moving startup ecosystem.

Archilo: helping architecture work become visible.
Archilo: helping architecture work become visible.

A focused platform for architecture portfolios, research, talent and creative opportunity. Built for architects, students and studios.

Enally announcement
Enally: building useful things, together.

A founder-led ecosystem connecting products, services, knowledge, community and opportunities. One belief, expressed in different ways.

Enally announcement
Build with us: internships, contributors and partnerships.

Practical ways for young builders, contributors and domain experts to learn through real products and useful responsibility.

Archilo growing steadily
Archilo growing steadily

Architecture portfolios and research pages now serve 5,000+ creative professionals.

Humble campus expansion
Humble campus expansion

Verified student communities now active across multiple campuses with 2K+ members.

Faaho partner beta live
Faaho partner beta live

Zero-brokerage living discovery is now available in partner beta. Technology by Enally.

Enally announcement
Enally Labs launched

Applied AI experiments, internal agents and prototype products now live under Labs.

Enally announcement
Blog redesigned

The Enally blog now brings practical guides, opportunities and ecosystem knowledge together.

Enally announcement
Services: SEO to AIO

Five-layer visibility services now available — SEO, AEO, GEO, SXO and AI Optimization.

Enally announcement
Build with us program

Internships, campus ambassadors and contributor roles open for builders who want real ownership.

Enally announcement
Company website rebuilt

Enally.in redesigned with improved performance, accessibility and dark theme support.

Full-Stack Development

What Building HumbleApp.in Taught Me About Realtime Systems, Scaling, and Backend Engineering

Explore how HumbleApp.in approaches realtime chat, scalable backend architecture, Redis failures, database design, presence, caching, and reliable message delivery.

What Building HumbleApp.in Taught Me About Realtime Systems, Scaling, and Backend Engineering
What you'll learn

Explore how HumbleApp.in approaches realtime chat, scalable backend architecture, Redis failures, database design, presence, caching, and reliable message delivery.

Jump to the guide

What Building Humble Taught Me About Realtime Systems, Scaling, and Backend Engineering

Humble is a campus platform built around real student communities, conversations, events, discovery, and other parts of campus life.

When I started building its realtime features, I initially thought the problem was fairly straightforward:

User sends a message → Socket.IO sends it → another user receives it.

That works surprisingly well when an application is small.

Then I started asking different questions.

What happens when a user opens multiple tabs?

What happens when the realtime server restarts?

What happens when Redis stops responding?

What happens when a message is successfully saved but the realtime broadcast fails?

What happens when one server is no longer enough?

And perhaps the most important question:

What should happen when something goes wrong?

Those questions changed how I approached Humble's backend.

Instead of treating realtime communication as the system itself, I started treating it as one layer of a larger system.

This post is about that journey, the decisions behind it, and some of the things I still haven't solved perfectly.


1. The Problem Was Bigger Than "Add WebSockets"

Humble uses a hybrid architecture.

The main application handles the product itself: authentication, business logic, APIs, database operations, notifications and other application features.

The realtime layer is a separate Socket.IO service.

At a high level, the system looks like this:

                    ┌─────────────────┐
                    │     Browser     │
                    └────────┬────────┘
                             │
                  ┌──────────┴──────────┐
                  │                     │
                HTTPS                 WebSocket
                  │                     │
          ┌───────▼────────┐   ┌────────▼────────┐
          │   Main App     │   │ Socket Service  │
          │  REST / API    │   │   Realtime      │
          └───────┬────────┘   └────────┬────────┘
                  │                     │
                  ▼                     ▼
            ┌───────────┐         ┌───────────┐
            │ PostgreSQL│         │   Redis   │
            │  Durable  │         │ Cache /   │
            │   Data    │         │ Presence  │
            └───────────┘         │ Pub/Sub   │
                                  └───────────┘

The important part isn't the number of technologies.

It is why each technology exists.

The main application is the source of truth. PostgreSQL stores the durable state. Redis supports caching, presence, distributed coordination and realtime scaling. Socket.IO is primarily responsible for delivering events to connected clients.

That separation became one of the most important design decisions in Humble.


2. The Database Should Know About the Message Before the Socket Does

One of the biggest architectural changes I made was separating persistence from delivery.

A naive realtime architecture can look like:

Client
   ↓
Socket.IO
   ↓
Broadcast message

But then an uncomfortable question appears:

What if the socket server broadcasts something that never gets persisted?

The client may see a message that doesn't actually exist in the database.

Or the opposite:

What if the database succeeds but the socket broadcast fails?

That shouldn't mean the message disappears.

So Humble follows a different model:

Client
   │
   ▼
REST API
   │
   ▼
PostgreSQL
   │
   │ message is now durable
   ▼
Realtime broadcast
   │
   ▼
Socket.IO
   │
   ▼
Recipients

The message is first written to PostgreSQL.

Only after that does the application trigger the realtime broadcast.

The broadcast is deliberately non-blocking. If the realtime layer has a problem, the message still exists in the database and can be retrieved again by the client.

This led to a simple rule:

The database is the source of truth. Realtime delivery is an optimization for experience, not the system of record.

That distinction made several other decisions much easier.


3. Why I Didn't Make Socket.IO Responsible for Everything

Once persistence and delivery were separated, Socket.IO became much simpler to reason about.

The realtime service is responsible for things such as:

  • delivering new messages

  • presence

  • typing indicators

  • realtime notifications

  • joining chat rooms

  • communicating with connected clients

The application API remains responsible for:

  • authentication

  • authorization

  • business rules

  • database writes

  • durable messages

  • notifications as stored records

  • application state

This means a temporary realtime failure doesn't necessarily become an application failure.

That was an important lesson for me:

A feature can be realtime without making realtime the only way that feature can function.


4. Then Redis Became More Than a Cache

Initially, Redis is easy to think about:

Database → slow
Redis → fast

But as the system evolved, Redis became useful for several different jobs:

Responsibility Why Redis is useful
Caching Reduce repeated database reads
Presence Track active connections
Pub/Sub Coordinate multiple realtime servers
Rate limiting Control high-frequency operations
Socket.IO adapter Allow realtime servers to communicate

The important part was realizing that these responsibilities have different failure consequences.

A cache disappearing shouldn't destroy the application.

A realtime coordination service disappearing shouldn't corrupt the database.

A rate limiter disappearing shouldn't necessarily make the whole application unavailable.

That pushed me toward graceful degradation.


5. What Happens When Redis Goes Down?

This became one of my favorite engineering problems in Humble.

Suppose the Socket.IO service is using Redis and Redis suddenly becomes unavailable.

A simplistic implementation might do this:

Redis fails
   ↓
Socket.IO throws errors
   ↓
Server crashes
   ↓
Everyone disconnects

I didn't want that.

Instead, the realtime layer can fall back to an in-memory mode when Redis is unavailable.

                 Redis available
                       │
                       ▼
              ┌─────────────────┐
              │   Redis Adapter │
              └─────────────────┘
                       │
                   broadcasts
                       │

Redis fails ────────────┤
                       ▼
              ┌─────────────────┐
              │ In-memory mode  │
              └─────────────────┘
                       │
                local realtime

The implementation catches Redis-related failures, disables the Redis adapter, and continues operating in single-server mode rather than allowing the dependency failure to crash the realtime process. When Redis becomes stable again, the system can recover back to Redis-backed operation.

There is an important limitation here.

If there are multiple realtime servers and Redis disappears, those servers can continue serving their own connected clients, but they lose the cross-server coordination that Redis provides.

So graceful degradation does not mean:

"Nothing changes when Redis fails."

It means:

"The system loses non-essential capabilities before it loses the entire service."

That distinction is something I understand much better now.


6. Presence Sounds Easy Until You Have Multiple Tabs

"Online" sounds like a boolean.

online = true
offline = false

But a user might actually have:

Prashant
 ├── Chrome tab
 ├── Firefox tab
 ├── Mobile browser
 └── Another laptop

If one connection disappears, should the user become offline?

Obviously not.

So Humble treats presence as a connection count rather than simply a boolean.

Conceptually:

Tab 1 connects
connections = 1
→ user becomes online

Tab 2 connects
connections = 2
→ user remains online

Tab 1 closes
connections = 1
→ user remains online

Tab 2 closes
connections = 0
→ user becomes offline

The implementation uses Redis-backed reference counting, connection expiry and heartbeats. A heartbeat periodically refreshes the connection's lifetime so abandoned connections don't remain online forever.

This was one of those features that looked trivial from the UI but required much more thought on the backend.


7. Reconnection Is Part of the Feature

A realtime connection will eventually disappear.

The server can restart.

The network can change.

A laptop can wake up from sleep.

A reverse proxy can terminate a connection.

A user can temporarily lose connectivity.

So I didn't want the client to simply say:

Disconnected.
Good luck.

The client uses reconnection with increasing delays and jitter.

Conceptually:

Disconnect
    ↓
Retry
    ↓
1s
    ↓
2s
    ↓
4s
    ↓
...
    ↓
30s maximum

The randomization is important because if a server goes down while thousands of clients are connected, you don't want every client reconnecting at exactly the same moment.

That creates what's commonly called a thundering herd.

The client therefore uses exponential backoff with jitter and then re-syncs application state through the API after reconnecting.

The lesson for me was:

A realtime system isn't just about establishing a connection. It's also about recovering from losing one.


8. Database Design Started Becoming More Important

Once the application became more feature-rich, the database stopped being "just storage."

Humble's schema covers multiple domains including:

  • users and profiles

  • chats

  • chat members

  • messages

  • posts

  • comments

  • notifications

  • dating-related data

  • moderation

  • reports

The current design contains 23 models and a substantial number of indexes and unique constraints.

But the important part isn't simply having many indexes.

The important part is connecting an index to a real query.

For example:

Chat
  ↓
Messages
  ↓
createdAt

Chat history is commonly retrieved by chat and time, so the database has a composite index around:

(chatId, createdAt)

Other examples include indexes supporting:

Query pattern Index strategy
Chat history chatId + createdAt
Posts by author authorId + createdAt
Unread notifications userId + isRead
Notification history userId + createdAt
Comments for a post postId + createdAt
Chat membership chatId + userId

These aren't indexes added because "production applications need indexes."

They exist because the application repeatedly asks those questions.

That changed how I think about database optimization.

Don't start with "Which indexes should I add?" Start with "Which queries will this feature execute repeatedly?"


9. Cursor Pagination Was Another Small Problem With a Big Lesson

Imagine a chat has thousands of messages.

You don't want to load everything.

So you paginate.

A simple approach might use:

OFFSET 100
LIMIT 50

But as datasets grow, offset-based pagination becomes less attractive for continuously changing data.

Humble uses cursor-based pagination for message history.

The interesting detail is that the cursor isn't based only on the timestamp.

It uses:

createdAt + id

Why?

Because two messages can have the same timestamp.

If I only compare timestamps:

Message A → 10:30:01.123
Message B → 10:30:01.123

the pagination boundary can become ambiguous.

Adding the ID as a tie-breaker creates a stable ordering.

This was a good example of something I learned by actually building the feature:

Correctness often lives in the edge cases, not the happy path.


10. I Started Looking for N+1 Problems

Another lesson was learning not to blindly query the database inside loops.

For example, if a chat has many members, this pattern can become expensive:

Get members

for each member:
    query something

Instead, Humble batches related reads and reuses the results where possible.

The message API combines membership, member-list and chat-state checks, then reuses the member information for subsequent cache invalidation rather than querying the same information again.

The general lesson is simple:

A database query that looks cheap once can become expensive when multiplied by users, messages and requests.


11. Caching Isn't Just "Put Everything in Redis"

The caching layer evolved into several strategies.

For example:

GET recent messages
        │
        ▼
   Cache exists?
     /       \
   yes        no
   │           │
   ▼           ▼
return      PostgreSQL
              │
              ▼
            cache

For writes, the system can update relevant cached data instead of always throwing the entire cache away.

The cache layer also has TTLs for different categories of data, and Redis being unavailable should result in a database fallback rather than a completely broken feature.

This led me to a broader rule:

A cache should improve the system, not become the only thing keeping it alive.


12. Rate Limiting Needs More Than One Layer

Realtime applications are especially vulnerable to event spam.

Things like:

typing:start
chat:join
message events

can happen much more frequently than normal HTTP requests.

So Humble has rate limiting at both the application/API level and the realtime event level.

The important design decision is that the two layers have different priorities.

The HTTP layer protects operations that affect durable application state.

The realtime layer protects event-heavy operations.

The realtime layer can also fail open when Redis is unavailable because, in this case, preserving availability is more important than completely enforcing the realtime event limit during a dependency outage.

This is another lesson I didn't appreciate initially:

Security and reliability aren't always about choosing "deny." Sometimes the correct decision depends on what you're protecting.


13. Notifications Taught Me About Abstraction

As Humble grew, different features started needing notifications.

Likes.

Comments.

Chat events.

Group changes.

Dating events.

Administrative actions.

I didn't want every API route to construct notification records differently.

So I separated notification creation from notification presentation.

Conceptually:

Feature
   │
   ▼
Notification Service
   │
   ├── recipient
   ├── actor
   ├── notification type
   ├── entity
   └── metadata
        │
        ▼
     Database
        │
        ▼
   Realtime delivery

New notification types are added through a shared notification service and templates rather than duplicating notification logic throughout the application. The documentation also includes rules such as avoiding self-notifications, preserving entity references and handling notification failures without breaking the main operation.

This isn't a revolutionary architecture.

But that's exactly why I like it.

Good abstractions don't always look impressive.

Sometimes they're simply the thing that prevents the tenth feature from becoming harder than the first.


14. Database Changes Need Their Own Discipline

Another part of building Humble that changed my engineering habits was database migration safety.

I documented a simple rule:

The Prisma schema and migration history are the source of truth.

The idea is to avoid casually modifying production tables and instead make schema changes through migrations.

The project documentation explicitly separates development migration workflows from production deployment workflows and emphasizes preserving migration history rather than resetting databases when drift is detected.

This may sound boring compared with Socket.IO and Redis.

But production engineering has a lot of boring things that become extremely important when something goes wrong.


15. Designing for Horizontal Scaling Doesn't Mean I Have Scaled Horizontally

This is probably the most important disclaimer in this article.

I designed Humble's realtime architecture so that multiple Socket.IO instances can eventually work together through Redis and a load-balancing layer.

The documented architecture looks roughly like:

                 Load Balancer
                 /     |     \
                /      |      \
        Socket 1   Socket 2   Socket 3
                \      |      /
                     Redis
                       │
                   PostgreSQL

The load-balancer documentation describes multiple socket instances sharing Redis for cross-instance communication.

But the current committed deployment is still a single-instance deployment.

The horizontal architecture is designed and documented; it is not something I want to pretend has been proven by a production load test.

That distinction matters.

There is a huge difference between:

"I designed a system that can scale horizontally."

and:

"I have demonstrated this system handling a specific amount of concurrent production traffic."

I currently have stronger evidence for the first statement than the second.

And I think being honest about that is part of engineering maturity.


16. What I Got Wrong or Still Need to Improve

I don't want this article to become a "look how perfect my architecture is" post.

It isn't perfect.

There are real gaps.

Automated testing

This is probably the biggest one.

The architecture contains failure paths for:

  • Redis failure

  • Redis recovery

  • presence counting

  • rate limiting

  • realtime reconnection

  • degraded operation

But the project documentation still identifies automated tests as a gap.

That is something I want to improve.

If I design a recovery mechanism, I should eventually have a test that proves the recovery mechanism still works.


Production performance measurement

I have thought carefully about indexes, caching and architecture.

But design assumptions are not the same as measurements.

I still want stronger production-level visibility into things such as:

  • p95 and p99 API latency

  • slow database queries

  • cache hit rate

  • realtime connection behavior

  • message throughput

  • failure frequency

The current architecture exposes health and metrics information, but the documentation also identifies the lack of production query/APM measurements as a remaining gap.


Delivery guarantees

There is another interesting problem I haven't completely solved.

Suppose:

Database write succeeds
        ↓
Broadcast succeeds
        ↓
Client disconnects immediately

Did the client actually receive the event?

Not necessarily.

The current approach relies on the client re-syncing state from the API after reconnecting.

A future improvement could involve stronger delivery acknowledgements or sequence-based reconciliation.

That is an important distinction between:

"The server emitted an event."

and:

"The client has confirmed receiving and processing that event."

I don't want to pretend those are the same thing.


17. The Biggest Change Wasn't Technical

When I started, I mostly thought about:

Does the feature work?

Now I find myself asking:

What happens when it fails?

What happens when the dependency is unavailable?

What happens when the user has two tabs?

What happens when the database gets large?

What happens when the request is repeated?

What happens when the server restarts?

What happens when the network disappears?

What happens when this becomes ten times larger?

That shift in questions has probably been more valuable to me than learning any individual technology.

Redis wasn't the biggest lesson.

Socket.IO wasn't the biggest lesson.

PostgreSQL wasn't the biggest lesson.

The biggest lesson was learning to think in failure modes and trade-offs.


18. How I Documented the System

One thing I deliberately started doing while working on Humble was documenting the reasoning behind the implementation.

Not just:

Added Redis.

But:

Why do we need Redis?

What happens if Redis fails?

What state can safely disappear?

What state must survive?

How does the system recover?

What is currently deployed?

What is only planned?

What remains unfinished?

That resulted in separate documentation for the high-level architecture, low-level implementation, Redis failure handling, database migration safety, notifications, upgrade progress and load-balancing strategy.

The HLD explicitly distinguishes the current deployment from the planned horizontal-scaling architecture, while the LLD documents actual entities, indexes, state machines and algorithms from the codebase.

For me, documentation became part of the engineering process rather than something written after the engineering was finished.


19. The Way I Think About Engineering Now

If I had to reduce what I learned from Humble into a few principles, they would be:

Principle What it means to me
Persist before broadcasting Durable state shouldn't depend on realtime delivery
Dependencies should fail gracefully Redis going down shouldn't automatically mean the whole application goes down
Design around real queries Database indexes should follow access patterns
Presence is state, not a boolean Multiple tabs and devices change the problem
Realtime needs recovery Reconnection is part of the feature
Caches should be optional A cache should accelerate the system, not define its correctness
Rate limits need context Different operations deserve different protection
Architecture needs honesty A documented scaling plan isn't the same as proven scale
Documentation should explain why Future debugging becomes much easier
Unfinished work should be visible Knowing what isn't solved is part of knowing the system

20. What I Want to Build Next

The next stage isn't simply adding more infrastructure.

I want to make the existing engineering easier to prove.

That means:

  1. Add automated tests around Redis failure and recovery.

  2. Test reference-counted presence.

  3. Add stronger realtime delivery reconciliation.

  4. Run proper load tests instead of relying only on estimates.

  5. Measure database and API performance under realistic workloads.

  6. Improve production observability.

  7. Validate the documented multi-instance architecture in an actual environment.

The goal isn't to say:

"Humble can handle X users."

The goal is to be able to answer:

"Here is how we tested it, here is what happened, here is where it failed, and here is what we changed."

That is a much more useful engineering statement.


21. What Building Humble Means to Me as a Developer

I'm still learning.

I don't consider myself someone who has already solved distributed systems.

But building Humble has given me the opportunity to encounter many of the problems that make backend engineering interesting:

  • consistency

  • persistence

  • realtime delivery

  • caching

  • presence

  • rate limiting

  • database indexing

  • failure recovery

  • deployment

  • observability

  • scalability

  • technical debt

And the more I work on it, the more I realize that writing the code is only one part of the job.

The other part is being able to explain:

Why does this exist?

What happens when it fails?

What trade-off did I make?

What evidence do I have that it works?

What would I change if the system grew?

For me, that is what Humble has become—not just a campus product, but a practical place to learn how software behaves outside the happy path.


A simple mental model I now use

                ┌─────────────────────┐
                │      User need      │
                └──────────┬──────────┘
                           │
                           ▼
                    Build the feature
                           │
                           ▼
                    Make it reliable
                           │
                           ▼
                    Think about failure
                           │
                           ▼
                    Measure the result
                           │
                           ▼
                    Document the why
                           │
                           ▼
                    Improve and repeat

Good engineering isn't only making the happy path work. It's understanding what happens when the happy path stops working.

Technical stack

Layer Technology
Frontend Next.js, React, TypeScript
API / Application Next.js REST APIs
Realtime Socket.IO
Realtime server Node.js + Express
Database PostgreSQL
ORM Prisma
Cache / coordination Redis / Upstash
Reverse proxy Nginx
Process management PM2
Media Cloudinary
Email / OTP Resend / Nodemailer
Analytics PostHog

Humble is being built as a real product rather than only as a technical experiment, so the architecture continues to evolve alongside the product itself. (humbleapp.in)

Project: humbleapp.in

Built and documented by Prashant Kumar

Written by Prashant Kumar
Prashant Kumar Founder & Product Engineer

Founder of Enally. Product engineer building focused platforms for communities, architecture and campus life. Full-stack developer working across strategy, desi

Frequently asked questions

HumbleApp is a campus-focused platform designed for student conversations, communities, events, rides, and local discovery.

When scaling real‑time chat, developers often overlook issues such as multiple tabs per user, server restarts, Redis outage, failed broadcasts after message persistence, and the need for a separation between the realtime layer and the core business logic. These problems force a redesign that prioritizes fault tolerance, message durability, and layered architecture. [1]

Treating WebSocket/Socket‑IO behaviour as a dedicated layer decouples it from user authentication, data persistence, and business logic. This separation simplifies scaling, improves fault isolation, and allows independent deployment and monitoring of each component. It also makes the system resilient: a realtime server crash doesn't double‑catastrophize the whole app. [1]

The approach combines persistent storage (e.g., a database or a reliable queue) with acknowledgement semantics. Once a message is confirmed persisted, it is enqueued to a publish‑subscribe system (like Redis) that retries or flags failures. Client reconnection logic then requests missed messages, guaranteeing eventual consistency. [1]

Humble horizontally scales its Socket‑IO microservice behind a load balancer, using sticky sessions or a shared session store. Redis serves as an in‑memory Pub/Sub broker, and a separate message queue (e.g., RabbitMQ) can back messages that need guaranteed delivery. This setup lets additional nodes be spun up without affecting user sessions. [1]

Multiple tabs can cause duplicate events or stale state. Humble mitigates this by idempotent message handling, client‑side sequencing IDs, and merging updates in the UI. Server‑side, it uses a single user session per user ID and broadcast deduplication logic so that each event is delivered only once per intended recipient. [1]

HumbleApp uses Next.js, React, TypeScript, Node.js, Express, Socket.IO, PostgreSQL, Prisma, Redis, Nginx, and PM2.

Messages are first persisted through the REST API and PostgreSQL. The realtime layer then broadcasts the persisted message through Socket.IO to connected users.

REST APIs provide the durable source of truth for application data, while Socket.IO is used primarily for realtime delivery and communication with connected clients.

The realtime service is designed to gracefully fall back to in-memory operation so that core single-server realtime functionality can continue instead of the Redis failure bringing down the service.

Presence is reference-counted. A user remains online while they have active connections and is marked offline only after their last active connection closes.

The application uses PostgreSQL with Prisma and indexes based on important query patterns, including chat messages, posts, notifications, comments, and chat membership.

The realtime architecture is designed for horizontal scaling using multiple Socket.IO instances, Redis coordination, and a load-balancing layer. The documented architecture is a scaling design rather than a claim of benchmarked production capacity.

The client uses reconnection with exponential backoff and jitter, while the server provides health monitoring and graceful handling of dependency failures.

Building HumbleApp reinforced the importance of separating persistence from realtime delivery, designing for failure, understanding database access patterns, handling distributed state, documenting architectural decisions, and measuring systems rather than relying only on assumptions.

Keep learning

Related articles

Summarize with ChatGPT Claude Gemini Perplexity Grok