Short answer: you can count Bluesky's active writers yourself by listening to Jetstream and counting distinct accounts. Every post, like, repost and follow passes through Bluesky's public event stream with the author's DID attached. Collect the DIDs for a day and you have a daily active count; merge 30 days and you have a monthly one. What you can't count this way is people who only read, which is why every outside MAU figure, including this one, is a floor.
What Jetstream Gives You
Jetstream is a lightweight version of the Bluesky firehose. It sends plain JSON over a WebSocket, lets you filter by record type, and doesn't need an account or an API key. Each event looks roughly like this:
{
"did": "did:plc:abc123...",
"time_us": 1762200000000000,
"kind": "commit",
"commit": { "operation": "create", "collection": "app.bsky.feed.like", ... }
}
That did is the account that did something. Count unique DIDs and you're counting active accounts. For the difference between Jetstream and the raw firehose, see Firehose vs Jetstream.
The Script
This needs Node 22 or later, which has WebSocket built in, and no packages:
// count-active.mjs: counts distinct Bluesky accounts that write, per UTC day.
// Run with: node count-active.mjs (Node 22+ has WebSocket built in)
import fs from "node:fs";
const COLLECTIONS = [
"app.bsky.feed.post", "app.bsky.feed.like", "app.bsky.feed.repost", "app.bsky.graph.follow",
];
const url = "wss://jetstream2.us-east.bsky.network/subscribe?" +
COLLECTIONS.map(c => "wantedCollections=" + c).join("&");
let day = new Date().toISOString().slice(0, 10);
let active = new Set();
function flush() {
fs.appendFileSync("dau.csv", `${day},${active.size}\n`);
fs.writeFileSync(`dids-${day}.txt`, [...active].join("\n"));
console.log(day, active.size);
}
function connect() {
const ws = new WebSocket(url);
ws.onmessage = (msg) => {
const ev = JSON.parse(msg.data);
if (ev.kind !== "commit" || ev.commit.operation !== "create") return;
const today = new Date(ev.time_us / 1000).toISOString().slice(0, 10);
if (today !== day) { flush(); day = today; active = new Set(); }
active.add(ev.did);
};
ws.onclose = () => setTimeout(connect, 5000); // reconnect, keep counting
}
connect();
It writes two things: a running dau.csv with one line per day, and a file of that day's DIDs so you can combine days later. For the monthly number, after 30 days:
# 30-day "writing MAU": distinct DIDs across the last 30 daily files
cat $(ls dids-*.txt | tail -30) | sort -u | wc -l
Run it somewhere that stays on, such as a small server or a Raspberry Pi. The DID files take a few tens of megabytes a day.
Choosing What Counts as "Active"
The script counts accounts that created a post, like, repost or follow. You can widen or narrow that by editing COLLECTIONS:
| Definition | Collections | Effect |
|---|---|---|
| Posters only | app.bsky.feed.post | Smallest number; closest to "people who talk" |
| Interacting accounts | post, like, repost, follow | The script's default |
| Any write | No filter at all | Largest; includes blocks, list edits, profile updates and every app built on the AT Protocol |
Definitions explain most of the disagreement between published estimates. Two honest counts over the same month can differ by millions depending on which line of this table they use.
What It Misses
- Readers. Someone who opens Bluesky every day and never likes anything produces no events. This is the biggest gap, and nothing public fills it.
- Private activity. DMs, group chats, bookmarks and mutes don't go through the firehose.
- Downtime. If your script disconnects for an hour, that hour's accounts are missing. Jetstream supports a
cursorparameter to resume from a timestamp if you want to make it more robust.
What It Overcounts
- Bots. Automated accounts are active every day by design. Filter accounts that post at inhuman rates if you want a human estimate.
- Other apps. Without the collection filter you'd count people using other AT Protocol apps who never use Bluesky itself.
- One person, several accounts. No way around this one from public data.
Reading Your Numbers
Use the ratio more than the absolute number. Writing DAU divided by writing MAU tells you how often active writers come back, and it's stable enough to compare month to month. Watch ordinary weekdays rather than news events, which spike for a day and fall back. Our monthly reports, most recently October 2026, use the same reasoning.
Frequently Asked Questions
Can I calculate Bluesky's monthly active users myself?
You can count accounts that post, like, repost or follow by listening to Jetstream and collecting distinct DIDs over 30 days. That gives a floor, because people who only read don't produce any public events.
Do I need an API key to use Jetstream?
No. Bluesky's public Jetstream instances don't require an account or key.
Why are Bluesky MAU estimates so different?
Mostly definitions. Counting only posters, counting all interactions, and counting any write to the network give very different numbers, and none of them include readers.
How much data does tracking active users take?
Storing one day's distinct DIDs takes tens of megabytes. The event stream itself is larger, but the script only keeps the account IDs.
Does this count bots?
Yes. Automated accounts write constantly, so filter out accounts with inhuman posting rates if you want an estimate of people.
Built on the Same API
Skyscraper is an independent Bluesky and Mastodon client for iPhone, iPad, Mac, Apple TV, Apple Watch and Vision Pro. Everything in this post comes from building it: caching, backoff and one long-lived session per account so you don't burn through your limits.