DEV Community

Cover image for I Almost Gave Up on QuizRank Because I Thought Render Was Too Small
Gideon Adeti
Gideon Adeti

Posted on

I Almost Gave Up on QuizRank Because I Thought Render Was Too Small

QuizRank has been one of those projects where I've learned almost as much from things going wrong as I have from actually building features.

Recently, I went through a whole deployment journey that started with me thinking:

"Maybe Render's free tier just isn't enough for this app."

Turns out, I was partly the problem. 😂

It started with Render

At some point, I couldn't get the QuizRank backend deployed on Render.

The build itself worked, but the application would run out of memory while starting. Render's instance has a 512 MB memory limit, and I was seeing OOM errors.

So naturally, I assumed the backend had simply become too large for Render's free tier.

I also had another problem: I wasn't even starting the production application correctly. I was using nest start instead of running the compiled production build.

There was also a NODE_OPTIONS configuration that wasn't suitable for both building and running the application. Eventually, I separated the memory requirements: more heap for the build and a much smaller limit for the running application.

Once we actually investigated what was happening instead of assuming Render was the problem, it became clear:

QuizRank could run on Render. I was just doing it wrong.

The detour

Before that realization, I had already moved the backend around quite a bit.

I self-hosted it with Coolify and got it running successfully for more than two months.

The problem was that my free trial eventually ran out, and I couldn't afford the subscription, so the QuizRank backend was suspended.

After that, I self-hosted the backend on my own machine for about two weeks.

Eventually, I gave up on that too.

Not because it didn't work, but because hardly anyone was using QuizRank at the time, and keeping a backend running on my own machine was consuming resources for very little benefit.

That's when I started thinking about simplifying the project and stripping out some features so I could get it back onto Render.

And that's when we discovered the actual problem.

The leaderboard was the bigger problem

Getting the backend onto Render solved the deployment problem, but then I looked more closely at how the leaderboard worked.

Every time someone submitted a quiz, the backend was rebuilding the entire leaderboard.

That meant:

  • Reading the entire Redis sorted set
  • Querying the database for all those users
  • Rebuilding the leaderboard array
  • Caching the entire thing
  • Broadcasting the entire leaderboard through Socket.IO

And this happened for every submission.

That might be perfectly fine when there are 20 users.

It becomes a very different story when there are thousands.

So instead of stripping features from QuizRank, I decided to fix the architecture.

Making the leaderboard scalable

The first change was simple:

Stop reading the entire leaderboard.

The UI only displays a limited number of rows at a time, so there's no reason for the backend to constantly materialize thousands of users.

The leaderboard is now bounded to the top 100.

Instead of:

ZREVRANGE 0 -1
Enter fullscreen mode Exit fullscreen mode

we effectively do:

ZREVRANGE 0 99
Enter fullscreen mode Exit fullscreen mode

The database queries are bounded too.

But there was another problem.

Even if rebuilding the top 100 is cheap, rebuilding it after every single submission still isn't ideal.

So I added a small debounce mechanism using Redis and BullMQ.

Multiple submissions arriving within a short window can now share a single leaderboard rebuild.

The result is roughly:

100 submissions
      ↓
100 score updates
      ↓
~1 leaderboard rebuild
      ↓
top 100 broadcast
Enter fullscreen mode Exit fullscreen mode

rather than:

100 submissions
      ↓
100 leaderboard rebuilds
Enter fullscreen mode Exit fullscreen mode

The leaderboard can now be a few seconds behind during bursts, which is a trade-off I'm completely comfortable with.

It's a live leaderboard, not a stock exchange. 😅

But what about users outside the top 100?

That introduced another interesting problem.

If the API only returns the top 100, what happens to someone ranked 847th?

They still need to know their own rank.

So the API now returns the top 100 plus the authenticated user's own leaderboard position when they're outside the top 100.

That means the shared leaderboard stays bounded while users can still see:

  • Their score
  • Their rank
  • Their relevant statistics

without forcing the backend to build the entire leaderboard for everyone.

Keeping Render awake

There was one more issue with using Render's free tier: the service can sleep after inactivity.

For QuizRank, I don't really want someone visiting the site after a period of inactivity and waiting for the backend to wake up.

So I added an external uptime monitor using UptimeRobot.

Instead of having QuizRank constantly ping itself, the monitor periodically hits the backend's lightweight liveness endpoint.

Simple, cheap, and good enough for this stage of the project.

And then Vercel decided to join the story

After finally getting the backend deployment sorted out, I ran into another deployment problem.

This time it was the frontend.

Vercel blocked the deployment because the GitHub commit author wasn't recognized as a member of the Vercel team.

For a while I was confused because I was already logged into the correct Vercel account.

Eventually I found the actual problem:

My GitHub account was connected to the wrong Vercel account.

I disconnected the GitHub integration, connected it to the correct account, and everything worked.

So after all that:

Render wasn't actually the problem.

Vercel wasn't actually the problem either.

Apparently, I was. 😂

What I learned

The biggest lesson from all of this isn't really about Render, Coolify, Redis, BullMQ, or Vercel.

It's this:

Don't assume your infrastructure is the bottleneck before investigating your application.

When Render failed, I initially thought:

"The free tier isn't powerful enough."

But the real issues included:

  • Running the wrong production command
  • Incorrect memory configuration
  • Rebuilding an entire leaderboard on every submission
  • Sending unnecessarily large Socket.IO payloads
  • Doing unbounded Redis and database work

None of those required me to remove features.

They required me to understand what the application was actually doing.

And that's probably the biggest thing I'm taking away from this phase of building QuizRank.

You don't always need more infrastructure.

Sometimes you need less work.

Top comments (1)

Collapse
 
uptimerobot profile image
UptimeRobot

Thanks for mentioning us!

We're actually doing a spotlight series, where we highlight members of our community: if you built something in the past and used UptimeRobot, we would love to have a short written interview with you.

I can share with you more info. Let us know if you're interested!