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 and keep only plus files in .
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
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 named .
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 :
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
}
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]]
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"
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?
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
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Default Function Arguments in Rust: What to Use Instead
Learn how Rust handles default function arguments, why they are not supported, and practical patterns to achieve similar behavior.
Fixing Rust "linker 'cc' not found" on Debian in WSL
Learn why Rust shows "linker 'cc' not found" on Debian in WSL and how to fix it by installing the required C build tools.