Question

How to Build Multiple Binaries with Cargo in Rust

rustrust-cargo

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 daemon binary from src/bin/daemon.rs
  • a client binary from src/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.rs is 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.

Take Quiz

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.rs
  • src/bin/daemon.rs
  • src/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?

  1. Cargo reads Cargo.toml.
  2. It sees one package named my_app.
  3. It checks src/lib.rs and builds the library target.
  4. It checks src/bin/daemon.rs and builds a binary named daemon.
  5. It checks src/bin/client.rs and builds a binary named client.
  6. Both binaries can call functions from the library crate my_app.

What happens when you run cargo run --bin daemon?

  1. Cargo selects the daemon binary.
  2. It compiles the library if needed.
  3. It compiles src/bin/daemon.rs.
  4. main() starts.
  5. my_app::greet("daemon") calls the library function.
  6. 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
Take Quiz

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.rs
  • src/main.rs
  • src/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]] in Cargo.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

ApproachBest forProsCons
src/main.rs onlyOne executableSimpleCannot naturally represent multiple named programs
src/bin/*.rsMultiple related binaries in one packageEasy, idiomatic, low setupAll binaries share one package dependency set
[[bin]] with custom pathsSpecial file layoutsFlexibleMore configuration, less conventional
Workspace with multiple packagesLarger apps with clearer boundariesSeparate packages, separate dependencies possibleMore files and structure to manage

src/bin/*.rs vs [[bin]]

OptionRecommendation
src/bin/*.rsUse by default
[[bin]]Use when you need nonstandard file locations

Single package vs workspace

ChoiceUse when
Single package with multiple binariesdaemon and client are tightly related and share lots of code
Workspacedaemon and client are more independent or growing into separate projects
Take Quiz

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.rs for 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.

Take Quiz

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

  1. Create a library in src/lib.rs with at least two public functions.
  2. Create a daemon binary in src/bin/daemon.rs.
  3. Create a client binary in src/bin/client.rs.
  4. Make both binaries call functions from the shared library.
  5. Verify that each binary runs independently with Cargo commands.
Take Quiz

Keep learning

Related questions

View more