Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

Rust Cow: Clone Only When You Actually Have To

Sean

Platform Writer

Aug 30, 2026
8 min read

Cow is an enum with two variants: Borrowed and Owned. It lets a function return borrowed data when nothing changed and owned data when it did, without the caller needing to know which.

Rust Cow: Clone Only When You Actually Have To

The classic case is a sanitising function. Most inputs need no changes, so allocating a new String for every one of them is waste — but the signature has to return something, and you cannot return a reference when some inputs do require a new string.

Cow resolves that. It also gets reached for in places where it adds complexity for no measurable gain, so it is worth being clear about both.

Table of contents

The shape of it

use std::borrow::Cow;

pub enum Cow<'a, B: ToOwned + ?Sized> {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

In practice you mostly see Cow<'a, str>, which is either a &'a str or a String. Also common: Cow<'a, [T]> for slice or Vec<T>, and Cow<'a, Path>.

It implements Deref, so you can call the borrowed type’s methods directly without matching on the variant:

let c: Cow<str> = Cow::Borrowed("deploy");
println!("{}", c.len());          // 6
println!("{}", c.to_uppercase()); // DEPLOY

That is what makes it usable. Callers treat it as the underlying type and only care about the variant if they want to avoid an allocation.

The case it is designed for

A function that usually returns its input unchanged:

use std::borrow::Cow;

fn escape_html(input: &str) -> Cow<'_, str> {
    if !input.contains(['<', '>', '&']) {
        return Cow::Borrowed(input);          // no allocation
    }

    let mut out = String::with_capacity(input.len() + 16);
    for c in input.chars() {
        match c {
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '&' => out.push_str("&amp;"),
            _   => out.push(c),
        }
    }
    Cow::Owned(out)
}

If 95% of inputs contain no special characters — which for most real text is about right — this eliminates 95% of the allocations that returning String would cause.

That is the whole argument, and it is a good one on a hot path. The caller uses the result as a string either way:

let safe = escape_html(user_input);
println!("{}", safe);              // Deref, no matching needed

The standard library uses this pattern itself — String::from_utf8_lossy returns a Cow<str>, borrowing when the input was already valid UTF-8 and allocating only when replacement characters are needed.

to_mut and into_owned

Two methods do the actual work of upgrading:

let mut c: Cow<str> = Cow::Borrowed("deploy");

// to_mut clones ONLY if currently Borrowed, then gives a &mut
c.to_mut().make_ascii_uppercase();
assert_eq!(c, Cow::Owned(String::from("DEPLOY")));

// into_owned always yields an owned value, cloning if needed
let owned: String = c.into_owned();

to_mut is the clone-on-write mechanism itself: it converts Borrowed to Owned in place and returns a mutable reference. If already Owned, it just hands back the reference with no allocation.

into_owned consumes the Cow and gives you the owned type — free if it was Owned, a clone if it was Borrowed. Use it at the boundary where you need to store the value and the borrow cannot be held.

There is a subtle trap in loops. Calling to_mut() repeatedly is fine — only the first call allocates — but calling into_owned() inside a loop clones every iteration. Hoist it out.

Accepting either type in an argument

The other genuinely useful pattern, letting a caller pass either without forcing an allocation:

fn log<'a>(message: impl Into<Cow<'a, str>>) {
    let message: Cow<'a, str> = message.into();
    println!("[log] {}", message);
}

log("static message");                  // borrowed, no allocation
log(format!("id={}", 42));              // owned, moved in, no extra clone

Compare the alternatives. fn log(m: &str) forces the caller with a String to pass &s and then, if you needed to store it, you clone. fn log(m: String) forces the caller with a literal to allocate. impl Into<Cow<str>> accepts both optimally.

This shows up in library APIs where you cannot predict which the caller has. In application code, &str is usually fine and simpler.

When not to use it

Cow has a real cost: it appears in your public signatures, it carries a lifetime parameter that propagates to any struct holding it, and it makes the code less obvious to read.

Skip it when:

  • Most inputs need modification anyway. If 80% of calls allocate, the branch is overhead with no payoff. Just return String.
  • The code is not hot. Startup configuration parsing does not need this.
  • The lifetime is inconvenient. A Cow<'a, str> in a struct means a lifetime parameter on the struct, which propagates. Owning a String is often worth the allocation.
  • You have not measured. This is a targeted optimisation, and treating it as a default is how a codebase acquires lifetime parameters it does not need.

The honest summary: Cow is excellent in library APIs and hot data paths, and over-engineering nearly everywhere else. String is the right default, and you upgrade when a profile says the allocation matters.

Where it pays off

The realistic wins are the paths that run on every request: header parsing, path normalisation, input sanitising, template rendering, deserialisation. In those, the majority of values pass through unmodified and the allocations you avoid are per-request rather than per-startup.

Serde makes a related trade worth knowing: #[serde(borrow)] lets a deserialised struct borrow from the input buffer rather than allocating a String per field. Same principle, and it can substantially reduce allocation in a JSON-heavy service — with the same constraint that the struct then cannot outlive the buffer.

Rust services doing this work deploy on RunxBuild from a repository as a Docker service, with a build log, a live route, runtime logs and metrics — which is where you find out whether the path you optimised is actually the hot one.

How this fits the rest of the stack

Cow gives you one return type that borrows when nothing changed and allocates when it did, and on a path where most inputs pass through unmodified that is a real saving. It is not a default: it puts a lifetime in your signature and complexity in your code, so reach for it in library APIs and measured hot paths, and use String everywhere else. The RunxBuild hosting calculator covers what the service you are optimising costs to run.

Useful related references:

FAQ

What is Cow in Rust?

A smart pointer enum in std::borrow with Borrowed and Owned variants. It holds either a reference or an owned value and clones only when mutation is required, which lets a function avoid allocating for inputs it does not need to change.

When should I use Cow?

When a function usually returns its input unchanged but sometimes must produce a modified version, and the path is hot enough for the allocations to matter. Also in library APIs via impl Into<Cow<str>>, so callers with either a &str or a String avoid an unnecessary clone.

What is the difference between to_mut and into_owned?

to_mut converts Borrowed to Owned in place and returns a mutable reference, cloning only on the first call. into_owned consumes the Cow and returns the owned value, cloning if it was borrowed. Do not call into_owned inside a loop.

Does Cow have runtime overhead?

A small one: it is an enum, so there is a discriminant and a branch on access. That is far cheaper than an allocation, so it wins whenever a meaningful share of calls avoid allocating, and loses when nearly all of them allocate anyway.

Should I use Cow everywhere instead of String?

No. Cow adds a lifetime parameter that propagates through any struct holding it and makes code harder to read. Use String by default and switch to Cow where profiling shows the allocation matters.

#rust cow#clone on write#std::borrow#Rust performance#Rust