Package Health: Crates, Go, Maven, NuGet, Packagist, RubyGems
Pricing
$0.80 / 1,000 package checkeds
Package Health: Crates, Go, Maven, NuGet, Packagist, RubyGems
Check packages on crates.io, Go modules, Maven Central, NuGet, Packagist and RubyGems in one schema: exists, latest version, release age, licence, repository, yanked or deprecated, plus readable flags.
Pricing
$0.80 / 1,000 package checkeds
Rating
0.0
(0)
Developer
Mohammed Samaan
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Package Health: crates.io, Go, Maven, NuGet, Packagist, RubyGems
Give this actor a list of packages and it tells you, for each one, whether it exists, what the latest version is, when it was released, whether that version is yanked, deprecated or relocated, what licence and repository the registry lists, and how many downloads the registry reports (where it reports them). All six registries come back in the same JSON shape, so a CI job, a dependency review or an AI coding agent reads one format instead of six.
Typical uses:
- Check a dependency list before a release or an audit: which packages have not shipped in two years, which are abandoned, which have no licence listed.
- Let an AI agent confirm that a package name it is about to add really exists in the ecosystem (a missing package returns
exists: false). - Build a spreadsheet of licences and repositories for a Rust, Go, Java, .NET, PHP or Ruby project.
npm and PyPI are deliberately not covered.
Sample input
{"packages": ["crates:fastrand","go:github.com/google/uuid","maven:com.google.guava:guava","nuget:Serilog","packagist:swiftmailer/swiftmailer","rubygems:rake"],"contactEmail": ""}
Input
| Field | Type | Meaning |
|---|---|---|
packages | array, 1 to 1000 | Each entry is "ecosystem:name" or an object {"ecosystem": "...", "name": "..."}. Maven also accepts {"ecosystem": "maven", "groupId": "...", "artifactId": "..."}. Ecosystems: crates, go, maven, nuget, packagist, rubygems (aliases such as cargo, golang, composer, gem work). |
contactEmail | string, optional | Sent to the registries inside the User-Agent header (Packagist and crates.io ask API users to identify themselves). Leave empty to send only the actor name. |
Name forms: crates serde; Go module path github.com/google/uuid; Maven groupId:artifactId (no version, the latest is always checked); NuGet Newtonsoft.Json; Packagist vendor/package (lower-cased for you); RubyGems rake. Duplicates in one run are dropped and counted in the run summary, so you are not charged twice; names are compared the way each registry compares them (crates.io treats case, - and _ as the same, so serde_json and serde-json are one crate; NuGet and Packagist ignore case; Go, Maven and RubyGems names are case sensitive). A bad entry (unknown ecosystem, malformed name) gets its own invalid_input row with the reason and does not stop the run. More than 1000 packages, or an empty list, fails the run with a message naming the field.
Output
One dataset row per package. Every row has the same keys.
| Field | Meaning |
|---|---|
status | ok (found), not_found (the registry says it does not exist), error (registry or network failure, nothing known), invalid_input |
exists | true, false, or null when the answer is unknown (error rows) |
latestVersion, latestReleaseDate, daysSinceRelease | See "What counts as latest" below. daysSinceRelease is counted from the run time. |
deprecatedOrYanked | true / false, or null when the registry data needed to decide could not be read |
deprecationNote | The registry's own message (yank message, deprecation text, replacement, relocation target) |
license | The licence exactly as the registry gives it. licenseSpdx is filled only when that text is a valid SPDX identifier or expression from the list in src/spdx-ids.js (OSI-approved and FSF-libre identifiers), otherwise null. |
repositoryUrl, homepageUrl | As listed by the registry |
downloads | Total downloads when the registry reports them (crates.io, NuGet, Packagist, RubyGems). null for Go and Maven, which publish none. |
flags | Readable rules, see below |
warnings | Things you should know about this row (a licence taken from a parent POM, a pre-release marker, a missing optional lookup) |
source | The registry URLs that were read |
error, errorKind | Specific message for error and invalid_input rows, and a short kind: timeout, network, server_error, rate_limited, http_error, bad_response, blocked, too_large, registry_unavailable (the registry was skipped after repeated failures), invalid_input, unexpected |
position, input, checkedAt, schemaVersion | 1-based place in your list, your entry as sent, UTC time, schema version (currently 1) |
Rows are written as each package finishes, so the order can differ from your list; sort by position.
Sample output (real run, 21 Oct 2026)
{"schemaVersion": 1,"position": 15,"input": "packagist:swiftmailer/swiftmailer","ecosystem": "packagist","name": "swiftmailer/swiftmailer","status": "ok","exists": true,"latestVersion": "v6.3.0","latestReleaseDate": "2021-10-18T15:26:12.000Z","daysSinceRelease": 1828,"deprecatedOrYanked": true,"deprecationNote": "Abandoned. Suggested replacement: symfony/mailer","license": "MIT","licenseSpdx": "MIT","repositoryUrl": "https://github.com/swiftmailer/swiftmailer","homepageUrl": "https://swiftmailer.symfony.com","downloads": 452698131,"flags": ["no-release-in-24-months", "latest-version-yanked-or-deprecated"],"warnings": [],"source": ["https://repo.packagist.org/p2/swiftmailer/swiftmailer.json", "https://packagist.org/packages/swiftmailer/swiftmailer.json"],"checkedAt": "2026-10-21T10:37:43.875Z","error": null}
Flag rules
There is no health score. A flag is one plain rule applied to the row:
| Flag | Raised when |
|---|---|
no-release-in-24-months | daysSinceRelease is greater than 730 |
latest-version-yanked-or-deprecated | deprecatedOrYanked is true: crates.io yanked, Go // Deprecated: in go.mod, NuGet deprecation, Maven <relocation>, Packagist abandoned. RubyGems has no such marker, so deprecatedOrYanked is null there and it never raises this. |
license-missing | The registry supports licence data and none is listed. Never raised for Go (the proxy serves no licence data). |
repository-missing | The registry supports a repository link and none is listed. Never raised for Go, whose proxy omits the origin for older versions; for github.com, gitlab.com and bitbucket.org module paths the repository is inferred from the path (with a warning). |
no-listed-version | The package exists but every version is unlisted (NuGet) or none is readable |
A quiet package can still be fine and a flagged one can still be fit for purpose; the flags only tell you where to look.
What counts as latest
| Registry | latestVersion | latestReleaseDate | Notes |
|---|---|---|---|
| crates.io | max_stable_version (highest non-yanked stable), else max_version | created_at of that version | If the newest published version is yanked, a warning says so |
| Go | @latest from the module proxy | The proxy's Time | Licence and downloads are not available from the proxy. Retractions are not checked. |
| Maven Central | <release> in maven-metadata.xml; if that is a pre-release, the newest version without a pre-release qualifier | Last-Modified header of that version's POM on Central (close to upload time, not guaranteed identical), falling back to the metadata lastUpdated | Licence and repository come from the POM, or from up to three parent POMs when the artifact declares none (a warning says so). Several licences are joined with ; . |
| NuGet | Highest listed non-pre-release version, else highest listed pre-release | published | Licence is licenseExpression; if the package only has a licence file or a licence URL, that URL is returned with a warning and licenseSpdx stays null. Repository comes from the nuspec. |
| Packagist | Highest stable version in the static p2 file | time | repositoryUrl is the source.url listed there (often ending in .git). Downloads come from the small /stats.json endpoint. Several licences are joined with OR (Composer treats a list as alternatives) |
| RubyGems | version from the gem document (latest non-pre-release) | version_created_at | repositoryUrl is source_code_uri only; a homepage is not guessed to be a repository |
Registry usage rules followed
| Registry | Policy read | Rule followed |
|---|---|---|
| crates.io | https://crates.io/data-access | The policy lists a sparse index (no rate limit) and a daily database dump first, and describes API use within 1 request per second and with a User-Agent that identifies the application and a way to contact the operator. The sparse index has no release dates, licences, repository links or download counts, which this actor reports, and the dump is a bulk download of everything that this actor does not use, so it reads the API: requests are 1.1 s apart, one at a time, with the User-Agent package-registry-health/0.1 (Apify actor; +https://apify.com/leadharbor/package-registry-health) plus your optional contact email. |
| Go modules | https://proxy.golang.org/ and https://go.dev/ref/mod#goproxy-protocol | No numeric limit is published. The actor keeps to about 5 requests per second, 4 at a time, and backs off on 429 and 5xx. |
| Maven Central | https://central.sonatype.org/faq/429-error/ and https://central.sonatype.org/consume/ | Consumption limits apply to very high-volume traffic and a 429 is a temporary block. The actor reads repo1.maven.org/maven2 (the URL Central documents) with at most about 4 requests per second, 2 at a time, never retries harder after a 429, and stops using Maven Central for the rest of the run after 3 rate-limit answers in a row. |
| NuGet | https://learn.microsoft.com/en-us/nuget/api/overview | No numeric limit is published on the pages read. Service URLs are taken from api.nuget.org/v3/index.json; about 5 requests per second, 3 at a time. |
| Packagist | https://packagist.org/apidoc | Send a User-Agent with a contact (add your email in contactEmail), at most 10 concurrent requests, prefer the static repo.packagist.org files. Version data uses the static file; downloads use the small stats.json endpoint. 3 at a time, about 5 per second. |
| RubyGems | https://guides.rubygems.org/rubygems-org-rate-limits/ | API limit 10 requests per second per IP. The actor stays at about 5 per second, 3 at a time, and honours Retry-After. |
This actor reads public registry metadata only. It does not ask any provider for permission beyond their published API rules, and you are responsible for how you use the data you collect and for complying with each registry's terms.
Every request has a 15 second timeout. A 429, a 5xx, a timeout or a network error is retried up to 3 times with back-off (1 s, 2 s, 4 s, capped at 30 s). If a registry asks for a wait longer than 30 s, the item fails fast with a rate_limited error row instead of waiting. Redirects are followed by hand, at most 3 hops, and only to the same registry hosts: a redirect to any other host or address is refused before it is requested.
Outages and run time. If a registry fails 5 requests in a row (timeouts, 5xx, network errors) or answers 429 three times in a row, it is skipped for the rest of the run: its remaining packages become error rows with errorKind: "registry_unavailable", are not charged, and the other registries keep going. The run also stops starting new packages after a 50 minute budget and says so in the status message and the SUMMARY record.
Because of the crates.io rule, a run with many crates is slow by design: N crates need at least 1.1 s times N (1000 crates is about 18 minutes). Other registries run in parallel with it.
Pricing
Pay per event: package-checked, $0.0008 per package (price to be confirmed on the Store page when published).
| Charged | Not charged |
|---|---|
ok rows and not_found rows (a definite answer, including "does not exist") | error rows (registry down, timeout, rate limited), invalid_input rows, duplicates |
Worked example: 250 packages, of which 240 exist, 6 do not exist and 4 hit a registry timeout: 246 charged x $0.0008 = $0.1968. Each row is charged first and delivered only if the charge was accepted, so the dataset never holds more definite answers than you paid for. If you set a maximum charge for the run and it is reached, the actor stops starting new packages and says so in the run status and the SUMMARY record (charged counts accepted charges only).
Use with AI agents / MCP
Send {"packages": ["crates:serde", "maven:com.google.guava:guava"]}. Read status first. If it is ok, use exists, latestVersion, deprecatedOrYanked and flags. exists: false with status: not_found means the registry has no such package. Treat status: error as "unknown, retry later", never as "does not exist".
Memory: measured peak RSS was 123 MB (88 MB at start) for 30 live packages across all six registries on Node 24 on Windows; the actor is set to 512 MB (Node 20 on the Apify image was not measured separately).
FAQ
Is there a health score? No. Only the flags above, each a rule you can read.
Why is license sometimes a URL or a sentence? It is shown as the registry gives it. licenseSpdx is filled only when the text is a valid SPDX identifier or expression from the bundled list.
Why does serde_json and serde-json cost once? They are the same crate on crates.io.
Why are Go licences empty? The Go proxy serves none. This actor does not scrape pkg.go.dev or GitHub.
Does it read private packages? No. Public registry endpoints only; no logins, cookies, proxies or browsers.
How current is the data? Read live from each registry at run time, except Packagist downloads (cached up to 12 hours by Packagist) and NuGet download counts (from the NuGet search index, which can lag).
Limits
- Release dates for Maven are the POM file's
Last-Modifiedheader, an approximation of the upload time. - Maven's latest is chosen by the metadata marker and a pre-release qualifier pattern (alpha, beta, rc, milestone, snapshot and similar); an unusual version scheme could be misread.
- Go retractions and pseudo-version edge cases are not analysed beyond the deprecation comment.
- Yanked versions other than the latest are not reported. RubyGems yanks are invisible because a yanked version disappears from the listing, which is why RubyGems rows have
deprecatedOrYanked: null. - Each ecosystem's own definition of "latest", "deprecated" and "licence" is used; the actor does not compare registries with each other.
Related actors
More actors by the same developer: https://apify.com/leadharbor
Open-source credits
| Component | Licence | Verified from |
|---|---|---|
apify 3.7.2 (Apify SDK for JavaScript), the only direct dependency | Apache-2.0 | node_modules/apify/LICENSE.md (Apache License 2.0 text) |
| Transitive packages of the SDK (187 installed) | MIT 135, Apache-2.0 18, BSD-2-Clause 14, ISC 8, BSD-3-Clause 5, other permissive 5 (MIT OR CC0-1.0, BlueOak-1.0.0, 0BSD, MIT/Apache-2.0 dual), plus caniuse-lite (CC-BY-4.0 data, attribution only) and map-stream (no license field in package.json; its LICENCE file is the MIT text) | license field of each installed package.json, node_modules/map-stream/LICENCE, node_modules/caniuse-lite/LICENSE |
SPDX identifier list in src/spdx-ids.js | Identifiers only, generated from SPDX License List v3.29.0 (https://spdx.org/licenses/) | licenses.json and exceptions.json of the SPDX license-list-data repository |
Everything else (HTTP client, rate limiter, adapters, POM and go.mod readers) is written for this actor with Node built-ins.