Publishing a crate takes ten minutes the first time and a single command afterwards, and every mistake in between is permanent. Once 0.1.0 of a crate is on crates.io, nobody can change what 0.1.0 contains, including you. That makes the dry run the most important step and the manifest the most important file. This post takes a small library from nothing to published, shows the output of each command, explains the failures people hit on a first attempt, and sets up token handling that does not put your whole account at risk. Tested with Cargo 1.99.
Before you start
You need a crates.io account (it signs in through GitHub), a verified email address on that account, and a crate with a name nobody has taken. Names are first come, first served, and there is no transfer process you can count on, so check the name early at crates.io/crates/yourname or with cargo search yourname. Pick a name you can live with: renaming later means publishing a new crate and abandoning the old one. Budget about ten minutes, plus the time to write a README worth a reader's attention.
Fill in the manifest
Run a package check on a freshly generated crate and Cargo tells you what is missing:
cargo new --lib textwidth --vcs none
cd textwidth
cargo publish --dry-runwarning: manifest has no description, license, license-file, documentation, homepage or repository
|
= note: see https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more infoWarnings now become errors at upload: crates.io rejects a crate with no description or no license. Fill them in and add the fields that make the crate findable and trustworthy:
[package]
name = "textwidth"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
description = "Measure the display width of a string in terminal columns"
license = "MIT OR Apache-2.0"
repository = "https://github.com/example/textwidth"
readme = "README.md"
keywords = ["unicode", "terminal", "width"]
categories = ["text-processing"]
exclude = [".github/", "benches/data/"]The license value is an SPDX expression, and MIT OR Apache-2.0 is the dual license most of the ecosystem uses, which lets downstream users pick whichever fits. Include the matching LICENSE-MIT and LICENSE-APACHE files in the repository; the manifest field names a license but does not carry its text. Keywords are limited to five and categories must come from the fixed list on crates.io, otherwise the upload is rejected. The metadata deserves more attention than it usually gets, and the manifest guide lists every field.
See what will ship
cargo package --listCargo.lock
Cargo.toml
Cargo.toml.orig
README.md
src/lib.rsRead that list line by line. It is the exact set of files that become your published crate. By default Cargo includes everything tracked in the repository that is not in the ignore rules, so a stray benches/data/10-gigabyte-corpus.txt would be uploaded, and crates.io limits a crate to 10 MB compressed. Use exclude to drop what users do not need, or include to whitelist the few paths that matter for a tiny library. Leaving out your CI configuration, test fixtures and large assets keeps downloads quick for everyone who depends on you.
Prove the packaged copy builds alone
cargo publish --dry-runA dry run packs the files, unpacks them into a temporary directory, and compiles the result as an independent crate. That catches the most common first-publish failure: your code builds in the repository because it reads ../data/words.txt or include_str!s a file you excluded, and the packaged copy does not have it. It also catches the path dependency problem, which is the error everyone meets once:
[dependencies]
unicode-width = "0.2"
textwidth-core = { path = "../textwidth-core", version = "0.1.0" }crates.io accepts only registry dependencies, so a path dependency must carry a version too. Cargo uses the path while you develop and the version when you publish, which means textwidth-core has to be published first and at that version. In a workspace, publish crates in dependency order; the workspace publishing guide covers tooling that does it for you.
Create a token and publish
In your crates.io account settings, create an API token. Scope it: choose the publish-update endpoint scope (the first release of a brand-new name needs publish-new), and limit it to the crate name pattern it needs. A token that can only publish textwidth* cannot be used to take over every crate you own if it leaks. Store it with Cargo's credential provider rather than in a file in the repository:
cargo login
cargo publishcargo login asks for the token on standard input and hands it to the configured credential provider; on a developer machine that is usually the OS keychain. For CI, use the registry's trusted publishing where it is available, or pass the token through CARGO_REGISTRY_TOKEN from the CI secret store, never printed or echoed. Handling that secret correctly is the same discipline described in secrets management for Rust services.
The upload itself takes seconds. The version then appears in the index, and docs.rs starts building documentation, which can take a few minutes. If the docs build fails, there is nothing to fix on crates.io; fix the crate and publish a patch version, and add a [package.metadata.docs.rs] table if the build needs particular features or a target.
Releases you cannot take back
Three rules govern what happens next. A version number is permanent: you cannot republish 0.1.0 with different contents, and you cannot delete it. If it has a serious defect, yank it, which prevents new projects from resolving to it, then publish 0.1.1. Secrets in a published file are exposed forever, because yanking hides a version from resolution but leaves the archive downloadable; rotate any credential that reached the package. And name squatting is discouraged and can be reclaimed only under narrow policy rules, so publish only names you intend to use.
Before the first release, run the checklist: examples compile (cargo test --doc, see documentation tests), public API is deliberate (public API design), and the minimum version is declared (MSRV).
The first release checklist
Run these in order before the real upload, because each catches a failure the next step would only reveal after the version is permanent:
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo doc --no-deps
cargo package --list
cargo publish --dry-runThe first four prove the crate is healthy; the last two prove the packaged tarball is what you intend. Read the package --list output for anything that should not ship (a .env, a fixtures/ directory of 80 MB) and for anything missing, such as the LICENSE files, which a license field alone does not provide. A tag in git matching the version, pushed after the publish succeeds, closes the loop: git tag v0.1.0 gives users and tools a stable reference, and it is the anchor that tools such as cargo semver-checks use as a baseline later.
When publishing is the wrong answer
A crate used by one company's services does not belong on a public registry, since publishing makes it public and permanent. Use a private registry or a git dependency instead, as covered in private registries in Cargo. And a crate that is not ready to support users should stay at 0.0.x or off the registry: a published name with a broken API wastes the next reader's afternoon, and you will be asked to maintain it.



