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:

  1. No allocation — writes to caller-provided buffer
  2. itoa::Buffer — reusable buffer for repeated conversions
  3. fmt::Display — integrates with standard formatting traits
  4. Write trait — can write to any std::fmt::Write or std::io::Write
  5. 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 buffer
  • buffer.format(n) returns &str without 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 to std::fmt::Write
  • itoa::write(&mut writer, n) writes to std::io::Write
  • 2-10x faster than to_string() for integer conversion
  • Zero allocation when writing to pre-sized buffer
  • Used internally by serde_json for fast number serialization
  • Perfect for: CSV/JSON generation, logging, metrics, HTTP responses, any hot-path integer formatting