I was recently working on a project that uses the AWS SDK for JavaScript. When updating the dependencies in said project, I noticed that the version of that dependency was v3.888.0
. Eight hundred eighty eight. That’s a big number as far as versions go.
That got me thinking: I wonder what package in the npm registry has the largest number in its version. It could be a major, minor, or patch version, and it doesn’t have to be the latest version of the package. In other words, out of the three numbers in <major>.<minor>.<patch>
for each version for each package, what is the largest number I can find?
TL;DR? Jump to the results to see the answer.
The npm API
Obviously npm has some kind of API, so it shouldn’t be too hard to get a list of all… 3,639,812 packages. Oh. That’s a lot of packages. Well, considering npm had 374 billion package downloads in the past month, I’m sure they wouldn’t mind me making a few million HTTP requests.
Doing a quick search for “npm api” leads me to a readme in the npm/registry repo on GitHub. There’s a /-/all
endpoint listed in the table of contents which seems promising. That section doesn’t actually exist in the readme, but maybe it still works?
1
$ curl 'https://registry.npmjs.org/-/all'
2
{"code":"ResourceNotFound","message":"/-/all does not exist"}
Whelp, maybe npm packages have an ID and I can just start at 1 and count up? It looks like packages have an _id
field… never mind, the _id
field is the package name. Okay, let’s try to find something else.
A little more digging brings me to this GitHub discussion about the npm replication API. So npm replicates package info in CouchDB at https://replicate.npmjs.com
, and conveniently, they support the _all_docs
endpoint. Let’s give that a try:
1
$ curl 'https://replicate.npmjs.com/registry/_all_docs'
2
{
3
"total_rows" : 3628088,
4
"offset" : 0,
5
"rows" : [
6
{
7
"id" : "-",
8
"key" : "-",
9
"value" : {
10
"rev" : "5-f0890cdc1175072e37c43859f9d28403"
11
}
12
},
13
{
14
"id" : "--------------------------------------------------------------------------------------------------------------------------------whynunu",
15
"key" : "--------------------------------------------------------------------------------------------------------------------------------whynunu",
16
"value" : {
17
"rev" : "1-1d26131b0f8f9702c444e061278d24f2"
18
}
19
},
20
{
21
"id" : "-----hsad-----",
22
"key" : "-----hsad-----",
23
"value" : {
24
"rev" : "1-47778a3a6f9d8ce1e0530611c78c4ab4"
25
}
26
},
27
# 997 more packages...
Those are some interesting package names. Looks like this data is paginated and by default I get 1,000 packages at a time. When I write the final script, I can set the limit
query parameter to the max of 10,000 to make pagination a little less painful.
Fortunately, the CouchDB docs have a guide for pagination, and it looks like it’s as simple as using the skip
query parameter.
1
$ curl 'https://replicate.npmjs.com/registry/_all_docs?skip=1000'
2
"Bad Request"
Never mind. According to the GitHub discussion linked above, skip
is no longer supported. The “Paging (Alternate Method)” section of the same page says that I can use startkey_docid
instead. If I grab the id
of the last row, I should be able to use that to return the next set of rows. Fun fact: The 1000th package (alphabetically) on npm is 03-webpack-number-test
.
1
$ curl 'https://replicate.npmjs.com/registry/_all_docs?startkey_docid="03-webpack-number-test"'
2
{
3
"total_rows" : 3628102,
4
"offset" : 999,
5
"rows" : [
6
# another 1000 packages...
Nice. Also, another 3628102 - 3628088 = 14
packages have been published in the ~15 minutes since I ran the last query.
Now, there’s one more piece of the puzzle to figure out. How do I get all the versions for a given package? Unfortunately, it doesn’t seem like I can get package version information along with the base info returned by _all_docs
. I have to separately fetch each package’s metadata from https://registry.npmjs.org/<package_id>
. Let’s see what good ol’ trusty 03-webpack-number-test
looks like:
1
$ curl 'https://registry.npmjs.org/03-webpack-number-test'
2
{
3
# i've omitted some fields here
4
"_id" : "03-webpack-number-test",
5
"versions" : {
6
"1.0.0" : { ... },
7
# the rest of the versions...
Alright, I have everything I need. Now I just need to write a bash script that— just kidding. A wise programmer once said, “if your shell script is more than 10 lines, it shouldn’t be a shell script” (that was me, I said that). I like TypeScript, so let’s use that.
The biggest bottleneck is going to be waiting on the GET
s for each package’s metadata. My plan is this:
- Grab all the package IDs from the replication API and save that data to a file (I don’t want to have to refetch everything if the something goes wrong later in the script)
- Fetch package data in batches so we’re not just doing 1 HTTP request at a time
- Save the package data to a file (again, hopefully I only have to fetch everything once)
Once I have all the package data, I can answer the original question of “largest number in version” and look at a few other interesting things.
(A few hours and many iterations later…)
1
$ bun npm-package-versions.ts
2
Fetching package IDs...
3
Fetched 10000 packages IDs starting from offset 0
4
# this goes on for a while...
5
Finished fetching package IDs
6
Fetched 50 packages in 884ms (57 packages/s)
7
Fetched 50 packages in 852ms (59 packages/s)
8
# this goes on for a really long while...
See the script section at the end if you want to see what it looks like.
Results
Some stats:
- Time to fetch all ~3.6 million package IDs: A few minutes
- Time to fetch version data for each one of those packages: ~12 hours (yikes)
- Packages fetched per second: ~84 packages/s
- Size of
package-ids.json
: ~78MB - Size of
package-data.json
: ~886MB
And the winner is… (not really) latentflip-test at version 1000000000000000000.1000000000000000000.1000000000000000000
. And no, there haven’t actually been one quintillion major versions of this package published. Disappointing, I know.
Okay, I feel like that shouldn’t count. I think we can do better and find a “real” package that actually follows semantic versioning. I think a better question to ask is this:
For packages that follow semantic versioning, which package has the largest number from <major>.<minor>.<patch>
in any of its versions?
So, what does it mean to “follow semantic versioning”? Should we “disqualify” a package for skipping a version number? In this case, I think we’ll just say that a package has to have more versions published than the largest number we find for that package. For example, a package with a version of 1.888.0
will have had at least 888 versions published if it actually followed semver.
Before we get to the real winner, here are the top 10 packages by total number of versions published:
1
electron-remote-control -> 37328 total versions
2
@npm-torg/public-scoped-free-org-test-package-2 -> 37134 total versions
3
public-unscoped-test-package -> 27719 total versions
4
carrot-scan -> 27708 total versions
5
@npm-torg/public-test-package-2 -> 27406 total versions
6
@octopusdeploy/design-system-components -> 26724 total versions
7
@octopusdeploy/type-utils -> 26708 total versions
8
@octopusdeploy/design-system-tokens -> 22122 total versions
9
@mahdiarjangi/phetch-cli -> 19498 total versions
10
@atlassian-test-prod/hello-world -> 19120 total versions
Top 10 packages that (probably) follow semver by largest number in one of its versions:
1
@mahdiarjangi/phetch-cli -> 19494 (1.0.19494)
2
electron-remote-control -> 19065 (1.2.19065)
3
@quip/collab -> 16999 (1.16999.0)
4
@atlassian-test-prod/hello-world -> 16707 (9.7.16707)
5
@wix/wix-code-types -> 14720 (2.0.14720)
6
@octopusdeploy/design-system-components -> 14274 (2025.3.14274)
7
@octopusdeploy/type-utils -> 14274 (2025.3.14274)
8
@octopusdeploy/design-system-tokens -> 14274 (2025.3.14274)
9
@atlassian-test-staging/test -> 13214 (49.4.13214)
10
binky -> 9906 (3.4.9906)
So it seems like the winner is @mahdiarjangi/phetch-cli with 19494
, right? Unfortunately, I’m not going to count that either. It only has so many versions because of a misconfigured GitHub action that published new versions in a loop.
I manually went down the above list, disqualifying any packages that had similar issues. I also checked that “new” versions actually differed from previous versions in terms of content. Overall, I looked for a package that was actually publishing new versions on purpose with some kind of change to the package content.
The real winner (#19 on the list) is: all-the-package-names with 2401
from version 2.0.2401
.
Well, that’s sort of disappointing, but also kind of funny. I don’t know what I was expecting to be honest. If you’re curious, you can see more results at the bottom of this post.
What you do with all of this extremely important and useful information is up to you.
Script
1
/* This script uses Bun specific APIs and should be executed directly with Bun */
2
3
import fs from "node:fs/promises"
4
import process from "node:process"
5
6
async function main() {
7
const NUM_TO_PRINT = 50
8
9
const packageIds = await fetchPackageIds()
10
const packageData = await fetchAllPackageData(packageIds)
11
const normalizedPackageData = normalizePackageData(packageData)
12
13
const packagsByNumOfVersions = packageData.toSorted((a, b) => b.versions.length - a.versions.length) // don't use normalizedPackageData here because it *only* includes valid semver versions
14
const packagesByLargestNumber = normalizedPackageData.toSorted((a, b) => b.largestNumber.num - a.largestNumber.num)
15
16
// Ignore packages where the number of versions isn't greater than the largest number.
17
// For example, a package with a version of 1.888.0 will have had *at least* 888 versions published if it actually followed semver.
18
const packagesWithSemverByLargestNumber = packagesByLargestNumber.filter(
19
(pkg) => pkg.versions.length >= pkg.largestNumber.num,
20
)
21
const packagesWithoutKnownBadByLargestNumber = packagesWithSemverByLargestNumber.filter((pkg) =>
22
KNOWN_BAD_PACKAGES.every((badId) => !pkg.id.startsWith(badId)),
23
)
24
25
console.log(`\nTop ${NUM_TO_PRINT} packages by total number of versions published:`)
26
packagsByNumOfVersions.slice(0, NUM_TO_PRINT).forEach(({ id, versions }, i) => {
27
console.log(`${i + 1}. ${id} -> ${versions.length} total versions`)
28
})
29
30
const logPackagesByLargestNumber = (packages: NormalizedPackageData[]) => {
31
packages.slice(0, NUM_TO_PRINT).forEach(({ id, largestNumber }, i) => {
32
console.log(`${i + 1}. ${id} -> ${largestNumber.num} (${largestNumber.version})`)
33
})
34
}
35
36
console.log(`\nTop ${NUM_TO_PRINT} packages by largest number in version:`)
37
logPackagesByLargestNumber(packagesByLargestNumber)
38
39
console.log(`\nTop ${NUM_TO_PRINT} packages that follow semver by largest number in version:`)
40
logPackagesByLargestNumber(packagesWithSemverByLargestNumber)
41
42
console.log(
43
`\nTop ${NUM_TO_PRINT} packages that follow semver by largest number in version (excluding known bad packages):`,
44
)
45
logPackagesByLargestNumber(packagesWithoutKnownBadByLargestNumber)
46
47
console.log("\nDone!")
48
}
49
50
/**
51
* These are packages that have a large number of versions because of some automation (e.g. GitHub Action), where each "new" version was identical to the last.
52
* For example, 'electron-remote-control' was publishing a version every hour for a long time due to a configuration mistake.
53
*/
54
const KNOWN_BAD_PACKAGES = [
55
"@mahdiarjangi/phetch-cli",
56
"electron-remote-control",
57
"@quip/collab",
58
"@atlassian-test",
59
"@wix/wix-code-types",
60
"@octopusdeploy",
61
"binky",
62
"carrot-scan",
63
"terrapin-test-1",
64
"@prisma/language-server",
65
"kse-visilia",
66
"intraactive-sdk-ui",
67
"@idxdb/promised",
68
"wix-style-react",
69
"botfather",
70
]
71
72
/**
73
* Fetches every single package ID from the npm replicate API and writes the data to a file.
74
*/
75
async function fetchPackageIds(): Promise<string[]> {
76
const packageIdsFile = Bun.file("package-ids.json")
77
// return the existing package IDs if they exist
78
if (await packageIdsFile.exists()) {
79
console.log("Using existing package IDs")
80
return (await packageIdsFile.json()) as string[]
81
}
82
83
console.log("Fetching package IDs...")
84
let firstFetch = true
85
let startKeyPackageId: string | undefined
86
const packageIds: string[] = []
87
88
// We use the last package ID of current fetch as the start key for the next fetch. Once the start key is the same as the last package ID, we've fetched all packages and can break out of the loop.
89
while (true) {
90
const LIMIT = 10_000
91
const startKeyQueryParam = firstFetch ? "" : `&startkey_docid="${startKeyPackageId}"`
92
const json = await fetchJson<{ rows: { id: string }[]; offset: number }>(
93
`https://replicate.npmjs.com/registry/_all_docs?limit=${LIMIT}${startKeyQueryParam}`,
94
)
95
if (!json) process.exit(1) // Stop the script if we fail to fetch package IDs. The error will have already been logged.
96
97
const { rows, offset } = json
98
console.log(`Fetched ${rows.length} package IDs starting from offset ${offset}`)
99
100
for (const { id: packageId } of rows) {
101
if (startKeyPackageId === packageId) continue // Skip the startKeyPackageId. The startKeyPackageId is already in the list because it's the same as the last package ID from the previous fetch
102
packageIds.push(packageId)
103
}
104
105
const lastPackageId = rows.at(-1)?.id
106
if (startKeyPackageId === lastPackageId) break // we've reached the end of the package IDs
107
108
startKeyPackageId = lastPackageId
109
110
firstFetch &&= false
111
}
112
113
console.log("Finished fetching package IDs")
114
console.log(`Writing package IDs to '${packageIdsFile.name}'...`)
115
await packageIdsFile.write(JSON.stringify(packageIds))
116
console.log(`Finished writing package IDs to '${packageIdsFile.name}'`)
117
118
return packageIds
119
}
120
121
interface PackageData {
122
id: string
123
versions: string[]
124
}
125
126
/**
127
* Fetches all package metadata from the npm registry API and writes the data to a file.
128
*/
129
async function fetchAllPackageData(packageIds: string[]): Promise<PackageData[]> {
130
/** The number of packages to fetch at once */
131
const BATCH_SIZE = 50
132
133
interface FetchedPackageData {
134
_id: string
135
versions?: Record<string, unknown> // when we fetch package data, sometimes the versions object is missing
136
}
137
138
const packageDataFile = Bun.file("package-data.json")
139
// return the existing package data if it exists
140
if (await packageDataFile.exists()) {
141
console.log("Using existing package data")
142
return (await packageDataFile.json()) as PackageData[]
143
}
144
145
console.log("Fetching package data...")
146
const allPackageData: PackageData[] = []
147
148
while (packageIds.length > 0) {
149
const startTime = Date.now()
150
151
const batch = packageIds.splice(0, BATCH_SIZE)
152
153
const packageDataPromises = batch.map(async (packageId) => {
154
const fetchedPackageData = await fetchJson<FetchedPackageData>(
155
`https://registry.npmjs.org/${encodeURIComponent(packageId)}`,
156
)
157
if (!fetchedPackageData) return
158
const { _id, versions = {} } = fetchedPackageData // default versions to an empty object if it doesn't exist
159
const packageData: PackageData = { id: _id, versions: Object.keys(versions).reverse() } // reverse the versions array so the newest version is first
160
return packageData
161
})
162
const packageData = (await Promise.all(packageDataPromises)).filter((data) => data !== undefined)
163
164
allPackageData.push(...packageData)
165
166
const endTime = Date.now()
167
const duration = endTime - startTime
168
console.log(
169
`Fetched ${packageData.length} packages in ${duration}ms (${Math.round((packageData.length / duration) * 1000)} packages/s)`,
170
)
171
}
172
173
console.log("Finished fetching package data")
174
console.log(`Writing package data to '${packageDataFile.name}'...`)
175
await packageDataFile.write(JSON.stringify(allPackageData))
176
console.log(`Finished writing package data to '${packageDataFile.name}'`)
177
178
return allPackageData
179
}
180
181
type SemverNumbers = [number, number, number]
182
interface NormalizedPackageData {
183
id: string
184
largestNumber: {
185
num: number
186
version: string
187
}
188
versions: SemverNumbers[]
189
}
190
191
/**
192
* Transforms package data so that it includes the largest number from all of its versions.
193
* In each `versions` array, only valid semver versions are kept.
194
*/
195
function normalizePackageData(packageData: PackageData[]): NormalizedPackageData[] {
196
console.log("Getting normalized package data...")
197
const normalizedPackageData = packageData
198
.map((pkg) => {
199
const semverVersions = pkg.versions
200
.map((version) => splitSemver(version))
201
.filter((version) => version !== undefined)
202
if (semverVersions.length === 0) return // if the package didn't have any valid semver versions, don't include it
203
204
let largestNumber = { num: 0, version: "" }
205
for (const semver of semverVersions) {
206
const [major, minor, patch] = semver
207
const num = Math.max(major, minor, patch)
208
if (num > largestNumber.num) largestNumber = { num, version: semver.join(".") }
209
}
210
211
const normalizedPackage = { id: pkg.id, largestNumber, versions: semverVersions }
212
return normalizedPackage
213
})
214
.filter((pkg) => pkg !== undefined)
215
216
console.log("Finished getting normalized package data")
217
218
return normalizedPackageData
219
}
220
221
await main()
222
223
/* UTILS */
224
225
/**
226
*
227
* Splits a valid semver string into an array of three number: `[major, minor, patch]`
228
* If the string is not a valid semver, `undefined` is returned.
229
*/
230
function splitSemver(version: string): SemverNumbers | undefined {
231
const versionParts = version
232
.split(".")
233
.slice(0, 3)
234
.map((part) => (Number.isInteger(Number(part)) ? Number.parseInt(part, 10) : undefined))
235
.filter((part) => part !== undefined)
236
if (versionParts.length !== 3) return
237
const [major, minor, patch] = versionParts as SemverNumbers
238
return [major, minor, patch]
239
}
240
241
/**
242
* Calls `console.error` with the message and appends the message to a `error.log` file.
243
*/
244
function logError(message: string) {
245
console.error(message)
246
fs.appendFile("error.log", message)
247
}
248
249
/**
250
* Fetches the url and returns the JSON response object. Calls {@link logError} and returns `undefined` instead of throwing if an error occurs.
251
*/
252
async function fetchJson<T>(url: string): Promise<T | undefined> {
253
try {
254
const response = await fetch(url)
255
if (!response.ok) throw new Error(`(${response.status}) ${await response.text()}`)
256
return (await response.json()) as T
257
} catch (error) {
258
const errorMessage = error instanceof Error ? error.message : String(error)
259
logError(`something went wrong fetching json for '${url}': ${errorMessage}`)
260
}
261
262
return
263
}
More Results
This is from the script:
1
Top 50 packages by total number of versions published:
2
1. electron-remote-control -> 37328 total versions
3
2. @npm-torg/public-scoped-free-org-test-package-2 -> 37134 total versions
4
3. public-unscoped-test-package -> 27719 total versions
5
4. carrot-scan -> 27708 total versions
6
5. @npm-torg/public-test-package-2 -> 27406 total versions
7
6. @octopusdeploy/design-system-components -> 26724 total versions
8
7. @octopusdeploy/type-utils -> 26708 total versions
9
8. @octopusdeploy/design-system-tokens -> 22122 total versions
10
9. @mahdiarjangi/phetch-cli -> 19498 total versions
11
10. @atlassian-test-prod/hello-world -> 19120 total versions
12
11. @quip/collab -> 17004 total versions
13
12. @gitpod/gitpod-protocol -> 16145 total versions
14
13. ricos-build-cache -> 15867 total versions
15
14. @wix/wix-code-types -> 15030 total versions
16
15. @gitpod/supervisor-api-grpc -> 14364 total versions
17
16. @atlassian-test-staging/test -> 13330 total versions
18
17. @yuming2022/app-dnpkg-beta -> 12995 total versions
19
18. nocodb-sdk-daily -> 12512 total versions
20
19. @dais/sdk-minimal -> 12090 total versions
21
20. nc-lib-gui-daily -> 11874 total versions
22
21. binky -> 11460 total versions
23
22. nocodb-daily -> 11283 total versions
24
23. construct-hub-probe -> 10822 total versions
25
24. renovate -> 10369 total versions
26
25. @primer/react -> 10320 total versions
27
26. @codecademy/styleguide -> 10186 total versions
28
27. @prisma/client -> 10064 total versions
29
28. @prisma/migrate -> 9994 total versions
30
29. @prisma/generator-helper -> 9847 total versions
31
30. decentraland-renderer -> 9640 total versions
32
31. gfcdn.startpage -> 9580 total versions
33
32. @prisma/debug -> 9521 total versions
34
33. @codecademy/gamut -> 9411 total versions
35
34. @coral-xyz/xnft-cli -> 9397 total versions
36
35. @birdeye-so/tokenswap -> 9382 total versions
37
36. @pdftron/webviewer -> 9373 total versions
38
37. @gitpod/supervisor-api-grpcweb -> 9274 total versions
39
38. @gitpod/local-app-api-grpcweb -> 9177 total versions
40
39. kse-visilia -> 9150 total versions
41
40. @prisma/fetch-engine -> 8988 total versions
42
41. @coral-xyz/common -> 8946 total versions
43
42. @prisma/get-platform -> 8926 total versions
44
43. @materializeinc/sql-lexer -> 8868 total versions
45
44. xnft -> 8846 total versions
46
45. @prisma/language-server -> 8826 total versions
47
46. prisma -> 8675 total versions
48
47. @stoplight/cli -> 8455 total versions
49
48. electron-apps -> 8445 total versions
50
49. @knapsack/schema-utils -> 8332 total versions
51
50. @knapsack/utils -> 8323 total versions