Creating Excel Files in Rust With rust_xlsxwriter Crate
Create Excel files in Rust with rust_xlsxwriter: typed cells, formats, dates, constant memory mode for large files, and reading workbooks with calamine.

Rust is chosen for tools that need to be fast and predictable, and sooner or later such a tool must produce a spreadsheet for a human. The crate for writing xlsx is rust_xlsxwriter, a port of the well-known XlsxWriter family of libraries. It is strongly typed, fast and memory-conscious, and it has an API that makes you state what each cell is. This post shows how to write a workbook, format it, keep memory flat on large outputs and read files with the companion crate calamine. It is a spoke of reading and writing Excel from JavaScript, Java and C Sharp.
Set up
# Cargo.toml
[dependencies]
rust_xlsxwriter = "0.70"
calamine = "0.25"Pin versions and read the changelog when upgrading; both crates are actively developed and have changed method names between releases.
Write a first workbook
use rust_xlsxwriter::{Workbook, XlsxError};
fn main() -> Result<(), XlsxError> {
let mut workbook = Workbook::new();
let worksheet = workbook.add_worksheet();
worksheet.set_name("Report")?;
worksheet.write_string(0, 0, "Order")?;
worksheet.write_string(0, 1, "Amount")?;
worksheet.write_string(1, 0, "0007")?; // text, keeps the leading zeros
worksheet.write_number(1, 1, 1234.5)?;
workbook.save("report.xlsx")?;
Ok(())
}Rows and columns are zero-based, so (0, 0) is A1. Each write_* method returns a Result, and the ? operator passes an error up to main. Errors you may see include a row or column beyond the sheet limits (1,048,576 rows and 16,384 columns), a string over the 32,767-character cell limit and a sheet name that is too long or contains forbidden characters. Those arrive as XlsxError values with a message, not as a corrupt file, which is a real advantage over libraries that write first and let Excel complain (Excel found unreadable content describes the cost of the alternative).
The methods are explicit about type: write_string, write_number, write_boolean, write_datetime, write_formula, write_url, write_blank. There is also a generic write that dispatches on the Rust type, convenient when types are known at compile time. State the type for identifiers (write_string) so that no inference can turn them into numbers.
Formats
A Format holds font, fill, borders, alignment and number format. Create it once and pass a reference to each write:
use rust_xlsxwriter::{Format, FormatAlign, FormatBorder, ExcelDateTime, Workbook, XlsxError};
fn build() -> Result<(), XlsxError> {
let mut workbook = Workbook::new();
let head = Format::new().set_bold().set_background_color("DDEBF7").set_border(FormatBorder::Thin);
let date = Format::new().set_num_format("yyyy-mm-dd");
let money = Format::new().set_num_format("#,##0.00").set_align(FormatAlign::Right);
let ws = workbook.add_worksheet();
ws.write_string_with_format(0, 0, "Order", &head)?;
ws.write_string_with_format(0, 1, "Date", &head)?;
ws.write_string_with_format(0, 2, "Amount", &head)?;
ws.write_string(1, 0, "0007")?;
ws.write_datetime_with_format(1, 1, &ExcelDateTime::from_ymd(2025, 3, 14)?, &date)?;
ws.write_number_with_format(1, 2, 1234.5, &money)?;
ws.set_freeze_panes(1, 0)?;
ws.autofilter(0, 0, 1, 2)?;
ws.set_column_width(0, 12)?;
ws.set_column_width(2, 14)?;
workbook.save("report.xlsx")
}Builder methods on Format return a new format, so chains read well. Dates need a number format to display as dates; without it, the cell shows a serial number. ExcelDateTime can be built from year, month and day, from a string, and, with the chrono feature enabled, from chrono types. Formulas are written with write_formula; the crate does not calculate them, so the file has no cached results until a spreadsheet application recalculates, though you can supply a result with write_formula_with_format or Formula::new(...).set_result("42") so non-calculating readers see a value.
Tables and columns of data
For a block of data, write rows in a loop, or use the table feature, which adds an Excel Table with banding, filters and structured references:
use rust_xlsxwriter::{Table, TableColumn};
let data = [("0001", 100.0), ("0002", 250.0), ("0003", 75.0)];
for (i, (order, amount)) in data.iter().enumerate() {
ws.write_string(1 + i as u32, 0, *order)?;
ws.write_number(1 + i as u32, 1, *amount)?;
}
let table = Table::new().set_columns(&[
TableColumn::new().set_header("Order"),
TableColumn::new().set_header("Amount"),
]);
ws.add_table(0, 0, data.len() as u32, 1, &table)?;With a table, the header cells are written by the table, and the table must not overlap other tables or merged ranges. Column headers must be unique and non-empty.
Optional Serde support (the serde feature) lets you serialize a Rust struct per row with worksheet.serialize(&record)?, with headers derived from field names, which turns an export into a loop over a collection. Check the crate's serialization docs for the options.
Large outputs: constant memory
A normal worksheet keeps its cells in memory until save. For very large sheets, use constant memory mode, which writes each row to a temporary file as soon as the next row starts:
let ws = workbook.add_worksheet_with_constant_memory();
for row in 0..1_000_000u32 {
ws.write_number(row, 0, row as f64)?;
ws.write_string(row, 1, format!("item {row}"))?;
}
workbook.save("big.xlsx")?;The rule is that you write rows in order and never return to a previous row; writing to an earlier row is an error. Strings are written inline, not through the shared strings table, which makes the file larger and Excel slightly slower to open it, in return for flat memory. Set column widths and other sheet-wide options before the first row is written. The limit of 1,048,576 rows still applies. If you need more than a sheet can hold, write several sheets or use a different format (converting a huge Excel file to CSV or Parquet covers alternatives).
Read with calamine
rust_xlsxwriter does not read files. Use calamine:
use calamine::{open_workbook_auto, Reader, Data};
fn read() -> Result<(), Box<dyn std::error::Error>> {
let mut wb = open_workbook_auto("orders.xlsx")?; // xlsx, xls, xlsb, ods
let range = wb.worksheet_range("Orders")?;
for row in range.rows().skip(1) {
match (&row[0], &row[2]) {
(Data::String(order), Data::Float(amount)) => println!("{order}: {amount}"),
(Data::String(order), Data::Int(amount)) => println!("{order}: {amount}"),
other => eprintln!("unexpected cells: {other:?}"),
}
}
Ok(())
}Data is an enum with variants such as Empty, String, Float, Int, Bool, DateTime, DateTimeIso, DurationIso and Error. Matching on it forces you to deal with every case, which suits messy sheets: an amount cell that holds text lands in your other arm, not in a silent zero. Date cells arrive as DateTime with an as_datetime() conversion (with the dates feature), or as a serial number. calamine reads values and cached formula results, not styles. It is fast, written in Rust and also the engine behind the python-calamine package.
Errors and structure
Use a custom error type, or Box<dyn Error> for tools, so that ? can combine XlsxError, I/O errors and parsing errors. Validate data before writing: a string over the cell limit or a NaN float cell should be handled (write a blank or an error marker) rather than failing the whole export. Sanitize user text: strings beginning with =, +, - or @ are written as strings by write_string, which is safe because the type is explicit; the risk appears only if you pass them to write_formula.
Check the result
Open the file in Excel, LibreOffice and a reader library. Compare the number of rows written with the source and a numeric sum with a database total. Confirm that the identifier column kept its zeros, that dates display as dates and that the freeze pane and filter are present. Try an empty data set and a value at the string limit.
When rust_xlsxwriter is not the crate you need
You need to modify an existing workbook. The crate writes new files only. Read with calamine and write a new file, or use a different tool.
You need to read formatted values. calamine reads values; formatting needs another approach.
The consumer wants CSV or Parquet. Use the csv crate or arrow/parquet and avoid Excel altogether (reading and writing CSV in Rust).
You need formula results in the file. Neither crate calculates; supply results yourself or recalculate with a spreadsheet application.
See the rust_xlsxwriter documentation and the calamine repository. To convert a small result to JSON, use the CSV to JSON converter.


