November 1, 2026 · 13 min read
How LinkedIn Shows Your Impressions Live: The System Design, Rebuilt and Measured
Someone views your LinkedIn post, and on your next refresh it is already in your impressions. How does that work, live, for millions of posts? Nothing is counted when you refresh. I rebuilt the pattern LinkedIn's engineers have described (events, a stream, a running count, a fast store) on Azure and measured it: a view reaches the count in under a second at 5,000 views a second, a plain database counter stalls at about 1,100 updates a second on popular posts, counting unique people needs an approximate counter, and the stream keeps every view through an outage. Plus the silent failure that cost me an hour.
One of my recent posts got far more impressions than I expected, and every time I refreshed, the number had moved. Someone views your post, and seconds later it's in your count. How does LinkedIn do that live, for millions of posts at once? The answer is a design where views are counted as they arrive, so a refresh only has to ask for the result. The same pattern runs like counts, live dashboards, leaderboards and fraud checks across a lot of software. I rebuilt it on Azure, following what LinkedIn's engineers have published, and measured how fast it is, where the obvious alternative breaks, and what happens when part of it goes down.
You don't need to know Kafka or stream processing. If you've used a database, you can follow this. Every section ends with a one-line Remember, and the whole article fits in 10 lines at the end.
- Part 1 · The design: what happens to one view, LinkedIn's version, and the Premium question, sections 1–3.
- Part 2 · What I measured: speed, the database counter, unique people, an outage and two silent failures, sections 4–9.
- Part 3 · Using it: where else it runs and a checklist for building one, sections 10–11.
Three things people often assume, and what's true:
| What people assume | What's true |
|---|---|
| "Refresh counts my views." | Views are counted as they arrive. Refresh asks for the result. |
| "Just add 1 to a counter in the database." | On popular posts, that stalled at about 1,100 updates a second on my test database. Inserting one row per view did 14,677. |
| "Premium switches on view tracking." | Every member sees post impressions. Premium unlocks the full profile-viewer list, which appears the moment you upgrade. |
Part 1
The design
One view, from someone's screen to your number, live.
1. What happens when someone sees your post
Four steps, and the order is the whole idea.
- Something happens. A member's app sends a tiny message: this post, this viewer, this time. That message is an event.
- The event joins a stream. A stream is an ordered log of events that keeps them for hours or days, whether or not anything has read them yet.
- A processor keeps a running count. As each event arrives, it adds 1 to that post's total.
- A fast store holds the totals. When you open your analytics, the page asks the store for a number that's already been worked out.
You hit refresh. The counting already happened as views arrived. The page just asks for the result.
The work happens on the way in, once per view. The read is cheap, so it doesn't matter whether one person or a million people are refreshing.
One detail makes this work at scale: the stream is split into partitions, and every event for the same post goes to the same partition. Many machines can count in parallel, each owning its own posts, and no two of them fight over the same total. My setup used 8 partitions and keyed every event by post id.
2. LinkedIn's version
In November 2016, LinkedIn's engineers described how post analytics, the "who's viewed your post" numbers, are built (LinkedIn Engineering). In their words:
- "an impression event gets fired from the client based on member action", and it travels through Kafka.
- A Samza job consumes "ImpressionEvents, LikeEvents, CommentEvents, ShareEvents" and does the "aggregation and decoration before making the data available for clients to consume".
- The output goes to "the data store", which a service queries "based on various facets and filters".
That's the same four steps. The post doesn't name the data store. The other two parts are open source and well documented:
- Kafka started at LinkedIn (Wikipedia), which still uses it "for activity stream data and operational metrics" (Apache Kafka). In 2015 LinkedIn wrote that it handled more than 1 trillion messages a day, with peaks of 4.5 million a second (LinkedIn Engineering).
- Samza is LinkedIn's stream processor. LinkedIn uses it to join and aggregate event streams as they arrive, for example joining ad clicks with ad impressions (LinkedIn Engineering).
For profile views, LinkedIn wrote in 2014 that Pinot, its real-time analytics store, powers Who Viewed Your Profile, answering in tens of milliseconds (LinkedIn Engineering).
These posts are from 2014 to 2016, and LinkedIn's setup today isn't public, so I don't claim to know it. What's public is the design, and the design is what I rebuilt.
3. And Premium?
Premium doesn't switch counting on.
- Every member can see impressions and other analytics for their own posts (LinkedIn Help).
- Premium unlocks the full list of people who viewed your profile, up to 365 days back. Basic accounts see the three most recent viewers from the last 90 days (LinkedIn Help).
The giveaway is what happens when someone upgrades: months of profile viewers appear straight away. That history can only exist if it was recorded all along. Premium changes what you can see, not what gets counted. That last step is my reasoning from how the feature behaves; LinkedIn doesn't say it in these words.
Part 2
What I measured
The same pattern on Azure, pushed and broken on purpose.
4. The setup
| Step | My version | |
|---|---|---|
| Stream | Kafka | Azure Event Hubs, which speaks the Kafka protocol (8 partitions, kept 24 hours) |
| Running count | Samza | A materialized view: the store adds each new view to a per-post total as it arrives |
| Data store | not named | Azure Data Explorer, Microsoft's real-time analytics database (smallest dev size, streaming ingestion) |
A small script on my laptop sent fake views for 10,000 posts over the Kafka protocol. Like a real feed, a few posts got most of the views. Everything ran in Azure's Central India region on 31 October 2026.
5. Under a second, at 50 times the traffic
I sent views at three speeds for 90 seconds each. Every few seconds I also sent one view to a brand-new post and kept refreshing its count until it showed 1, timing it on my laptop.
| Views a second | Views sent | Views counted | View → count (median) | Slowest 5% |
|---|---|---|---|---|
| 100 | 8,670 | 8,670 | 0.73 s | 1.16 s |
| 1,000 | 86,700 | 86,700 | 0.73 s | 0.75 s |
| 5,000 | 432,500 | 432,500 | 0.76 s | 0.87 s |
When I ran it, every one of the 527,870 views was counted, and the time to see a new view didn't change between 100 and 5,000 a second. One refresh took about 0.2 seconds, most of it network time between my laptop in India and the data centre. The "view → count" time also includes my own polling gaps, so the real figure is a little lower.
6. Why not just add 1 in the database?
It's the first thing most people would build: one row per post, and UPDATE posts SET view_count = view_count + 1 for every view. I tested it on a 2-vCPU PostgreSQL 16 server, with the load sent from a machine in the same region using pgbench, PostgreSQL's own benchmark tool, for 60 seconds per point.
| 64 connections | per second | median | slowest 1% |
|---|---|---|---|
| +1 on a counter row, popular posts | 1,092 | 3.2 ms | 1044 ms |
| Insert one row per view (events) | 14,677 | 3.2 ms | 35 ms |
| +1, views spread evenly (unrealistic) | 15,162 | 3.6 ms | 11 ms |
Why the red line goes flat: when a row is updated, PostgreSQL locks it until the transaction finishes. Every view of a popular post needs the same row, so the updates queue up behind each other. More connections just make a longer queue. With the views spread evenly across posts, the same update scales fine, which shows the problem is the hot row, not the database. Inserting one row per view never fights over a row, so it scales like the even case even when traffic is lopsided. That's the event approach.
When I ran it, the +1 counter on popular posts managed 1,092 updates a second at 64 connections, and the slowest 1% took 1,044 ms. Inserts did 14,677 a second, with the slowest 1% at 35 ms.
7. Impressions vs members reached
LinkedIn shows both, and they're different problems. Impressions count every view. "Members reached" counts people, so the system has to remember who it has already counted.
Remembering every viewer of every post gets expensive. At scale, systems use an approximate distinct count, often HyperLogLog: instead of storing every name, it keeps a small sketch that estimates how many different names went in. Azure Data Explorer's dcount() works this way.
I sent 60 seconds of views at 5,000 a second from a pool of 200,000 members, where some members scroll far more than others.
| Value | |
|---|---|
| Impressions | 282,000 |
| Unique members, exact | 125,203 |
| Unique members, approximate (default accuracy) | 125,326 (0.1% off) |
| Worst single post, default accuracy | 1.04% off |
| Worst single post, highest accuracy | 0.22% off |
At this size, the exact and approximate queries took about the same time (around half a second). The approximate counter pays off at much larger scale, where storing every viewer per post stops fitting in memory, and I didn't test that scale.
8. When the counter goes down
I switched off the store, the part that counts, and kept sending views for three minutes at 1,000 a second. Then I switched it back on.
The 167,100 views sent while the store was off waited in the stream. The first time the restarted store answered a query, 91% were already counted; the rest arrived by about five minutes. When I ran it, none were lost and none were counted twice.
This is the reason a stream sits in the middle. The thing that produces events and the thing that counts them don't have to be up at the same time. As long as the outage is shorter than the stream keeps events (24 hours in my setup), nothing is lost.
One caution: streams usually promise to deliver every event at least once, not exactly once. My test saw no duplicates, but a real system should be able to spot a repeated event, for example by its id.
9. Two silent failures
Both cost me time, and neither raised an error.
Compression. I set the sender to compress batches with lz4. Event Hubs' Kafka endpoint only supports gzip (Microsoft Learn). The sender got no error and Event Hubs accepted the batches, but not one view reached the store. Turning compression off fixed it: 7,700 sent, 7,700 counted.
A brand-new connection. Right after I connected the store to the stream, the first test views took several minutes to appear instead of seconds, while the connection switched to streaming. None were lost; later views arrived in seconds.
The lesson is the same for both: check that what you sent equals what got counted. That one comparison caught both.
Part 3
Using it
Where else it runs, and what to check if you build one.
10. The same pattern everywhere
Swap "view" for any event and the four steps stay the same:
- Likes, views and play counts on posts, videos and songs.
- Live analytics dashboards: sales by the minute, sign-ups, errors.
- Leaderboards in games and contests.
- Fraud and rate-limit checks: count recent attempts per card or per user and act within seconds.
- Order and delivery tracking: every status change is an event.
- Microservices: instead of calling each other directly, services publish events to a stream, and each one reads what it needs. The orders service doesn't have to know the email service exists.
11. If you build one
- Count on the way in, not when someone asks.
- Never make every event update one row. Append events and aggregate them, or split a hot counter across several rows.
- Keep events longer than your longest outage. Retention is your safety margin.
- Expect duplicates. Give each event an id so a repeat can be ignored.
- Estimate unique counts at scale, and say that they're estimates.
- Watch the gap between events produced and events counted. A growing gap means the counter is falling behind.
- Check the boring settings: supported compression codecs, how long a new connection takes to stream, retention.
- Reconcile: compare sent with counted on a schedule.
Everything I ran is in the lab folder: producer.py (the views), measure.py (view → count), unique.py, recovery.py, setup.kql (the store), the PostgreSQL scripts (hot.sql, append.sql, spread.sql, bench.sh) and every result file (analysis.json). The Azure resources ran for a few hours and are deleted.
Limits of my tests
- Small scale. My top speed was about 4,800 views a second on the smallest Azure Data Explorer cluster. LinkedIn wrote about peaks of 4.5 million messages a second in 2015.
- One run per setting. The numbers were steady across speeds, but each point is one run.
- Timing from my laptop. "View → count" includes network time to Central India and my polling gaps.
- The database test is one database and one size. A bigger server, sharded counters or a cache in front would move the red line, but popular items would still compete for the same row.
- LinkedIn's internals. I used what LinkedIn has published. Its current setup isn't public.
The whole guide in 10 lines
- Refresh doesn't count your views; they were counted as they arrived.
- The pattern: event, stream, running count, fast store, cheap read.
- LinkedIn described post analytics as impression events in Kafka, aggregated by Samza, served from a data store.
- Premium changes what you can see, not what gets counted.
- In my rebuild, a view reached the count in under a second at up to 5,000 views a second, with none lost.
- A plain +1 counter stalled at about 1,100 updates a second on popular posts, because they all wait for the same row.
- Appending one row per view did about 14,700 a second on the same database.
- Counting unique people needs an estimate at scale; it was within 1% here.
- When the counter went down for three minutes, the stream held every view and nothing was lost.
- Silent failures are real: compare what you sent with what got counted.
Checked against LinkedIn Engineering posts (post analytics Nov 2016, Kafka 2015, Samza 2016, Pinot 2014), the Apache Kafka site, PostgreSQL 16 docs (row-level locks), LinkedIn Help (creator analytics; profile viewers Basic vs Premium) and Microsoft Learn (Event Hubs Kafka configurations), as of November 2026. Every measured number is from my runs on 31 October 2026 in Azure Central India: Event Hubs Standard, Azure Data Explorer Dev SKU, PostgreSQL 16 Flexible Server General Purpose D2ds_v5, load from a D2s_v5 VM. The step-through figure is an illustration; the charts and tables are measured.