How do I convert integers to strings fast with Itoa in Rust?

Walkthrough

The itoa crate provides ultra-fast integer-to-string conversion. It's significantly faster than Rust's standard ToString or fmt::Display implementations because it avoids the overhead of the formatter infrastructure. The crate uses carefully optimized assembly and SIMD instructions on supported platforms. While to_string() is perfectly fine for most use cases, itoa shines in performance-critical code like JSON serialization, logging systems, metrics collection, and any scenario where you're converting many integers to strings.

Key concepts:

  1. Integer trait — provides itoa() method returning Display type
  2. Buffer struct — reusable buffer for avoiding allocations
  3. No allocations — writes directly to provided buffer
  4. fmt::Display — integrates with standard formatting
  5. All integer types — supports i8 through i128, u8 through u128, and isize/usize

Code Example

# Cargo.toml
[dependencies]
itoa = "1"
use itoa::Integer;
 
fn main() {
    let mut buffer = itoa::Buffer::new();
    let printed = buffer.format(12345);
    println!("{}", printed);
}

Basic Usage

use itoa::{Buffer, Integer};
 
fn main() {
    // Method 1: Using Buffer::format (fastest, no allocation)
    let mut buffer = Buffer::new();
    let s = buffer.format(42);
    println!("Integer: {}", s);
    
    // Method 2: Using Integer trait's fmt::Display
    use std::fmt::Display;
    let value: i32 = 12345;
    let display = value.itoa();
    println!("Via itoa: {}", display);
    
    // Buffer is reusable - reassigning is fine
    let s1 = buffer.format(100);
    let s2 = buffer.format(200);
    println!("{}, {}", s1, s2); // Be careful: s1 is now invalid!
}

Buffer Reuse for Performance

use itoa::Buffer;
 
fn main() {
    let mut buffer = Buffer::new();
    
    // Convert multiple integers using the same buffer
    let numbers: Vec<i64> = (0..10).collect();
    
    for num in numbers {
        // buffer.format() returns a &str that borrows the buffer
        let s = buffer.format(num);
        println!("{}", s);
        // s is invalidated on next buffer.format() call
    }
    
    // For storing results, you must copy
    let mut results = Vec::new();
    for num in 0..5 {
        let s = buffer.format(num);
        results.push(s.to_string()); // Copy to owned string
    }
    
    println!("Stored: {:?}", results);
}

All Supported Integer Types

use itoa::Buffer;
 
fn main() {
    let mut buffer = Buffer::new();
    
    // Signed integers
    let _ = buffer.format(42i8);
    let _ = buffer.format(42i16);
    let _ = buffer.format(42i32);
    let _ = buffer.format(42i64);
    let _ = buffer.format(42i128);
    let _ = buffer.format(42isize);
    
    // Unsigned integers
    let _ = buffer.format(42u8);
    let _ = buffer.format(42u16);
    let _ = buffer.format(42u32);
    let _ = buffer.format(42u64);
    let _ = buffer.format(42u128);
    let _ = buffer.format(42usize);
    
    // Negative numbers
    println!("Negative: {}", buffer.format(-12345));
    
    // Large numbers
    println!("Large: {}", buffer.format(9223372036854775807i64));
    println!("U64 max: {}", buffer.format(18446744073709551615u64));
    
    // i128/u128
    println!("i128: {}", buffer.format(170141183460469231731687303715884105727i128));
    println!("u128: {}", buffer.format(340282366920938463463374607431768211455u128));
}

Writing to Writers

use itoa::{Buffer, Integer};
use std::io::{self, Write};
 
fn main() -> io::Result<()> {
    let mut buffer = Buffer::new();
    let mut stdout = io::stdout();
    
    // Write integer to stdout
    let s = buffer.format(12345);
    stdout.write_all(s.as_bytes())?;
    stdout.write_all(b"\n")?;
    
    // Batch write integers
    let values: Vec<i32> = (0..10).collect();
    
    for value in values {
        let s = buffer.format(value);
        stdout.write_all(s.as_bytes())?;
        stdout.write_all(b", ")?;
    }
    stdout.write_all(b"\n")?;
    
    Ok(())
}

Integration with fmt::Display

use itoa::Integer;
use std::fmt;
 
struct Measurement {
    value: i64,
    unit: &'static str,
}
 
impl fmt::Display for Measurement {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Use itoa for fast integer formatting
        self.value.itoa().fmt(f)?;
        write!(f, " {}", self.unit)
    }
}
 
fn main() {
    let m = Measurement { value: 42, unit: "kg" };
    println!("Measurement: {}", m);
    
    // Or with precision, width, etc.
    println!("Padded: {:>10}", m);
}

JSON Number Serialization

use itoa::Buffer;
 
fn write_json_integer(buffer: &mut Buffer, value: i64, output: &mut Vec<u8>) {
    let s = buffer.format(value);
    output.extend_from_slice(s.as_bytes());
}
 
fn write_json_array(values: &[i64]) -> Vec<u8> {
    let mut buffer = Buffer::new();
    let mut output = Vec::new();
    
    output.push(b'[');
    
    for (i, &value) in values.iter().enumerate() {
        if i > 0 {
            output.push(b',');
        }
        write_json_integer(&mut buffer, value, &mut output);
    }
    
    output.push(b']');
    output
}
 
fn main() {
    let numbers = vec![1, 42, 100, -500, 999999];
    let json = write_json_array(&numbers);
    
    println!("JSON: {}", String::from_utf8_lossy(&json));
}

CSV Generation

use itoa::Buffer;
use std::fs::File;
use std::io::{self, Write};
 
struct DataRecord {
    id: u64,
    count: i32,
    score: i64,
}
 
fn write_csv<W: Write>(writer: &mut W, records: &[DataRecord]) -> io::Result<()> {
    let mut buffer = Buffer::new();
    
    // Header
    writeln!(writer, "id,count,score")?;
    
    // Records
    for record in records {
        writer.write_all(buffer.format(record.id).as_bytes())?;
        writer.write_all(b",")?;
        
        writer.write_all(buffer.format(record.count).as_bytes())?;
        writer.write_all(b",")?;
        
        writer.write_all(buffer.format(record.score).as_bytes())?;
        writer.write_all(b"\n")?;
    }
    
    Ok(())
}
 
fn main() -> io::Result<()> {
    let records = vec![
        DataRecord { id: 1, count: 42, score: 100 },
        DataRecord { id: 2, count: -5, score: 85 },
        DataRecord { id: 3, count: 1000, score: -50 },
    ];
    
    let mut output = Vec::new();
    write_csv(&mut output, &records)?;
    
    println!("CSV output:");
    println!("{}", String::from_utf8_lossy(&output));
    
    Ok(())
}

Log Formatting

use itoa::Buffer;
use std::io::{self, Write};
use std::time::Instant;
 
struct FastLogger {
    buffer: Buffer,
    output: Vec<u8>,
}
 
impl FastLogger {
    fn new() -> Self {
        Self {
            buffer: Buffer::new(),
            output: Vec::new(),
        }
    }
    
    fn log(&mut self, level: &str, code: i32, message: &str) {
        // [TIMESTAMP] LEVEL (CODE): message
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_millis() as u64)
            .unwrap_or(0);
        
        self.output.push(b'[');
        self.output.extend_from_slice(self.buffer.format(timestamp).as_bytes());
        self.output.extend_from_slice(b"] ");
        
        self.output.extend_from_slice(level.as_bytes());
        self.output.extend_from_slice(b" (");
        self.output.extend_from_slice(self.buffer.format(code).as_bytes());
        self.output.extend_from_slice(b"): ");
        
        self.output.extend_from_slice(message.as_bytes());
        self.output.push(b'\n');
    }
    
    fn get_output(&self) -> &[u8] {
        &self.output
    }
    
    fn clear(&mut self) {
        self.output.clear();
    }
}
 
fn main() {
    let mut logger = FastLogger::new();
    
    logger.log("INFO", 200, "Request processed successfully");
    logger.log("WARN", 429, "Rate limit exceeded");
    logger.log("ERROR", 500, "Internal server error");
    
    print!("{}", String::from_utf8_lossy(logger.get_output()));
}

Metrics Collection

use itoa::Buffer;
use std::collections::HashMap;
 
struct Metrics {
    counters: HashMap<String, u64>,
    buffer: Buffer,
}
 
impl Metrics {
    fn new() -> Self {
        Self {
            counters: HashMap::new(),
            buffer: Buffer::new(),
        }
    }
    
    fn increment(&mut self, name: &str) {
        *self.counters.entry(name.to_string()).or_insert(0) += 1;
    }
    
    fn add(&mut self, name: &str, value: u64) {
        *self.counters.entry(name.to_string()).or_insert(0) += value;
    }
    
    fn format_prometheus(&mut self) -> String {
        let mut output = String::new();
        
        for (name, value) in &self.counters {
            output.push_str(name);
            output.push(' ');
            output.push_str(self.buffer.format(*value));
            output.push('\n');
        }
        
        output
    }
}
 
fn main() {
    let mut metrics = Metrics::new();
    
    for _ in 0..100 {
        metrics.increment("requests_total");
    }
    
    metrics.add("bytes_sent", 1048576);
    metrics.increment("errors_total");
    
    println!("Prometheus format:");
    println!("{}", metrics.format_prometheus());
}

HTTP Response Building

use itoa::Buffer;
 
struct HttpResponse {
    status: u16,
    headers: Vec<(String, String)>,
    body: Vec<u8>,
}
 
impl HttpResponse {
    fn new(status: u16, body: Vec<u8>) -> Self {
        Self {
            status,
            headers: Vec::new(),
            body,
        }
    }
    
    fn header(mut self, name: &str, value: &str) -> Self {
        self.headers.push((name.to_string(), value.to_string()));
        self
    }
    
    fn to_bytes(&self, buffer: &mut Buffer) -> Vec<u8> {
        let mut output = Vec::new();
        
        // Status line: HTTP/1.1 200 OK
        output.extend_from_slice(b"HTTP/1.1 ");
        output.extend_from_slice(buffer.format(self.status).as_bytes());
        
        let status_text = match self.status {
            200 => " OK",
            201 => " Created",
            404 => " Not Found",
            500 => " Internal Server Error",
            _ => "",
        };
        output.extend_from_slice(status_text.as_bytes());
        output.extend_from_slice(b"\r\n");
        
        // Headers
        for (name, value) in &self.headers {
            output.extend_from_slice(name.as_bytes());
            output.extend_from_slice(b": ");
            output.extend_from_slice(value.as_bytes());
            output.extend_from_slice(b"\r\n");
        }
        
        // Content-Length
        output.extend_from_slice(b"Content-Length: ");
        output.extend_from_slice(buffer.format(self.body.len()).as_bytes());
        output.extend_from_slice(b"\r\n\r\n");
        
        // Body
        output.extend_from_slice(&self.body);
        
        output
    }
}
 
fn main() {
    let response = HttpResponse::new(200, b"Hello, World!".to_vec())
        .header("Content-Type", "text/plain")
        .header("Connection", "keep-alive");
    
    let mut buffer = Buffer::new();
    let bytes = response.to_bytes(&mut buffer);
    
    println!("HTTP Response:");
    println!("{}", String::from_utf8_lossy(&bytes));
}

Database Query Building

use itoa::Buffer;
 
struct QueryBuilder {
    buffer: Buffer,
    query: Vec<u8>,
}
 
impl QueryBuilder {
    fn new() -> Self {
        Self {
            buffer: Buffer::new(),
            query: Vec::new(),
        }
    }
    
    fn select(&mut self, columns: &[&str]) -> &mut Self {
        self.query.extend_from_slice(b"SELECT ");
        for (i, col) in columns.iter().enumerate() {
            if i > 0 {
                self.query.push(b',');
            }
            self.query.extend_from_slice(col.as_bytes());
        }
        self
    }
    
    fn from_table(&mut self, table: &str) -> &mut Self {
        self.query.extend_from_slice(b" FROM ");
        self.query.extend_from_slice(table.as_bytes());
        self
    }
    
    fn limit(&mut self, n: u64) -> &mut Self {
        self.query.extend_from_slice(b" LIMIT ");
        self.query.extend_from_slice(self.buffer.format(n).as_bytes());
        self
    }
    
    fn offset(&mut self, n: u64) -> &mut Self {
        self.query.extend_from_slice(b" OFFSET ");
        self.query.extend_from_slice(self.buffer.format(n).as_bytes());
        self
    }
    
    fn where_id(&mut self, id: i64) -> &mut Self {
        self.query.extend_from_slice(b" WHERE id = ");
        self.query.extend_from_slice(self.buffer.format(id).as_bytes());
        self
    }
    
    fn build(&self) -> String {
        String::from_utf8_lossy(&self.query).into_owned()
    }
}
 
fn main() {
    let mut builder = QueryBuilder::new();
    
    let query = builder
        .select(&["id", "name", "email"])
        .from_table("users")
        .where_id(42)
        .limit(10)
        .offset(20)
        .build();
    
    println!("Query: {}", query);
}

Custom Number Formatting

use itoa::Buffer;
 
fn format_with_commas(n: i64) -> String {
    let mut buffer = Buffer::new();
    let digits = buffer.format(n.abs());
    
    let mut result = String::new();
    if n < 0 {
        result.push('-');
    }
    
    let chars: Vec<char> = digits.chars().collect();
    let len = chars.len();
    
    for (i, c) in chars.iter().enumerate() {
        if i > 0 && (len - i) % 3 == 0 {
            result.push(',');
        }
        result.push(*c);
    }
    
    result
}
 
fn format_hex(n: u64) -> String {
    let mut buffer = Buffer::new();
    let decimal = buffer.format(n);
    format!("0x{:X} ({})", n, decimal)
}
 
fn format_percentage(value: i64, total: i64) -> String {
    if total == 0 {
        return "0%".to_string();
    }
    
    let mut buffer = Buffer::new();
    let pct = (value * 100) / total;
    format!("{}%", buffer.format(pct))
}
 
fn main() {
    println!("With commas: {}", format_with_commas(1234567890));
    println!("With commas: {}", format_with_commas(-1234567890));
    println!("Hex: {}", format_hex(255));
    println!("Percentage: {}", format_percentage(42, 100));
}

Performance Comparison

use itoa::Buffer;
use std::time::Instant;
 
fn bench_itoa(n: i64, iterations: u64) -> u128 {
    let mut buffer = Buffer::new();
    let start = Instant::now();
    
    for _ in 0..iterations {
        let _ = buffer.format(n);
    }
    
    start.elapsed().as_nanos()
}
 
fn bench_to_string(n: i64, iterations: u64) -> u128 {
    let start = Instant::now();
    
    for _ in 0..iterations {
        let _ = n.to_string();
    }
    
    start.elapsed().as_nanos()
}
 
fn bench_write(n: i64, iterations: u64) -> u128 {
    let mut output = Vec::new();
    let start = Instant::now();
    
    for _ in 0..iterations {
        output.clear();
        std::write!(&mut output, "{}", n).unwrap();
    }
    
    start.elapsed().as_nanos()
}
 
fn main() {
    let iterations = 1_000_000;
    let test_number = 123456789i64;
    
    println!("Converting {} {} times:", test_number, iterations);
    
    let itoa_time = bench_itoa(test_number, iterations);
    let to_string_time = bench_to_string(test_number, iterations);
    let write_time = bench_write(test_number, iterations);
    
    println!("  itoa::Buffer::format: {:?}ns", itoa_time);
    println!("  to_string(): {:?}ns", to_string_time);
    println!("  write!(): {:?}ns", write_time);
    
    println!("\nitoa is {:.1}x faster than to_string()", 
        to_string_time as f64 / itoa_time as f64);
}

Buffer in Struct

use itoa::Buffer;
 
struct NumberFormatter {
    buffer: Buffer,
}
 
impl NumberFormatter {
    fn new() -> Self {
        Self {
            buffer: Buffer::new(),
        }
    }
    
    fn format(&mut self, n: i64) -> &str {
        self.buffer.format(n)
    }
    
    fn format_to_string(&mut self, n: i64) -> String {
        self.buffer.format(n).to_string()
    }
    
    fn format_pair(&mut self, a: i64, b: i64) -> String {
        let first = self.buffer.format(a).to_string();
        let second = self.buffer.format(b);
        format!("({}, {})", first, second)
    }
}
 
fn main() {
    let mut formatter = NumberFormatter::new();
    
    println!("Direct: {}", formatter.format(42));
    println!("Owned: {}", formatter.format_to_string(12345));
    println!("Pair: {}", formatter.format_pair(10, 20));
}

Thread-Local Buffer

use itoa::Buffer;
use std::cell::RefCell;
 
thread_local! {
    static BUFFER: RefCell<Buffer> = RefCell::new(Buffer::new());
}
 
fn fast_format(n: i64) -> String {
    BUFFER.with(|buf| {
        buf.borrow_mut().format(n).to_string()
    })
}
 
fn fast_format_ref<F, R>(n: i64, f: F) -> R
where
    F: FnOnce(&str) -> R,
{
    BUFFER.with(|buf| f(buf.borrow().format(n)))
}
 
fn main() {
    println!("Thread-local: {}", fast_format(42));
    
    fast_format_ref(12345, |s| {
        println!("Reference: {}", s);
    });
    
    // Works in multi-threaded context
    let handles: Vec<_> = (0..4)
        .map(|t| {
            std::thread::spawn(move || {
                for i in 0..10 {
                    println!("Thread {}: {}", t, fast_format(i * 100));
                }
            })
        })
        .collect();
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Embedded in Serde Serializer

use itoa::Buffer;
use std::fmt;
 
// Example of how serde_json uses itoa internally
struct FastIntSerializer<'a> {
    buffer: &'a mut Buffer,
    output: &'a mut Vec<u8>,
}
 
impl<'a> FastIntSerializer<'a> {
    fn new(buffer: &'a mut Buffer, output: &'a mut Vec<u8>) -> Self {
        Self { buffer, output }
    }
    
    fn serialize_i64(&mut self, value: i64) {
        let s = self.buffer.format(value);
        self.output.extend_from_slice(s.as_bytes());
    }
    
    fn serialize_u64(&mut self, value: u64) {
        let s = self.buffer.format(value);
        self.output.extend_from_slice(s.as_bytes());
    }
}
 
fn main() {
    let mut buffer = Buffer::new();
    let mut output = Vec::new();
    let mut serializer = FastIntSerializer::new(&mut buffer, &mut output);
    
    serializer.serialize_i64(-42);
    output.push(b',');
    serializer.serialize_u64(12345);
    
    println!("Serialized: {}", String::from_utf8_lossy(&output));
}

Integer Array Formatting

use itoa::Buffer;
 
fn format_int_array(values: &[i32]) -> String {
    let mut buffer = Buffer::new();
    let mut result = String::new();
    
    result.push('[');
    
    for (i, &value) in values.iter().enumerate() {
        if i > 0 {
            result.push_str(", ");
        }
        result.push_str(buffer.format(value));
    }
    
    result.push(']');
    result
}
 
fn format_matrix(matrix: &[Vec<i32>]) -> String {
    let mut buffer = Buffer::new();
    let mut result = String::new();
    
    result.push_str("[\n");
    
    for row in matrix {
        result.push_str("  [");
        for (i, &value) in row.iter().enumerate() {
            if i > 0 {
                result.push_str(", ");
            }
            result.push_str(buffer.format(value));
        }
        result.push_str("],\n");
    }
    
    result.push(']');
    result
}
 
fn main() {
    let arr = vec![1, 2, 3, 4, 5];
    println!("Array: {}", format_int_array(&arr));
    
    let matrix = vec![
        vec![1, 2, 3],
        vec![4, 5, 6],
        vec![7, 8, 9],
    ];
    println!("Matrix: {}", format_matrix(&matrix));
}

Summary

  • Buffer::new() creates a reusable buffer (no allocation per conversion)
  • buffer.format(integer) returns &str borrowing the buffer
  • Buffer has fixed size (max 40 bytes for i128/u128)
  • Integer trait provides itoa() method for fmt::Display integration
  • Supports all integer types: i8–i128, u8–u128, isize, usize
  • Significantly faster than to_string() or format!()
  • Use buffer.format(n).to_string() if you need an owned string
  • Thread safety: use separate buffers or thread_local!
  • Perfect for: JSON serialization, CSV generation, logging, metrics, HTTP responses, query building
  • Zero allocations per conversion when buffer is reused