How do I convert integers to strings fast with itoa in Rust?
Walkthrough
The itoa crate provides ultra-fast integer-to-string conversion in Rust. While std::format!("{}", n) or n.to_string() work fine for most cases, they allocate a new String each time. itoa writes directly to a provided buffer without allocation, making it significantly faster for hot paths. It's designed for scenarios where you're converting many integers—like formatting JSON, CSV output, logging, or building strings incrementally. The crate uses carefully optimized lookup tables and algorithms to minimize CPU cycles.
Key concepts:
- No allocation — writes to caller-provided buffer
- itoa::Buffer — reusable buffer for repeated conversions
- fmt::Display — integrates with standard formatting traits
- Write trait — can write to any
std::fmt::Writeorstd::io::Write - Performance — 2-10x faster than
to_string()for integers
Code Example
# Cargo.toml
[dependencies]
itoa = "1"use itoa;
fn main() {
let mut buffer = itoa::Buffer::new();
let printed = buffer.format(12345);
println!("{}", printed); // "12345"
}Basic Usage
use itoa;
fn main() {
// Create a reusable buffer
let mut buffer = itoa::Buffer::new();
// Format various integer types
let s1 = buffer.format(42i32);
println!("i32: {}", s1);
let s2 = buffer.format(-123i64);
println!("i64: {}", s2);
let s3 = buffer.format(9876543210u64);
println!("u64: {}", s3);
let s4 = buffer.format(0usize);
println!("usize: {}", s4);
// Buffer is reused - each call overwrites previous
println!("Reused: {}", buffer.format(999));
}Supported Integer Types
use itoa;
fn main() {
let mut buffer = itoa::Buffer::new();
// Signed integers
let _: &str = buffer.format(42i8);
let _: &str = buffer.format(42i16);
let _: &str = buffer.format(42i32);
let _: &str = buffer.format(42i64);
let _: &str = buffer.format(42i128);
let _: &str = buffer.format(42isize);
// Unsigned integers
let _: &str = buffer.format(42u8);
let _: &str = buffer.format(42u16);
let _: &str = buffer.format(42u32);
let _: &str = buffer.format(42u64);
let _: &str = buffer.format(42u128);
let _: &str = buffer.format(42usize);
// Negative numbers
println!("Negative: {}", buffer.format(-9876543210i64));
// Edge cases
println!("Min i32: {}", buffer.format(i32::MIN));
println!("Max u64: {}", buffer.format(u64::MAX));
}Writing to Strings
use itoa;
fn main() {
let mut buffer = itoa::Buffer::new();
// Build a string incrementally
let mut output = String::new();
for i in 0..5 {
output.push_str(buffer.format(i));
output.push(',');
}
println!("Numbers: {}", output.trim_end_matches(','));
}Writing to Writers
use itoa;
use std::fmt::Write;
use std::io::Write as IoWrite;
fn main() -> std::io::Result<()> {
// Write to a String (std::fmt::Write)
let mut s = String::new();
itoa::fmt(&mut s, 12345)?;
println!("Written to string: {}", s);
// Write to stdout (std::io::Write)
let stdout = std::io::stdout();
let mut handle = stdout.lock();
itoa::write(&mut handle, 67890)?;
println!(); // newline
Ok(())
}Performance Comparison
use itoa;
fn benchmark_to_string(count: u32) -> u128 {
let start = std::time::Instant::now();
let mut sum = 0u64;
for i in 0..count {
let s = i.to_string();
sum += s.len() as u64;
}
std::hint::black_box(sum);
start.elapsed().as_nanos()
}
fn benchmark_itoa(count: u32) -> u128 {
let start = std::time::Instant::now();
let mut buffer = itoa::Buffer::new();
let mut sum = 0u64;
for i in 0..count {
let s = buffer.format(i);
sum += s.len() as u64;
}
std::hint::black_box(sum);
start.elapsed().as_nanos()
}
fn main() {
let count = 1_000_000;
println!("Converting {} integers:", count);
println!(" to_string(): {} ns", benchmark_to_string(count));
println!(" itoa: {} ns", benchmark_itoa(count));
}Building CSV Output
use itoa;
fn write_csv_row(values: &[i64]) -> String {
let mut buffer = itoa::Buffer::new();
let mut row = String::new();
for (i, &value) in values.iter().enumerate() {
if i > 0 {
row.push(',');
}
row.push_str(buffer.format(value));
}
row
}
fn main() {
let data = [
vec![1, 10, 100],
vec![2, 20, 200],
vec![3, 30, 300],
];
for row in &data {
println!("{}", write_csv_row(row));
}
}Building JSON Arrays
use itoa;
fn write_json_array(numbers: &[u32]) -> String {
let mut buffer = itoa::Buffer::new();
let mut json = String::from("[");
for (i, &num) in numbers.iter().enumerate() {
if i > 0 {
json.push_str(", ");
}
json.push_str(buffer.format(num));
}
json.push(']');
json
}
fn main() {
let numbers = vec![10, 20, 30, 40, 50];
println!("{}", write_json_array(&numbers));
}Custom Number Formatting
use itoa;
fn format_with_units(value: i64, unit: &str) -> String {
let mut buffer = itoa::Buffer::new();
let mut result = String::new();
result.push_str(buffer.format(value));
result.push_str(unit);
result
}
fn format_percentage(value: f64, total: f64) -> String {
let percentage = (value / total * 100.0) as i64;
let mut buffer = itoa::Buffer::new();
let mut result = String::new();
result.push_str(buffer.format(percentage));
result.push('%');
result
}
fn main() {
println!("{}", format_with_units(1024, " bytes"));
println!("{}", format_percentage(75.0, 100.0));
}Logging Numbers
use itoa;
use std::fmt::Write;
struct FastLogger {
buffer: itoa::Buffer,
output: String,
}
impl FastLogger {
fn new() -> Self {
FastLogger {
buffer: itoa::Buffer::new(),
output: String::new(),
}
}
fn log(&mut self, level: &str, code: i32, message: &str) {
self.output.clear();
let _ = write!(&mut self.output, "[{}] ", level);
let _ = write!(&mut self.output, "code={}: ", self.buffer.format(code));
let _ = write!(&mut self.output, "{}", message);
println!("{}", self.output);
}
}
fn main() {
let mut logger = FastLogger::new();
logger.log("INFO", 200, "Request processed");
logger.log("WARN", 429, "Rate limited");
logger.log("ERROR", 500, "Internal error");
}HTTP Response Building
use itoa;
fn build_http_response(status: u16, content_length: usize) -> String {
let mut buffer = itoa::Buffer::new();
let mut response = String::new();
response.push_str("HTTP/1.1 ");
response.push_str(buffer.format(status as u32));
response.push_str(" OK\r\n");
response.push_str("Content-Length: ");
response.push_str(buffer.format(content_length as u32));
response.push_str("\r\n\r\n");
response
}
fn main() {
let body = "Hello, World!";
let response = build_http_response(200, body.len());
println!("{}{}", response, body);
}Progress Reporting
use itoa;
struct ProgressBar {
buffer: itoa::Buffer,
width: usize,
}
impl ProgressBar {
fn new(width: usize) -> Self {
ProgressBar {
buffer: itoa::Buffer::new(),
width,
}
}
fn render(&mut self, current: usize, total: usize) -> String {
let mut output = String::new();
let percent = if total == 0 { 0 } else { (current * 100) / total };
let filled = if total == 0 { 0 } else { (current * self.width) / total };
output.push('[');
for i in 0..self.width {
if i < filled {
output.push('=');
} else {
output.push(' ');
}
}
output.push(']');
output.push(' ');
output.push_str(self.buffer.format(percent as u32));
output.push('%');
output.push_str(" (");
output.push_str(self.buffer.format(current as u32));
output.push('/');
output.push_str(self.buffer.format(total as u32));
output.push(')');
output
}
}
fn main() {
let mut progress = ProgressBar::new(20);
for i in 0..=10 {
println!("{}", progress.render(i, 10));
}
}Table Formatting
use itoa;
struct TableBuilder {
buffer: itoa::Buffer,
columns: Vec<String>,
rows: Vec<Vec<i64>>,
}
impl TableBuilder {
fn new(columns: Vec<&str>) -> Self {
TableBuilder {
buffer: itoa::Buffer::new(),
columns: columns.iter().map(|s| s.to_string()).collect(),
rows: Vec::new(),
}
}
fn add_row(&mut self, values: Vec<i64>) {
self.rows.push(values);
}
fn render(&mut self) -> String {
let mut output = String::new();
// Header
output.push('|');
for col in &self.columns {
output.push_str(&format!(" {:^10} |", col));
}
output.push('\n');
// Separator
output.push('|');
for _ in &self.columns {
output.push_str("------------|");
}
output.push('\n');
// Rows
for row in &self.rows {
output.push('|');
for value in row {
output.push_str(&format!(" {:>10} |", self.buffer.format(*value)));
}
output.push('\n');
}
output
}
}
fn main() {
let mut table = TableBuilder::new(vec!["ID", "Count", "Total"]);
table.add_row(vec![1, 100, 1000]);
table.add_row(vec![2, 200, 2000]);
table.add_row(vec![3, 300, 3000]);
println!("{}", table.render());
}Batch Processing
use itoa;
fn process_ids(ids: &[u64]) -> Vec<String> {
let mut buffer = itoa::Buffer::new();
ids.iter()
.map(|&id| buffer.format(id).to_string())
.collect()
}
// For even better performance, write to single buffer
fn join_ids(ids: &[u64], separator: &str) -> String {
let mut buffer = itoa::Buffer::new();
let mut result = String::new();
for (i, &id) in ids.iter().enumerate() {
if i > 0 {
result.push_str(separator);
}
result.push_str(buffer.format(id));
}
result
}
fn main() {
let ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let strings = process_ids(&ids);
println!("As vector: {:?}", strings);
let joined = join_ids(&ids, ", ");
println!("Joined: {}", joined);
}Embedded in Custom Format
use itoa;
struct Coordinate {
x: i32,
y: i32,
z: i32,
}
impl Coordinate {
fn format(&self, buffer: &mut itoa::Buffer) -> String {
let mut s = String::from("(");
s.push_str(buffer.format(self.x));
s.push_str(", ");
s.push_str(buffer.format(self.y));
s.push_str(", ");
s.push_str(buffer.format(self.z));
s.push(')');
s
}
}
fn main() {
let mut buffer = itoa::Buffer::new();
let coords = [
Coordinate { x: 1, y: 2, z: 3 },
Coordinate { x: 10, y: 20, z: 30 },
Coordinate { x: -5, y: -10, z: -15 },
];
for coord in &coords {
println!("{}", coord.format(&mut buffer));
}
}Real-World: High-Performance Metrics
use itoa;
use std::collections::HashMap;
struct Metrics {
buffer: itoa::Buffer,
data: HashMap<String, u64>,
}
impl Metrics {
fn new() -> Self {
Metrics {
buffer: itoa::Buffer::new(),
data: HashMap::new(),
}
}
fn increment(&mut self, key: &str) {
*self.data.entry(key.to_string()).or_insert(0) += 1;
}
fn record(&mut self, key: &str, value: u64) {
self.data.insert(key.to_string(), value);
}
fn format_prometheus(&mut self) -> String {
let mut output = String::new();
for (key, value) in &self.data {
output.push_str(key);
output.push(' ');
output.push_str(self.buffer.format(*value));
output.push('\n');
}
output
}
fn format_statsd(&mut self) -> String {
let mut output = String::new();
for (key, value) in &self.data {
output.push_str(key);
output.push(':');
output.push_str(self.buffer.format(*value));
output.push_str("|c\n");
}
output
}
}
fn main() {
let mut metrics = Metrics::new();
metrics.increment("requests.total");
metrics.increment("requests.total");
metrics.increment("requests.total");
metrics.record("requests.latency_ms", 42);
println!("Prometheus format:");
println!("{}", metrics.format_prometheus());
println!("StatsD format:");
println!("{}", metrics.format_statsd());
}Real-World: Log Formatter
use itoa;
use std::time::{SystemTime, UNIX_EPOCH};
struct LogFormatter {
buffer: itoa::Buffer,
}
impl LogFormatter {
fn new() -> Self {
LogFormatter {
buffer: itoa::Buffer::new(),
}
}
fn format(&mut self, level: &str, message: &str, code: u32, user_id: u64) -> String {
let mut output = String::new();
// Timestamp
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
output.push_str(self.buffer.format(ts));
output.push(' ');
output.push_str(level);
output.push_str(" [code=");
output.push_str(self.buffer.format(code));
output.push(']');
output.push_str(" [user=");
output.push_str(self.buffer.format(user_id));
output.push(']');
output.push(' ');
output.push_str(message);
output
}
}
fn main() {
let mut formatter = LogFormatter::new();
println!("{}", formatter.format("INFO", "Request received", 200, 12345));
println!("{}", formatter.format("ERROR", "Database error", 500, 12345));
}Real-World: CSV Writer
use itoa;
use std::fs::File;
use std::io::{BufWriter, Write};
struct CsvWriter<W: Write> {
buffer: itoa::Buffer,
writer: BufWriter<W>,
}
impl<W: Write> CsvWriter<W> {
fn new(writer: W) -> Self {
CsvWriter {
buffer: itoa::Buffer::new(),
writer: BufWriter::new(writer),
}
}
fn write_row(&mut self, values: &[i64]) -> std::io::Result<()> {
for (i, &value) in values.iter().enumerate() {
if i > 0 {
self.writer.write_all(b",")?;
}
self.writer.write_all(self.buffer.format(value).as_bytes())?;
}
self.writer.write_all(b"\n")?;
Ok(())
}
fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
}
fn main() -> std::io::Result<()> {
let file = File::create("output.csv")?;
let mut csv = CsvWriter::new(file);
csv.write_row(&[1, 100, 1000])?;
csv.write_row(&[2, 200, 2000])?;
csv.write_row(&[3, 300, 3000])?;
csv.flush()?;
println!("CSV written to output.csv");
Ok(())
}Integration with serde
// itoa is used internally by serde_json for fast number formatting
// You can also use it in custom Serialize implementations
use itoa;
use serde::{Serialize, Serializer};
struct FastInteger(i64);
impl Serialize for FastInteger {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut buffer = itoa::Buffer::new();
serializer.serialize_str(buffer.format(self.0))
}
}
fn main() {
let num = FastInteger(12345);
let json = serde_json::to_string(&num).unwrap();
println!("{}", json);
}Buffer Reuse Pattern
use itoa;
// For maximum performance in hot loops, allocate buffer once
fn process_numbers(numbers: &[u32]) -> Vec<String> {
let mut buffer = itoa::Buffer::new();
numbers
.iter()
.map(|&n| buffer.format(n).to_string())
.collect()
}
// Even better: don't allocate strings at all
fn process_to_writer<W: std::io::Write>(
writer: &mut W,
numbers: &[u32],
) -> std::io::Result<()> {
let mut buffer = itoa::Buffer::new();
for (i, &n) in numbers.iter().enumerate() {
if i > 0 {
writer.write_all(b"\n")?;
}
writer.write_all(buffer.format(n).as_bytes())?;
}
Ok(())
}
fn main() {
let numbers: Vec<u32> = (0..1000).collect();
// To vector
let strings = process_numbers(&numbers);
println!("First 5: {:?}", &strings[..5]);
// Direct to stdout
let stdout = std::io::stdout();
let mut handle = stdout.lock();
process_to_writer(&mut handle, &numbers[..10]).unwrap();
println!();
}Summary
itoa::Buffer::new()creates a reusable conversion bufferbuffer.format(n)returns&strwithout allocation- Buffer is overwritten on each call; copy result if you need to keep it
- Supports all integer types: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
itoa::fmt(&mut writer, n)writes tostd::fmt::Writeitoa::write(&mut writer, n)writes tostd::io::Write- 2-10x faster than
to_string()for integer conversion - Zero allocation when writing to pre-sized buffer
- Used internally by
serde_jsonfor fast number serialization - Perfect for: CSV/JSON generation, logging, metrics, HTTP responses, any hot-path integer formatting
