How Instagram Handles Millions of Likes?

How Instagram Handles Millions of Likes?

For a normal application, a like may look like a very simple operation:

User clicks Like

        ↓

API

        ↓

Database

        ↓

Like Saved

Simple, right?

But imagine a celebrity uploads a post and millions of users start liking it within a few seconds.

Now the problem becomes completely different.

If every like directly hits the database, we could end up with:

1 Like

   ↓

1 Database Request

1 Million Likes

   ↓

1 Million Database Requests

At this scale, the database can quickly become the bottleneck.

So how would we design a system that can handle this kind of traffic?

Let's understand it step by step.

1. First, Understand What Happens When You Like a Post

Let's start with the simplest flow.

A user clicks the Like button.

The application sends something like:

POST /posts/123/like

The request goes through the backend:

User

 ↓

API Gateway

 ↓

Like Service

 ↓

Database

The database stores the relationship:

user_id          |             post_id

101                  |             123

102                  |             123

103                  |             123


This works perfectly when traffic is small.

But now imagine:

Post 123

   ↓

10 users

   ↓

1,000 users

   ↓

100,000 users

   ↓

1,000,000 users

Suddenly, millions of requests are trying to update the same piece of information.

This is where system design becomes important.

2. Don't Make Every Like a Heavy Database Operation

One of the biggest mistakes would be treating every like as a synchronous database transaction.

Imagine:

User 1 → Database

User 2 → Database

User 3 → Database

User 4 → Database

User 1,000,000 → Database

The database now has to process an enormous number of operations.

Instead, we can separate the user's immediate action from the heavy background processing.

For example:

                User

                  ↓

              Like API

                  ↓

               Redis

                  ↓

              Response

                  ↓

                Kafka

                  ↓

          Background Workers

                  ↓

              Database

Now the API doesn't necessarily have to wait for every database operation to finish before responding.

This is where asynchronous processing becomes useful.

 3. Use a Message Queue for Massive Traffic

Let's say a post suddenly becomes viral.

Thousands or millions of users may click Like almost simultaneously.

Instead of processing everything immediately:

Like Request

     ↓

Database

we can use a message broker:

Like Request

     ↓

Like Service

     ↓

Kafka

     ↓

Consumers

     ↓

Database

A like event could look conceptually like:

{

  "userId": 101,

  "postId": 123,

  "action": "LIKE"

}

Kafka can act as a buffer between the API and the consumers.

So if traffic suddenly increases:

Normal Traffic

     ↓

Kafka

     ↓

Consumer

During a traffic spike:

Huge Traffic

     ↓

Kafka

     ↓

Consumer

     ↓

Consumer

     ↓

Consumer

We can add more consumers to process the events in parallel.

The important idea is:

Don't force the database to process every request at exactly the same moment.»

4. Store Likes Efficiently

Now let's think about the database.

We need to answer questions like:

- Did this user already like the post?

- How many likes does this post have?

- Which users liked this post?

A basic relational design could look like:

POST

----------------

id

caption

created_at

POST_LIKE

----------------

user_id

post_id

created_at

We can create a unique constraint:

(user_id, post_id)

This prevents the same user from liking the same post multiple times.

For example:

user_id | post_id

101        | 123

If user "101" tries to like post "123" again, the system can detect that the relationship already exists.

This is much safer than simply incrementing a counter every time.

5. Don't Count Millions of Rows Every Time

Imagine a post has:

50,000,000 likes

Now someone opens the post.

Would we run:

SELECT COUNT(*)

FROM post_like

WHERE post_id = 123;

for every request?

That can become expensive at huge scale.

Instead, we can maintain a counter:

Post

----------------

id: 123

like_count: 50000000

When a like is processed:

like_count = like_count + 1

But there's another problem.

If millions of users update the same counter simultaneously, we can create contention around that hot record.

This is known as a hot key / hot row problem.

So we need a better strategy.

 6. The Hot Post Problem

This is one of the most interesting parts of the design.

Imagine a normal post:

Post A

100 likes

No problem.

But a celebrity posts something:

Post B

10,000,000 likes

Now millions of requests are related to the same "post_id".

That post becomes a hot key.

Instead of having:

Post 123

like_count = 10,000,000

we can distribute the counter.

For example:

Post 123

Counter 1 → 2,500,000

Counter 2 → 2,500,000

Counter 3 → 2,500,000

Counter 4 → 2,500,000

Then:

Total Likes =

  Counter 1

+ Counter 2

+ Counter 3

+ Counter 4

This spreads the write load instead of forcing every request to update the same database row.

This technique is often called counter sharding or sharded counters.

7. Use Redis for Fast Reads

Now think about the Instagram feed.

Users don't just like posts.

They continuously open posts and feeds.

For every post, the application may need to display:

❤️ 2.4M likes

We don't necessarily want to query the database every time.

This is where a distributed cache such as Redis can help.

A simplified architecture:

                Request

                   ↓

                Redis

                 /      \

              Hit      Miss

               ↓         ↓

            Return Database

                       ↓

                     Redis

For example:

post:123:likes = 2400000

Now the application can retrieve the count quickly.

The database remains the source of durable data, while Redis can handle many frequent reads.

8. What Happens When a User Likes a Post?

Let's put the pieces together.

A simplified flow could be:

User

 ↓

Load Balancer

 ↓

API Gateway

 ↓

Like Service

 ↓

Redis / Fast Path

 ↓

Kafka

 ↓

Like Consumers

 ↓

Database

The user gets a quick response:

❤️ Liked

Meanwhile, background workers process the event.

This is a classic example of event-driven architecture.

The API handles the user interaction.

The event-processing system handles the work that doesn't need to block the user.

9. But What If the User Likes the Post Twice?

This is an important problem.

Imagine:

User clicks Like

      ↓

Request 1

User clicks Like again quickly

      ↓

Request 2

If both requests are processed independently, we might accidentally count:

1 User

   ↓

2 Likes

That's obviously wrong.

So the system needs idempotency.

The idea is simple:

Processing the same operation multiple times should not produce an incorrect final result.

For example, the database can enforce:

UNIQUE(user_id, post_id)

So only one like relationship can exist.

This protects the system even if duplicate requests arrive.

10. What About Unlike?

Now the user presses Unlike.

We have another event:

{

  "userId": 101,

  "postId": 123,

  "action": "UNLIKE"

}

The system processes it accordingly.

Conceptually:

LIKE

 ↓

Create relationship

 ↓

Increase counter

UNLIKE

 ↓

Remove relationship

 ↓

Decrease counter

But remember:

At large scale, we should carefully design how counters and events are updated so that duplicate or out-of-order events don't corrupt the final count.

This is one reason event processing often needs:

- idempotency

- ordering considerations

- retries

- dead-letter handling

- reconciliation jobs

11. What If Kafka or a Consumer Goes Down?

Distributed systems fail.

We shouldn't design assuming:

“Everything will always work.”

Suppose:

Like API

   ↓

Kafka

   ↓

Consumer

The consumer crashes.

Does the like disappear?

It shouldn't.

The event can remain in Kafka until a consumer successfully processes it, depending on the configured delivery and retention strategy.

When the consumer comes back:

Kafka

 ↓

Consumer

 ↓

Process pending events

This gives the system much better resilience than directly coupling every request to immediate database processing.

12. Scale the Like Service Horizontally

One Like Service instance won't be enough for massive traffic.

So we run multiple instances:

                                   Load Balancer

                                          ↓

          ┌────────────┼────────────┐

          ↓                                 ↓                                    ↓

     Like Service              Like Service                 Like Service       

    Instance 1              Instance 2                Instance 3

If traffic increases:

3 instances

     ↓

10 instances

     ↓

50 instances

This is horizontal scaling.

The service itself should ideally be stateless, so any instance can process a request.

13. Authentication Is Also Important

Before processing a Like request, the system needs to know:

Who is sending this request?

A typical request might contain an authentication token.

For example:

User

 ↓

API Gateway

 ↓

Authentication

 ↓

Like Service

The Like Service can then determine:

userId = 101

postId = 123

and create the relationship.

Authentication also prevents users from simply sending arbitrary requests pretending to be someone else.

14. Monitoring Becomes Critical at This Scale

Imagine you're handling millions of likes.

Something goes wrong.

How do you know?

You need metrics such as:

Likes / Second

API Latency

Error Rate

Kafka Consumer Lag

Redis Hit Rate

Database Load

Queue Size

Failed Events

For example:

Kafka Consumer Lag

        ↓

    Increasing

        ↓

Consumers are too slow

        ↓

Add more consumers

Monitoring isn't just about looking at graphs.

It helps engineers understand where the system is becoming a bottleneck.

15. The Complete Architecture

Now let's combine everything.

A simplified Instagram-scale Like System could look like this:

                         USERS

                           ↓

                    Load Balancer

                           ↓

                      API Gateway

                           ↓

                     Like Service

                    /               \

                   ↓              ↓

                Redis           Kafka

                                      ↓

             ┌───────┼───────┐

             ↓                           ↓                 ↓

             Consumer         Consumer       Consumer

              ↓                 ↓                  ↓

              └───────┼───────┘

                                      ↓

                              Database

For the like count:

User

 ↓

Like API

 ↓

Counter / Redis

 ↓

Background Processing

 ↓

Durable Storage

And for a viral post:

Millions of Likes

       ↓

Load Balancer

       ↓

Multiple Like Service Instances

       ↓

Kafka

       ↓

Multiple Consumers

       ↓

Distributed / Sharded Storage

Now the workload is distributed across multiple components instead of hitting one database directly.

So, How Does Instagram Handle Millions of Likes?

The important thing is that there is no single magic technology.

You don't solve millions of likes by simply adding Redis.

You don't solve it by simply adding Kafka.

You don't solve it by simply adding more servers.

The solution comes from combining multiple concepts:

Horizontal Scaling

        +

Caching

        +

Message Queues

        +

Asynchronous Processing

        +

Database Optimization

        +

Counter Sharding

        +

Idempotency

        +

Load Balancing

        +

Monitoring

Each component solves a different problem.

Load Balancer

Distributes incoming traffic.

API Gateway

Provides a controlled entry point to backend services.

Like Service

Handles the business logic for likes.

Redis

Provides fast access to frequently requested data.

Kafka

Buffers and distributes high-volume events.

Consumers

Process events asynchronously.

Database

Stores durable like relationships.

Sharded Counters

Help distribute updates for extremely popular posts.

Monitoring

Helps engineers detect bottlenecks and failures.

💡 The Biggest Lesson

When you're designing a system for millions of users, don't ask:

“How can I make my database handle millions of requests?"

Ask:

“How can I prevent millions of requests from reaching my database in the first place?"

That's the mindset behind scalable system design.

Instead of:

1 Million Requests

        ↓

   1 Database

we build:

                 1 Million Requests

                         ↓

                  Load Balancer

                         ↓

                 Multiple Services

                    /                \

                   ↓                ↓

                Redis            Kafka

                                       ↓

                                 Consumers

                                        ↓

                                    Database

The goal isn't to make one component infinitely powerful.

The goal is to distribute the workload across the system.