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:
- Integer trait — provides
itoa()method returningDisplaytype - Buffer struct — reusable buffer for avoiding allocations
- No allocations — writes directly to provided buffer
- fmt::Display — integrates with standard formatting
- 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&strborrowing the buffer- Buffer has fixed size (max 40 bytes for i128/u128)
Integertrait providesitoa()method forfmt::Displayintegration- Supports all integer types: i8–i128, u8–u128, isize, usize
- Significantly faster than
to_string()orformat!() - 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
