You built something useful. Now share it
You spent the weekend writing a parser that handles CSV files with embedded newlines. It works. Your friends want to use it. You can't just email them the folder. You need to put it on crates.io so they can type cargo add and get it. But the registry isn't a git remote. It has rules, and it will reject your crate if your metadata is thin or your package includes your .git directory.
Publishing a crate is a deliberate process. Cargo checks your crate against a strict set of requirements before the registry accepts it. This protects everyone who depends on your code. The workflow involves preparing metadata, verifying the package locally, and uploading the artifact.
Treat the registry as a gatekeeper. Respect the rules, and your crate ships.
Publishing is a one-way trip
Once a version exists on crates.io, it stays there forever. You cannot overwrite 0.1.0. If you publish a version with a typo, a broken build, or a missing file, that version is permanent. The only way to fix a published version is to bump the version number and publish again.
This immutability is a feature, not a bug. It ensures that builds are reproducible. If a project depends on my-crate = "0.1.0", that dependency will always resolve to the exact same code. If you could overwrite versions, existing projects could break without changing their Cargo.toml.
Publishing is immutable. Treat every version as a permanent artifact.
The minimal setup
Before you can publish, you need an account on crates.io and an API token. Generate the token in your profile settings. The token authenticates your local machine with the registry.
# Login stores the token in ~/.cargo/credentials.
# You only need to do this once per machine.
cargo login <YOUR_API_TOKEN>
Your Cargo.toml must include specific metadata fields. The registry rejects crates missing description, license, or repository. These fields help users discover and trust your crate.
[package]
name = "my-crate"
version = "0.1.0"
edition = "2021"
// Registry requires these fields. Missing them causes rejection.
description = "A crate that parses CSV files with embedded newlines"
license = "MIT"
repository = "https://github.com/user/my-crate"
Run cargo package to verify everything before uploading. This command creates a tarball locally and checks metadata, file inclusion, and build success. It simulates exactly what the registry will do.
# Dry run. Checks metadata and file inclusion.
# If this fails, cargo publish will also fail.
cargo package
# Upload the tarball to crates.io.
# Idempotent for the same version.
cargo publish
Run cargo package first. It catches the mistakes that make you look bad.
What happens under the hood
When you run cargo package, Cargo creates a tarball in target/package. It strips out files listed in .gitignore, checks that all required metadata is present, and attempts to build the crate from the tarball. This build step ensures that the crate compiles in a clean environment, free from local artifacts or uncommitted changes.
If cargo package succeeds, the tarball is ready. cargo publish takes that tarball and sends it to the crates.io API. The registry validates the metadata again and stores the artifact. The registry also runs the build to double-check that the crate compiles.
The dry run is your safety net. Trust it.
Real-world crates have more files
Real crates include more than src/lib.rs. You likely have examples/, tests/, README.md, and a LICENSE file. Cargo uses .gitignore by default to decide what to include in the package. Files ignored by git are excluded from the tarball. This prevents your target/ directory, .env files, and editor backups from ending up in the published crate.
If you need to include a file that git ignores, you must explicitly list it in the include field. This overrides the default behavior.
[package]
name = "real-crate"
version = "0.1.0"
edition = "2021"
description = "A realistic crate with examples and docs"
license = "Apache-2.0 OR MIT"
repository = "https://github.com/user/real-crate"
// Explicit include list overrides gitignore behavior.
// Use this only when you need files that git ignores.
include = ["src/", "Cargo.toml", "README.md", "LICENSE", "examples/"]
Convention aside: Use cargo package --list to audit your tarball. This command prints every file that will be included. Run it before publishing to verify that you aren't shipping secrets or missing your documentation.
Audit your tarball. cargo package --list tells you exactly what the world gets.
Versioning and the SemVer contract
Crates.io follows Semantic Versioning. Versions have three parts: MAJOR.MINOR.PATCH. The MAJOR version increments when you make breaking changes. The MINOR version increments when you add features in a backward-compatible way. The PATCH version increments for bug fixes.
Versions starting with 0.x.y are pre-1.0. The community treats 0.x.y as unstable. You can make breaking changes in minor versions when the major version is zero. Once you reach 1.0.0, you commit to stability. Breaking changes require a major version bump.
The registry allows pre-release versions like 0.1.0-alpha.1. These are distinct from 0.1.0. You can publish 0.1.0-alpha.1, then 0.1.0-beta.1, then 0.1.0. Each is a unique version. Users must opt-in to pre-release versions.
Convention aside: Start your crate at 0.1.0. Avoid 0.0.0. It looks like a placeholder and confuses users. Use 0.1.0 for the first usable release.
Bump the version. Never rewrite history.
When things go wrong: yanking and fixes
If you publish a broken version, you cannot delete it. You can yank it. Yanking marks a version as unavailable for new downloads. Existing projects that already depend on the version can still build. This preserves the dependency graph while preventing new users from hitting the broken code.
# Yank version 0.1.0.
# New cargo add commands will skip this version.
cargo yank --vers 0.1.0
# Yank with a reason.
# This helps users understand why the version disappeared.
cargo yank --vers 0.1.0 --reason "Critical bug in parser that crashes on empty input"
After yanking, publish a fix as a new patch version. If you yanked 0.1.0, publish 0.1.1. Users can update to the fixed version. The yanked version remains in the registry for historical reasons, but it is hidden from search and dependency resolution.
Yank with a reason. Help your users understand the breakage.
Pitfalls that block your publish
The registry rejects crates with missing metadata. You'll see an error like "missing description in Cargo.toml" if the field is absent. Fix the metadata and try again.
Private dependencies are a common blocker. If your crate depends on other-crate = { path = "../other" }, the registry rejects it. Other users don't have ../other on their machine. You must publish the dependency first or use a registry URL.
# BAD: Private path dependency. Registry rejects this.
[dependencies]
other-crate = { path = "../other" }
# GOOD: Published dependency.
[dependencies]
other-crate = "0.2.0"
The registry also checks for reserved names. You cannot publish a crate with a name that conflicts with an existing crate or a reserved keyword. If you get a name conflict, rename your crate.
Check your dependencies. Private paths break the registry.
Decision: which command for which moment
Use cargo package when you need to verify the tarball contents and build success before touching the network. Use cargo publish when the package validates cleanly and you are ready to release the version. Use cargo yank --vers <VERSION> when a published version contains a critical error and you need to block new downloads while preserving existing dependency graphs. Use cargo publish --allow-dirty when you have uncommitted local changes and still want to publish, though committing first keeps your repository history aligned with releases.
Use the right tool. cargo package verifies; cargo publish releases.