Archilo: helping architecture work become visible.

Explore the ecosystem →

Updates & announcements

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
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.

System Design

800 Users Exposed Performance Problems I Didn’t Know My App Had - Here’s Everything I Found

I scaled Archilo past 800 users and discovered hidden performance issues. Here’s how I found and fixed N+1 queries, database bottlenecks, indexes, and more.

800 Users Exposed Performance Problems I Didn’t Know My App Had - Here’s Everything I Found
What you'll learn

I scaled Archilo past 800 users and discovered hidden performance issues. Here’s how I found and fixed N+1 queries, database bottlenecks, indexes, and more.

Jump to the guide

800 Users Exposed Performance Problems I Didn’t Know My App Had - Here’s Everything I Found

Building an application is one thing.

Building an application that still feels fast when real users start using it is a completely different problem.

When I started building Archilo, I was mostly focused on features, UI, authentication, uploads, search, feeds, profiles, comments, likes, saves, and everything else a growing product needs.

At first, everything felt fine.

Then Archilo started getting real usage.

Around 800 users, I began noticing something that every developer eventually encounters:

The application worked. It just didn't feel fast anymore.

Pages were taking longer to load. Some interactions felt delayed. The feed was doing far more work than it should. And the interesting part was that there wasn't one obvious bug causing everything.

So I stopped guessing and started investigating.

What I found was a collection of small problems that became a big problem when combined.

This is how I found them and what I changed.


What is Archilo?

For context, Archilo is an architecture platform built for discovering, sharing, and exploring architectural work, research, and creative projects.

Users can publish content, explore designs, interact with posts, like and save content, comment, search, manage profiles, and more.

The application is built around a modern web stack including:

  • Next.js

  • React

  • Prisma

  • PostgreSQL

  • Neon

  • Firebase authentication

  • S3-based asset storage

So when performance started becoming an issue, I had quite a few places to investigate.

Frontend?

API?

Database?

Images?

Authentication?

Network?

Turns out, the answer was:

A little bit of everything.

But the database and query patterns were some of the biggest offenders.


The First Mistake: Looking for One Big Problem

My first instinct was the same instinct many developers have:

"There must be one really slow query."

So I started looking at individual API routes.

But that wasn't the real problem.

The problem was how many things the application was asking the database to do.

One feed request could trigger additional requests for things like:

  • Whether the user liked a post

  • Whether the user saved a post

  • Assets and images

  • Mentioned users

  • Related content

  • Comments

  • Counts

Individually, each query looked reasonable.

Together?

Not so reasonable.


The N+1 Problem Was Hiding in Plain Sight

Imagine the feed contains 20 posts.

A naive implementation might do this:

Get 20 posts
    ↓
Check like status for post 1
Check like status for post 2
Check like status for post 3
...
Check like status for post 20

And then potentially repeat the same pattern for saves and assets.

Suddenly, a page that looks like one request from the user's perspective can turn into dozens of database operations.

That's the infamous N+1 query problem.

And it gets worse as the number of cards increases.

The fix

Instead of asking:

"Did I like this post?"

20 separate times, I changed the approach to:

"Give me all liked posts for this user from these 20 post IDs."

Conceptually:

SELECT post_id
FROM post_likes
WHERE user_id = ?
AND post_id IN (...);

One query.

The same idea was applied to saves and other repeated lookups.

The feed API now folds viewerLiked and viewerSaved into the response itself instead of making every card independently figure it out.

That was one of the biggest wins.


Then I Found Another Problem: We Were Fetching Too Much Data

This one is surprisingly common.

The UI might need:

title
thumbnail
author
likes
date

But the database could be returning:

title
thumbnail
author
likes
date
description
attachments
collaborators
metadata
external links
technologies
project information
...

Why?

Because it was convenient.

But convenience has a cost.

The database has to retrieve it.

Prisma has to process it.

The server has to serialize it.

The network has to transfer it.

And the browser has to receive it.

So I started adding explicit Prisma select statements to list queries.

Instead of:

findMany({
  include: ...
})

the query became much more intentional:

findMany({
  select: {
    id: true,
    title: true,
    ...
  }
})

For example, the Designs list stopped fetching the large TipTap description field and many other columns that the card never needed.

This led to a simple rule I now follow:

List pages should be thin. Detail pages can be fat.

Your card doesn't need the entire article.


Then Came the Database Indexes

At this point I had reduced the number of queries.

But there was another question:

Are the remaining queries easy for PostgreSQL to find?

This is where indexes came in.

Think of a database index like the index at the back of a textbook.

Without it, you might have to search through hundreds of pages.

With it, you jump much closer to the answer.

For Archilo, I added indexes around the queries we actually use.

For example, content listing queries commonly filter around things like:

status
isPublic
isActive
isArchived
publishedAt

So I added a composite index around those fields.

I also added:

(userId, createdAt)

indexes for activity-related tables such as likes, saves, and shares.

These indexes were applied to the live Neon database.

The important lesson here is:

Don't add indexes because a blog post told you indexes are good. Add indexes because your queries need them.


Search Had Its Own Problem

Archilo also has search.

And search often uses queries conceptually similar to:

WHERE title ILIKE '%architecture%'

Traditional B-tree indexes aren't designed to handle arbitrary substring searches particularly well.

So I added PostgreSQL pg_trgm GIN indexes for the fields being searched heavily:

posts.title
posts.category

designs.title
designs.category

research_papers.title
research_papers.category

users.name
users.username

 

This is one of those database features you don't necessarily need when you're starting out.

But as your dataset grows, the difference becomes increasingly important.


Tags and Keywords Were Another Story

Archilo also stores things like tags and research keywords in arrays.

Queries such as:

tags.has
tags.hasSome
keywords.has
keywords.hasSome

can become expensive when the dataset gets larger.

So I added PostgreSQL GIN indexes to the relevant array columns:

posts.tags
designs.tags
research_papers.keywords

Again, the important part wasn't simply "add an index."

It was:

Look at what the application actually asks the database. Then design the database around those questions.


We Were Also Counting Things We Didn't Need to Count

Another interesting optimization happened in feed pagination.

For an infinite feed, I don't necessarily need to know:

"There are exactly 13,482 matching posts."

I only need to know:

"Are there more posts?"

So instead of running an expensive COUNT(*) for every infinite-feed request, the API fetches one extra record.

If I want 20:

Fetch 21

If I receive 21:

20 displayed
1 hidden
hasMore = true

If I receive only 20:

hasMore = false

Simple.

And much more appropriate for an infinite scroll experience.


50 Posts Became 20

This was another small change with a surprisingly large impact.

The feed originally loaded up to 50 posts on the first request.

I reduced that to 20.

Why make the browser process 50 cards when the user can only see a handful of them initially?

Every additional item can mean:

  • Database work

  • JSON serialization

  • Network transfer

  • React rendering

  • Image loading

  • Asset processing

So instead of:

50 posts

the initial request now starts with:

20 posts

and loads more as the user continues scrolling.


Fewer Queries Isn't Enough

There was another pattern I found.

Some queries were independent but were being executed one after another.

Something like:

Query A
   ↓
wait
   ↓
Query B
   ↓
wait
   ↓
Query C

If A takes 50ms, B takes 40ms, and C takes 30ms, the total waiting time can approach 120ms.

If they don't depend on each other, why wait?

So I changed appropriate server-side operations to run concurrently:

await Promise.all([
  queryA(),
  queryB(),
  queryC()
]);

Now the request can move closer to the duration of the slowest operation instead of the sum of all three.

This was applied to several API routes, including the posts API and post detail API.


And Then There Was the Connection Pool

This was another piece of the puzzle.

The application initially had a database connection pool of only 5 connections.

Once the query storms were fixed, I increased the pool to 12 while using Neon's pooled/PgBouncer endpoint.

But notice the order:

I didn't solve N+1 by throwing more database connections at it.

That would be like fixing a traffic jam by building more parking spaces while leaving the road blocked.

First:

Reduce unnecessary queries
        ↓
Batch queries
        ↓
Select only required data
        ↓
Add appropriate indexes
        ↓
Parallelize independent work
        ↓
Then increase pool capacity

That's a much healthier approach.


The Bigger Lesson

The most important thing I learned wasn't about PostgreSQL.

It was about thinking in systems.

A page can feel slow even when no individual query looks terrible.

For example:

1 reasonable query
+
20 unnecessary queries
+
20 more asset lookups
+
large database payload
+
large images
+
serial server operations
+
connection pool waiting

None of those problems necessarily screams:

"I AM THE PERFORMANCE BUG."

Together, they absolutely do.


What Changed in Archilo?

The optimization ended up covering several layers:

Database

  • Composite indexes

  • Trigram GIN indexes

  • Array GIN indexes

  • Better query patterns

API

  • N+1 query elimination

  • Batched database lookups

  • Explicit select

  • Parallel queries

  • Pagination improvements

  • Bounded request parameters

Frontend

  • Fewer redundant requests

  • Viewer state included in list responses

  • Smaller initial feed

  • Better React Query invalidation

Infrastructure

  • Increased connection pool

  • Neon connection pooling

  • Background processing for non-critical work

And importantly, these weren't theoretical optimizations.

They came from looking at what Archilo was actually doing in production.


The Takeaway for Developers

If your application has 10 users, you can get away with a lot.

At 100 users, you start noticing some cracks.

At 800 users, those cracks can start introducing themselves properly.

And eventually, your application tells you exactly where you cut corners.

So if your application is becoming slow, don't immediately add Redis, a bigger server, or a new database.

First ask:

How many queries are happening?

Which queries are repeated?

Am I fetching data I don't use?

Does my database have indexes matching my real queries?

Can independent operations run in parallel?

Do I really need that COUNT(*)?

Am I loading 50 things when I only need 20?

Because sometimes the fastest database query isn't a faster query.

It's the query your application no longer needs to make.

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

The author noticed a slowdown when Archilo reached around 800 users, at which point the app “just didn’t feel fast anymore.” (Enally blog, 800 Users Exposed Performance Problems)

Pages took longer to load, interactions felt delayed, and the feed performed more work than necessary—classic signs of performance bottlenecks. (Enally blog)

Archilo is built with Next.js, React, Prisma, PostgreSQL on Neon, Firebase authentication, and S3‑based asset storage. (Enally blog)

Instead of guessing, the author systematically investigated each layer—frontend, backend, database, and assets—looking for small inefficiencies that compounded at scale. (Enally blog)

Use incremental profiling, optimize database queries, cache heavy data, paginate feeds, and monitor real‑user metrics to catch slowdowns early. (Enally blog)

Keep learning

Related articles

Summarize with ChatGPT Claude Gemini Perplexity Grok