Question
I want to create a Rust project that contains both a daemon and a client which communicate through a Unix socket.
Because a daemon and a client are separate programs, I need Cargo to build two different binaries from two different source files.
I would also like to keep the main daemon logic in a library crate, and then have a small binary wrapper around it that handles socket communication and startup.
In other words, I am looking for a project structure similar to this:
├── Cargo.toml
├── target
│ └── debug
│ ├── daemon
│ └── client
└── src
├── daemon
│ ├── bin
│ │ └── main.rs
│ └── lib
│ └── lib.rs
└── client
└── bin
└── main.rs
How should I structure this in Cargo so that both binaries are built correctly?
I could combine both concerns into a single executable, but that is not what I want unless that is considered the best practice.
Short Answer
By the end of this page, you will understand how Cargo handles multiple binary targets, how to organize a Rust project with src/main.rs, src/bin/, and src/lib.rs, and when to use a single package versus a workspace. You will also see practical layouts for a daemon/client application where both binaries can share common code.
Concept
In Rust, Cargo can build more than one binary from the same project. This is useful when your application naturally consists of multiple executables, such as:
- a daemon and a client
- a server and an admin tool
- a CLI plus helper utilities
- a main app plus migration scripts
The key idea is that a Cargo package can contain:
- one library target in
src/lib.rs - one default binary target in
src/main.rs - additional binary targets in
src/bin/
Each file inside src/bin/ becomes a separate executable.
For example:
src/
├── lib.rs
├── main.rs
└── bin/
├── daemon.rs
└── client.rs
This package can produce:
- a library crate from
src/lib.rs - a default binary from
src/main.rs - a
daemonbinary fromsrc/bin/daemon.rs - a
clientbinary fromsrc/bin/client.rs
If you do not need a default binary, you can omit src/main.rs and keep only src/lib.rs plus files in src/bin/.
This matters because it lets you:
- keep shared logic in a reusable library
- keep each executable small and focused
- avoid duplicating code between programs
- compile and run binaries independently
For a daemon/client design, the most common approach is:
- put shared logic in
src/lib.rs - put daemon startup code in
src/bin/daemon.rs - put client startup code in
src/bin/client.rs
That is usually simpler and more idiomatic than nesting custom daemon/bin/main.rs and client/bin/main.rs folders inside src/.
Mental Model
Think of a Cargo package like a workshop.
src/lib.rsis the toolbox shared by everyone.- each file in
src/bin/is a different worker using tools from that toolbox. - Cargo is the manager that knows how to build each worker into its own executable.
So instead of cramming daemon logic and client logic into one big program, you let each binary do its own job while both reuse the same library code.
A good mental shortcut is:
- library = shared brain
- binary = entry point
The daemon binary starts the service. The client binary sends commands. Both can call the same Rust library code.
Syntax and Examples
The simplest Cargo layout for multiple binaries is:
my_app/
├── Cargo.toml
└── src/
├── lib.rs
└── bin/
├── daemon.rs
└── client.rs
Basic example
Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
src/lib.rs
pub fn run_daemon() {
println!("daemon logic is running");
}
pub fn run_client() {
println!("client logic is running");
}
src/bin/daemon.rs
fn main() {
my_app::run_daemon();
}
src/bin/client.rs
fn main() {
my_app::run_client();
}
Build and run
Build all targets:
cargo build
Run the daemon:
cargo run --bin daemon
Run the client:
cargo run --bin client
Alternative: explicit [[bin]] entries
If you want to place binaries in custom paths, you can declare them in Cargo.toml:
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "daemon"
path = "src/daemon/bin/main.rs"
[[bin]]
name = "client"
path = "src/client/bin/main.rs"
And then you might have:
src/
├── daemon/
│ └── bin/
│ └── main.rs
└── client/
└── bin/
└── main.rs
If you also want shared code, it is still more idiomatic to place that shared code in src/lib.rs or in modules used by src/lib.rs.
Which approach should you choose?
For most projects, prefer:
src/lib.rssrc/bin/daemon.rssrc/bin/client.rs
Use explicit [[bin]] entries only when you have a strong reason for a custom file layout.
Step by Step Execution
Consider this example:
// src/lib.rs
pub fn greet(name: &str) {
println!("Hello, {name}!");
}
// src/bin/client.rs
fn main() {
my_app::greet("client");
}
// src/bin/daemon.rs
fn main() {
my_app::greet("daemon");
}
What happens when you run cargo build?
- Cargo reads
Cargo.toml. - It sees one package named
my_app. - It checks
src/lib.rsand builds the library target. - It checks
src/bin/daemon.rsand builds a binary nameddaemon. - It checks
src/bin/client.rsand builds a binary namedclient. - Both binaries can call functions from the library crate
my_app.
What happens when you run cargo run --bin daemon?
- Cargo selects the
daemonbinary. - It compiles the library if needed.
- It compiles
src/bin/daemon.rs. main()starts.my_app::greet("daemon")calls the library function.- The program prints:
Hello, daemon!
Why this is useful
The daemon and client each have their own main() function, but they do not need to duplicate shared logic. That shared logic lives in the library crate.
Real World Use Cases
Multiple binaries in one Cargo package are common in real Rust projects.
Common scenarios
-
Daemon + client
- a background service listens on a Unix socket
- a client program sends commands to it
-
Server + admin tool
- one binary runs the web server
- another binary performs maintenance tasks
-
CLI + migration utility
- one binary is the main application
- another updates databases or generates files
-
Worker + scheduler
- one binary consumes jobs
- another submits or manages jobs
Why shared libraries help
A shared library avoids repeating:
- configuration parsing
- protocol types
- validation logic
- error types
- utility functions
For your daemon/client example, both programs may share:
- socket path constants
- request and response enums
- serialization code
- error handling helpers
Real Codebase Usage
In real projects, developers usually separate responsibilities like this:
Typical pattern
src/lib.rs- core business logic
- protocol types
- shared utilities
- configuration loading
src/bin/daemon.rs- parse CLI args
- initialize logging
- bind Unix socket
- call library functions
src/bin/client.rs- parse CLI args
- connect to socket
- send requests
- display responses
Common patterns
Guard clauses
Binaries often validate inputs early:
fn main() {
let socket_path = "/tmp/myapp.sock";
if socket_path.is_empty() {
eprintln!("socket path cannot be empty");
std::process::exit(1);
}
my_app::run_daemon();
}
Error handling
Many binaries return Result from main:
fn main() -> Result<(), Box<dyn std::error::Error>> {
my_app::run_client()?;
Ok(())
}
Shared protocol types
A library can define messages used by both binaries:
pub enum Command {
Status,
Shutdown,
}
This reduces bugs because the daemon and client use the same definitions.
When to use a workspace instead
Use a workspace if the daemon and client are large enough to deserve separate packages, or if you want:
- independent dependencies
- independent versions
- clearer package boundaries
But if they are closely related and share code heavily, a single package with multiple binaries is often the best starting point.
Common Mistakes
1. Creating custom folders and expecting Cargo to detect them automatically
Cargo automatically understands:
src/lib.rssrc/main.rssrc/bin/*.rs
It does not automatically treat folders like src/daemon/bin/main.rs as binaries unless you declare them explicitly.
Broken expectation
src/
└── daemon/
└── bin/
└── main.rs
Cargo will not automatically build this as a binary.
Fix
Either:
- move it to
src/bin/daemon.rs, or - declare it with
[[bin]]inCargo.toml
2. Duplicating shared logic in each binary
Less ideal
// src/bin/daemon.rs
fn parse_config() {
// duplicated logic
}
// src/bin/client.rs
fn parse_config() {
// duplicated logic again
}
Better
// src/lib.rs
pub fn parse_config() {
// shared logic
}
3. Using src/main.rs when you really want named binaries
If you only use src/main.rs, Cargo creates one default binary named after the package. That does not give you separate daemon and client executables.
4. Forgetting how to run a specific binary
When a package has multiple binaries, this may fail:
cargo run
Cargo may not know which binary you want.
Use:
cargo run --bin daemon
cargo run --bin client
5. Putting all logic in main()
Beginners often make the binary file too large.
Better structure
- keep
main()small - move reusable logic into
lib.rs - keep binaries focused on startup and I/O
Comparisons
| Approach | Best for | Pros | Cons |
|---|---|---|---|
src/main.rs only | One executable | Simple | Cannot naturally represent multiple named programs |
src/bin/*.rs | Multiple related binaries in one package | Easy, idiomatic, low setup | All binaries share one package dependency set |
[[bin]] with custom paths | Special file layouts | Flexible | More configuration, less conventional |
| Workspace with multiple packages | Larger apps with clearer boundaries | Separate packages, separate dependencies possible | More files and structure to manage |
src/bin/*.rs vs [[bin]]
| Option | Recommendation |
|---|---|
src/bin/*.rs | Use by default |
[[bin]] | Use when you need nonstandard file locations |
Single package vs workspace
| Choice | Use when |
|---|---|
| Single package with multiple binaries | daemon and client are tightly related and share lots of code |
| Workspace | daemon and client are more independent or growing into separate projects |
Cheat Sheet
Cargo multiple binaries quick reference
Default conventions
- Library:
src/lib.rs - Default binary:
src/main.rs - Extra binaries:
src/bin/*.rs
Example layout
src/
├── lib.rs
└── bin/
├── daemon.rs
└── client.rs
Run commands
cargo build
cargo run --bin daemon
cargo run --bin client
Share code between binaries
Put shared functions, types, and modules in:
// src/lib.rs
pub fn helper() {}
Use them from binaries:
fn main() {
my_app::helper();
}
Custom binary paths
[[bin]]
name = "daemon"
path = "src/daemon/bin/main.rs"
Good practice
- keep
main()small - use
src/lib.rsfor reusable logic - use
src/bin/for separate executables - choose a workspace only when package boundaries matter
Common gotcha
Cargo does not automatically detect arbitrary nested binary paths unless you configure them.
FAQ
Can Cargo build more than one binary in the same project?
Yes. Put extra binaries in src/bin/ or define them explicitly with [[bin]] in Cargo.toml.
How do I share code between a daemon and a client in Rust?
Put the shared code in src/lib.rs, then call it from both binaries.
Should I use src/bin/ or a workspace?
Use src/bin/ for closely related executables in one package. Use a workspace when they should be separate packages.
Does Cargo automatically detect src/daemon/bin/main.rs?
No. Cargo only auto-detects standard locations like src/main.rs, src/lib.rs, and src/bin/*.rs.
How do I run one specific binary when there are multiple?
Use cargo run --bin <name>, for example cargo run --bin daemon.
Can a package have both a library and multiple binaries?
Yes. This is a common and idiomatic Rust project structure.
Is it good practice to keep daemon logic in a library?
Yes. It keeps the binary entry point small and makes the core logic easier to test and reuse.
Mini Project
Description
Build a small Rust application with two binaries: one acts as a daemon-like process and the other acts as a client command. Both binaries will share logic from a library crate. This mirrors a common real-world setup where separate executables use the same core code.
Goal
Create a Cargo project that builds daemon and client binaries, both using shared functions from src/lib.rs.
Requirements
- Create a library in
src/lib.rswith at least two public functions. - Create a
daemonbinary insrc/bin/daemon.rs. - Create a
clientbinary insrc/bin/client.rs. - Make both binaries call functions from the shared library.
- Verify that each binary runs independently with Cargo commands.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.