What is the difference between dev-dependencies and dependencies

Dependencies are for runtime use, while dev-dependencies are only for building and testing your Rust project.

The invisible boundary

You publish a crate. The tests pass. You feel good. A week later, a user opens an issue. Their project fails to compile the moment they add your crate. The error points to a type in your public API that references a testing library you used for mocking. The problem isn't your logic. The problem is your Cargo.toml. You put a development tool in [dependencies], or worse, you used a [dev-dependencies] crate inside your library code. Cargo treats these two sections differently. One section ships with your code. The other stays on your machine. Confusing them breaks downstream users instantly. Treat the split as a hard contract. Your library promises to run with its dependencies. It makes no promises about its dev-dependencies.

Ingredients versus tools

Think of your project like a bakery. The flour, sugar, and eggs are your dependencies. They go into the product. Every customer who buys a cake gets the flour and sugar inside it. If you forget the eggs, the cake doesn't exist. The oven, the whisk, and the food thermometer are your dev-dependencies. You need them to make the cake. You need them to check if the cake is done. But you don't weld the oven to the cake and ship it to the customer. The customer doesn't need the whisk to eat the cake. They just need the cake. In Rust, [dependencies] are the ingredients. They are required for your code to run. [dev-dependencies] are the tools. They are required to build, test, and benchmark your code, but they are stripped away before anyone else uses your library. Keep the kitchen tools in the kitchen.

How Cargo draws the line

Here is the smallest case: a Cargo.toml that splits runtime needs from testing needs.

# Cargo.toml
[package]
name = "bakery-crate"
version = "0.1.0"

[dependencies]
# serde is needed at runtime to serialize data.
# The derive feature pulls in a proc-macro for compile-time code generation.
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
# criterion is only used for benchmarks.
# It provides a statistical harness that runs outside the final binary.
criterion = "0.5"
# serde_json is only used in tests to verify serialization.
# Tests need it to parse expected outputs, but the library doesn't.
serde_json = "1.0"

When you run cargo test, Cargo makes both sections available. Your tests can import criterion and serde_json. Your library code can import serde. When you run cargo bench, Cargo makes both sections available. Benchmarks often need the runtime crates plus the benchmarking harness. The magic happens when someone else depends on bakery-crate. Cargo only downloads serde. It never touches criterion or serde_json. Your downstream user's build is faster, lighter, and free of your testing tools. Cargo enforces this split at compile time. It builds a dependency graph for your target. If the target is a library or binary, it only pulls from [dependencies]. If the target is a test or benchmark, it pulls from both. The compiler never sees the dev-dependencies unless you explicitly ask for them. Trust the target flag. Cargo knows exactly which crates belong in which build.

The library trap

The most common mistake happens when you write a library and accidentally use a dev-dependency in your public API. This works on your machine because cargo test and cargo run both load dev-dependencies. It fails for everyone else.

// src/lib.rs
use mockall::automock;

// This struct is part of your public API.
// The macro expands into trait implementations that live in the final binary.
#[automock]
pub struct DatabaseClient {
    // connection details omitted for brevity
    // The struct fields are private, but the mock trait is public.
}

If mockall is in [dev-dependencies], this code compiles when you test it. It fails when a user adds bakery-crate to their project. The user doesn't have mockall. The compiler rejects the import with E0432 (unresolved import). The message points to mockall and says it can't find the crate. This error is Cargo protecting your users. It simulates the downstream environment. If the crate isn't in [dependencies], the compiler assumes it doesn't exist for library code. Don't leak your test harness into your production API. If a type or function requires a dev-dependency, it belongs in a test module, not in src/lib.rs.

Another trap is forgetting to add a dependency when you use a crate in both tests and production. You write code that uses a new crate. You add it to [dev-dependencies] because you're writing tests. You forget to add it to [dependencies]. Your tests pass. You publish. Users fail. This happens when you use a crate like rand to generate test data, and you also use rand in your application logic. If you only add rand to [dev-dependencies], the production code breaks for users. The fix is checking your imports. If src/lib.rs or src/main.rs imports a crate, that crate must be in [dependencies]. If only tests/ or benches/ import it, it can stay in [dev-dependencies]. Keep your dependency graph clean. Audit your imports before you publish.

Feature flags add another layer of complexity. Features defined in [dependencies] propagate to downstream users. Features defined in [dev-dependencies] stay local. If you enable a heavy feature for a testing crate, it won't bloat your users' builds. If you enable a heavy feature for a runtime crate, it will. Convention aside: The community expects testing frameworks like mockall, proptest, and criterion to live in [dev-dependencies]. If you see a crate with criterion in [dependencies], it's a signal that the author hasn't thought about downstream build times. Read the feature list like a menu. Only order what the runtime actually eats.

The proc-macro exception

There is one major exception that trips up beginners: procedural macros. Proc-macros run at compile time, not runtime. They generate code. They belong in [dependencies].

[dependencies]
# serde_derive is a proc-macro. It runs at compile time.
# It must be a dependency so downstream users can derive traits.
# The generated code lives in the user's binary, not in your tests.
serde = { version = "1.0", features = ["derive"] }

The serde crate uses the derive feature to pull in serde_derive. When you write #[derive(Serialize)], the macro runs during compilation and generates the serialization code. That generated code lives in your binary. It runs at runtime. If serde_derive were a dev-dependency, the macro would only run for you. Your users wouldn't get the generated code. Their builds would fail with trait bound errors. The macro must be available to anyone who uses your crate. The rule is simple: if the macro generates code that is part of the compiled output, the macro is a dependency. If the macro only generates test code or is used in build scripts, it can be a dev-dependency. Most derive macros are dependencies. Put macros where the generated code lives.

Documentation tests follow the same rule. When you run cargo test --doc, Cargo compiles the code blocks in your /// comments. Those examples often need extra crates. If your doc comments show how to use serde_json to parse a response, serde_json must be in [dev-dependencies] so the doc tests compile. The examples run in an isolated scope. They don't affect your library's public API. They just prove your documentation works. Keep doc-test helpers in the dev section. Let the compiler verify your examples without shipping them.

When to pick which

Use [dependencies] for crates your code calls at runtime. If a function in src/lib.rs or src/main.rs invokes a method from the crate, it belongs here.

Use [dependencies] for procedural macros that generate code used in the final binary. Derive macros like serde_derive or tokio::macros go here because the generated code is part of the runtime.

Use [dev-dependencies] for testing frameworks like criterion, proptest, or rstest. These tools run during cargo test or cargo bench and are never needed by downstream users.

Use [dev-dependencies] for mocking libraries like mockall or wiremock. Mocks replace real implementations during testing. Shipping mocks to users breaks their code or adds unnecessary weight.

Use [dev-dependencies] for helper crates that simplify test setup. If a crate provides fixtures, test data generators, or assertion helpers, it stays in dev-dependencies.

Use [dev-dependencies] for documentation examples that require extra tools. If your README or doc comments show code that uses a specific testing harness, put that harness in dev-dependencies so cargo test --doc works.

Where to go next