DEV Community

Chandrabhan Shekhawat
Chandrabhan Shekhawat

Posted on

The Django Mistake That Made My API 20 Slower (And How I Fixed It)

If you’ve been working with Django for a while, you’ve probably experienced this.

Your API works perfectly in development.

You test it with a few records.
Everything feels fast.

Then one day production data arrives.

Suddenly an endpoint that used to respond in 300ms now takes 6–10 seconds.

That happened to me.

The Problem

I had an API returning a list of companies along with their owners and projects.

The code looked completely normal.

companies = Company.objects.all()
data = []
for company in companies:
data.append({
"name": company.name,
"owner": company.owner.name,
"projects": [project.name for project in company.projects.all()]
})

Nothing looked suspicious.

Yet the endpoint became painfully slow as the database grew.

What Was Actually Happening?

Every time the loop executed, Django performed additional database queries.

One query fetched the companies.

Another query fetched the owner.

Another query fetched the projects.

If there were 500 companies, Django wasn’t executing 3 queries.

It was executing more than 1,000 queries.

This is known as the N+1 Query Problem.

Many developers unknowingly ship this to production because everything appears fast with a small development database.

How I Found It

The first thing I checked wasn’t the Python code.

It was the SQL queries.

Using Django Debug Toolbar, I discovered the API was generating hundreds of nearly identical SQL statements.

Instead of asking the database once, Django kept asking the same question repeatedly.

The Fix

The solution was surprisingly simple.

Instead of letting Django fetch related objects one by one, I told it to fetch everything in advance.

companies = (
Company.objects
.select_related("owner")
.prefetch_related("projects")
)

Now the rest of the code stayed exactly the same.

No major refactoring.

No caching.

No Redis.

Just better ORM usage.

Why It Works

select_related() performs SQL joins for ForeignKey and OneToOne relationships.

prefetch_related() performs efficient separate queries for Many-to-Many and reverse relationships, then joins the data in Python.

The result?

Instead of executing hundreds of queries, Django executes only a handful.

The Result

Before:

  • 1,043 SQL queries
  • Response time: 8.4 seconds

After:

  • 3 SQL queries
  • Response time: 320ms

That’s more than a 20× improvement without changing the database schema or adding any caching.

When Should You Use It?

Use select_related() for:

  • ForeignKey
  • OneToOneField

Use prefetch_related() for:

  • ManyToManyField
  • Reverse ForeignKey relationships

Choosing the right method can dramatically reduce database traffic.

Final Thoughts

One of the biggest lessons I’ve learned after years of building Django applications is this:

Performance problems often aren’t caused by slow databases.

They’re caused by asking the database the same question hundreds of times.

Before introducing caching, Redis, Celery, or expensive infrastructure, inspect your SQL queries.

Sometimes the biggest performance improvement comes from changing a single line of Django ORM code.

Have you ever encountered an N+1 query problem in Django? I’d love to hear how you discovered it and what you did to fix it.

Top comments (0)