- Published on
Where a Rust String Literal Actually Lives
- Authors

- Name
- Mehdi Akiki
Article · Through the layers
When you write let s: &str = "hello world";, it looks like one simple thing. But there are really two things in this line, and they live in two different places. New Rust developers mix them up, and that confusion is what makes lifetimes feel hard later. So let me open it up.
Two things in one line
"hello world" is the string literal, the text itself. s is the variable, and it is not the text. It is a pointer to the text, plus a little bookkeeping.
They are not stored in the same place, and they do not live for the same amount of time. Once you see that, a lot of Rust starts to make sense.
Where the bytes go
The bytes of "hello world" are known at compile time, so the compiler does not wait until the program runs to deal with them. It writes them straight into the compiled binary, into the read-only data section (you often see it written as .rodata).
So the text is not on the stack and not on the heap. Nothing is allocated while the program runs. The bytes are just part of the program file, like the machine code, and the operating system loads them when it loads the program.
Because they are part of the program, they are there from the first instruction to the last. They live for the whole run. Nobody frees them, because there is nothing to free.
They are also read-only. That section is mapped as read-only memory, so writing to a literal would be a crash, not a silent edit. This is why a &str from a literal is an immutable view.
What the variable actually is
Now s. It does live on the stack, like any normal local. But it is small, and it does not hold the text. A &str is a fat pointer, two words wide:
- the address where the bytes start, inside that read-only section
- the length, how many bytes the string has
let s: &str = "hello world";
// s is two machine words on the stack:
// pointer -> address of 'h' in .rodata
// length -> 11
So s points into the binary and remembers the length. That is all it is. Copying s copies these two words, not the eleven bytes.
s follows the normal scope rules: it lives from its declaration to the end of the function or block. When the function returns, the stack slot for s goes away. But the bytes it pointed at do not, because they were never on the stack.
Why the lifetime is 'static
This is where it connects to lifetimes. A reference is only valid for as long as the thing it points to is valid, and the compiler tracks that with lifetimes.
The bytes of a literal are valid for the whole program, so a reference to them is valid for the whole program too. Rust calls that longest possible lifetime 'static. That is why every string literal is &'static str by default. You do not write the lifetime, the compiler fills it in, but it is always there.
let s: &'static str = "hello world"; // same as &str, the 'static is implied
Why you can return it, but not a stack reference
This is the practical payoff. Look at two functions.
fn good() -> &'static str {
"hello world" // fine: the bytes live for the whole program
}
This compiles. The returned reference points at something that outlives the call, so nothing dangles after good returns.
fn bad<'a>() -> &'a str {
let local = String::from("hello world");
&local // error: returns a reference to data owned by the current function
}
This does not. local is a String, which owns memory on the heap. When bad returns, local is dropped and that memory is freed, so the reference would point at memory that is no longer yours. Rust refuses this at compile time. In many other languages it would be a use-after-free bug you only find in production.
(I wrote the lifetime out as <'a> here on purpose. Without it Rust stops even earlier and asks where the reference is supposed to come from, since there is no input to borrow from. The explicit lifetime gets us to the real point: the data is gone.)
So the difference is not the &str syntax. It is where the data lives and how long. A literal lives forever, so a reference to it can travel anywhere. A local lives until the function ends, so a reference to it cannot escape.
A quick word on String vs &str
&str is a borrowed view of text that someone else owns, and for a literal that someone is the binary itself. String is the other case: it owns its bytes on the heap, and they are freed when the String is dropped. You reach for String when the text is built or changed at runtime, and you get &str for free when the text is fixed at compile time.
The literal case is just the simplest one, and it hides the most interesting detail. The text was never really inside your program at runtime. It was baked into the program itself.