FastExcel is a small Java library that writes and reads large .xlsx files by streaming them, so a file with a million rows needs only a few megabytes of heap. It has two modules, fastexcel for writing and fastexcel-reader for reading, and both are much faster than the regular Apache POI API.
We use FastExcel for big exports and imports where POI’s XSSFWorkbook is too slow or runs out of memory, such as a report of all orders of the year or a nightly upload of sensor data. In exchange, it supports only basic styling and no charts.
The following example writes two rows of daily step counts with FastExcel 0.20.2 and reads them back.
Path file = Path.of("steps.xlsx");
try (OutputStream os = Files.newOutputStream(file);
Workbook wb = new Workbook(os, "StepTracker", "1.0")) {
Worksheet ws = wb.newWorksheet("Steps");
ws.value(0, 0, "Day"); // A1
ws.value(0, 1, "Steps"); // B1
ws.value(1, 0, "Monday"); // A2
ws.value(1, 1, 8200); // B2
}
List<String> days;
try (ReadableWorkbook wb = new ReadableWorkbook(file.toFile());
Stream<Row> rows = wb.getFirstSheet().openStream()) {
days = rows.skip(1).map(r -> r.getCellText(0)).toList();
}
String day = days.get(0); // "Monday"
Notice that value(row, column, value) takes zero-based row and column indexes, so (1, 1) is cell B2, and that the reader returns a Stream of rows. We cover the Maven setup, writing a million rows, styles and formulas, typed reading, parallel sheets, a comparison with POI and the questions people ask most.
1. FastExcel Maven Setup
FastExcel is split into two artifacts with the same version. The writer has no dependencies apart from a small ZIP library, and the reader adds the aalto-xml StAX parser and commons-compress. Both run on Java 8 and later, and the jars include a module-info for modular apps.
- fastexcel writes .xlsx files.
- fastexcel-reader reads .xlsx files.
<dependency>
<groupId>org.dhatim</groupId>
<artifactId>fastexcel</artifactId>
<version>0.20.2</version>
</dependency>
<dependency>
<groupId>org.dhatim</groupId>
<artifactId>fastexcel-reader</artifactId>
<version>0.20.2</version>
</dependency>
The name FastExcel is also used by an unrelated project, Alibaba’s fork of EasyExcel with the cn.idev.excel group ID, which moved to the Apache Software Foundation as Apache Fesod. This article covers dhatim FastExcel with the org.dhatim group ID.
2. Writing a Large Excel File
The writer keeps the cells of each worksheet in memory until we flush them, and writes them to the OutputStream as XML. Strings go into a shared table, so repeated values such as status names are stored only once in the file.

2.1. API
To write a file, we create an org.dhatim.fastexcel.Workbook on an OutputStream and add sheets with newWorksheet(). The second and third constructor arguments are the application name and version stored in the file properties.
try (OutputStream os = Files.newOutputStream(Path.of("api.xlsx"));
Workbook wb = new Workbook(os, "StepTracker", "1.0")) {
Worksheet ws = wb.newWorksheet("Week 41");
ws.value(0, 0, "Tuesday"); // A1, String
ws.value(0, 1, 10450); // B1, Number
ws.value(0, 2, LocalDate.of(2026, 10, 6)); // C1, date as a number
ws.style(0, 2).format("yyyy-mm-dd").set(); // C1 shown as a date
ws.value(0, 3, true); // D1, Boolean
ws.formula(1, 1, "SUM(B1:B1)"); // B2
}
The value() method has overloads for String, Number, Boolean, LocalDate, LocalDateTime, ZonedDateTime, Instant and Date. Excel stores a date as a number, so a date cell needs a format such as yyyy-mm-dd, otherwise Excel shows 46301.
Closing the Workbook calls finish(), which writes the remaining parts of the file. If we do not use try-with-resources, we call wb.finish() ourselves before closing the stream, or the file is incomplete and Excel refuses to open it.
2.2. Example
The following example writes 1,000,000 rows with three columns. Calling ws.flush() every 10,000 rows writes the buffered rows to the stream and frees them, which keeps the heap use flat for any number of rows.
static void writeSteps(Path file, int rows) throws IOException {
try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(file));
Workbook wb = new Workbook(os, "StepTracker", "1.0")) {
Worksheet ws = wb.newWorksheet("Steps");
ws.value(0, 0, "Day");
ws.value(0, 1, "User");
ws.value(0, 2, "Steps");
ws.range(0, 0, 0, 2).style().bold().fillColor(Color.GRAY2).set();
for (int i = 1; i <= rows; i++) {
ws.value(i, 0, i);
ws.value(i, 1, "user-" + (i % 1000));
ws.value(i, 2, 4000 + i % 9000);
if (i % 10_000 == 0) {
ws.flush(); // write buffered rows to the stream
}
}
}
}
Path big = Path.of("steps-1m.xlsx");
writeSteps(big, 1_000_000);
boolean written = Files.size(big) > 10_000_000; // true, about 18 MB
In our test on JDK 25 with a 256 MB heap (-Xmx256m) on a 2-core machine, writing one million rows of three columns took between 1.4 and 2.7 seconds after a warm-up run, and the file was 18 MB. The regular XSSFWorkbook keeps every cell as an object until it writes the file, so the same file needs far more heap. The FastExcel benchmark reports the regular POI API as about 10 times slower with 12 times more heap, and POI’s streaming SXSSFWorkbook as close in speed.
2.3. Styles, Merged Cells and Freeze Panes
FastExcel covers the formatting that reports need, but not more. A style is built with style() on a cell or range() on a block of cells and applied with set().
try (OutputStream os = Files.newOutputStream(Path.of("report.xlsx"));
Workbook wb = new Workbook(os, "StepTracker", "1.0")) {
Worksheet ws = wb.newWorksheet("Report");
ws.value(0, 0, "Weekly steps");
ws.range(0, 0, 0, 2).merge(); // A1:C1 merged
ws.style(0, 0).bold().fontSize(14).horizontalAlignment("center").set();
ws.value(1, 0, "Day");
ws.value(1, 1, "Steps");
ws.freezePane(0, 2); // rows 1 and 2 stay visible
ws.width(0, 18); // column A width
ws.value(2, 1, 0.75);
ws.style(2, 1).format("0%").set(); // shows 75%
}
Charts and pivot tables are outside the scope of FastExcel, and its support for images and conditional formatting covers only simple cases. For a report that needs more, we write the data with FastExcel into a template sheet or use Apache POI.
3. Reading a Large Excel File
The reader parses the sheet XML with a StAX parser and creates one Row object at a time. Each Row holds its cells, so the memory needed depends on the width of a row and the size of the shared strings table, not on the number of rows.
3.1. API
We open a ReadableWorkbook from a File or an InputStream, pick a sheet, and call openStream(). The stream must be closed, so it goes into try-with-resources together with the workbook.
| Method | Returns | Notes |
|---|---|---|
| getCellText(i) | String | Works for every cell type, empty string for a missing cell |
| getCellAsString(i) | Optional<String> | Throws ExcelReaderException if the cell is not text |
| getCellAsNumber(i) | Optional<BigDecimal> | Throws ExcelReaderException if the cell is not a number |
| getCellAsDate(i) | Optional<LocalDateTime> | Converts the stored number to a date |
| getCellAsBoolean(i) | Optional<Boolean> | Throws ExcelReaderException if the cell is not a Boolean |
| getCell(i).getType() | CellType | NUMBER, STRING, FORMULA, BOOLEAN, ERROR or EMPTY |
| getRowNum() | int | One-based, the header row is 1 |
The typed getters check the cell type and throw ExcelReaderException instead of converting. Calling getCellAsNumber(1) on the header row, where the cell holds the text Steps, is the most common reason for that exception, so we skip the header first.
Row row;
try (ReadableWorkbook wb = new ReadableWorkbook(new File("api.xlsx"))) {
row = wb.getFirstSheet().read().get(0);
}
String day = row.getCellText(0); // "Tuesday"
BigDecimal steps = row.getCellAsNumber(1).orElseThrow(); // 10450
LocalDateTime date = row.getCellAsDate(2).orElseThrow(); // 2026-10-06T00:00
Boolean goal = row.getCellAsBoolean(3).orElseThrow(); // true
String bad = row.getCellAsString(1).orElse(""); // ExcelReaderException: Wrong cell type NUMBER, wanted STRING
3.2. Example
The following example reads the million-row file from section 2.2 and sums the Steps column. The stream is processed lazily, so skip(1) drops the header and mapToLong() handles one row after the other.
static long totalSteps(Path file) throws IOException {
try (ReadableWorkbook wb = new ReadableWorkbook(file.toFile());
Stream<Row> rows = wb.getFirstSheet().openStream()) {
return rows.skip(1)
.mapToLong(r -> r.getCellAsNumber(2).map(BigDecimal::longValue).orElse(0L))
.sum();
}
}
long total = totalSteps(Path.of("steps-1m.xlsx")); // 8495501000
Reading the file took between 1.6 and 2.5 seconds in the same test with the same 256 MB heap. For small files, sheet.read() returns all rows as a List, which is simpler but keeps every row in memory.
Formula cells written by FastExcel come back with type FORMULA and no value. FastExcel writes only the formula, and Excel calculates it when the file is opened, so the reader has no cached result until someone saves the file in Excel.
4. Writing Several Sheets in Parallel
Each Worksheet can be filled on its own thread, and FastExcel keeps the shared strings and styles thread-safe. A monthly report with one sheet per region is a typical case, because every region’s data comes from a different database query.
try (OutputStream os = Files.newOutputStream(Path.of("regions.xlsx"));
Workbook wb = new Workbook(os, "StepTracker", "1.0")) {
Worksheet north = wb.newWorksheet("North");
Worksheet south = wb.newWorksheet("South");
CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> {
for (int i = 0; i < 1000; i++) north.value(i, 0, i);
});
CompletableFuture<Void> f2 = CompletableFuture.runAsync(() -> {
for (int i = 0; i < 500; i++) south.value(i, 0, i);
});
CompletableFuture.allOf(f1, f2).join();
}
int southRows;
boolean hasWest;
try (ReadableWorkbook wb = new ReadableWorkbook(new File("regions.xlsx"))) {
southRows = wb.findSheet("South").orElseThrow().read().size();
hasWest = wb.findSheet("West").isPresent();
}
int south = southRows; // 500
boolean west = hasWest; // false
The CompletableFuture tasks must finish before the workbook closes, which is what allOf(…).join() waits for. Two threads must never write to the same Worksheet.
5. Exporting a Download from a REST Endpoint
An admin page of a fitness app offers “Download all activity” as an Excel file. With 600,000 rows, building the whole file in memory before sending it would hold a lot of heap per request. FastExcel writes to any OutputStream, so we pass the HTTP response stream and the browser receives the file while it is being built.
static void exportActivity(OutputStream responseBody, Stream<String[]> records) throws IOException {
try (Workbook wb = new Workbook(responseBody, "StepTracker", "1.0")) {
Worksheet ws = wb.newWorksheet("Activity");
ws.value(0, 0, "User");
ws.value(0, 1, "Steps");
int[] row = {1};
Iterator<String[]> it = records.iterator();
while (it.hasNext()) {
String[] r = it.next();
ws.value(row[0], 0, r[0]);
ws.value(row[0], 1, Integer.parseInt(r[1]));
if (++row[0] % 10_000 == 0) {
ws.flush();
}
}
}
}
ByteArrayOutputStream body = new ByteArrayOutputStream();
exportActivity(body, Stream.of(new String[]{"anna", "9100"}, new String[]{"ben", "6400"}));
boolean isZip = body.toByteArray()[0] == 'P'; // true, an .xlsx file starts with "PK"
In a Spring MVC controller, the stream is response.getOutputStream(), with the content type application/vnd.openxmlformats-officedocument.spreadsheetml.sheet and a Content-Disposition header for the file name. The Excel download with Spring Boot article shows the controller part.
6. FastExcel vs Apache POI
Choose FastExcel when the file is large and the formatting is simple, and Apache POI when we need charts, formula evaluation or .xls files. POI also has streaming options, namely SXSSFWorkbook for writing and the POI event API with SAX for reading, but both need more code than FastExcel.
| Feature | FastExcel | POI XSSFWorkbook | POI SXSSF / event API |
|---|---|---|---|
| Memory for 1 million rows | Low | Very high | Low |
| API style | Simple value() and Stream<Row> | Full object model | Usermodel window or SAX callbacks |
| .xls (Excel 97-2003) | No | No, HSSF is separate | No |
| Charts, pivot tables, images | No (basic images only) | Yes | Limited |
| Formula evaluation | No | Yes | No |
| Edit an existing file | No | Yes | No |
7. FastExcel FAQs
Speed is the reason people pick FastExcel, and limits are what they ask about next, from templates and dates to the second library with the same name.
7.1. Can FastExcel modify an existing Excel file?
No. The writer always creates a new file, and the reader only reads. To change cells in an existing file or fill a template, we use Apache POI, or we read the old file with the FastExcel reader and write a new one.
7.2. Does FastExcel support .xls files?
No. Both modules handle only the .xlsx format. The method ReadableWorkbook.isOLE2Header() helps to detect an old .xls file before parsing, so the app can show a clear error message.
7.3. Why does getCellAsDate() return a LocalDateTime?
Excel stores dates and times as one number, the days since 1900 plus a fraction of a day. FastExcel converts that number into a LocalDateTime, and toLocalDate() drops the time part.
7.4. How many rows can one sheet hold?
An .xlsx sheet has a limit of 1,048,576 rows and 16,384 columns, and FastExcel checks it with Worksheet.MAX_ROWS and Worksheet.MAX_COLS. For more data, we split the rows across several sheets.
7.5. Is FastExcel the same as Alibaba FastExcel or EasyExcel?
No. Alibaba’s FastExcel (cn.idev.excel) is a fork of EasyExcel that became Apache Fesod, with an annotation-based API. The dhatim FastExcel library (org.dhatim) in this article is a separate project with its own API.
8. Conclusion
FastExcel writes and reads large .xlsx files with very little memory. The writer works with Workbook, Worksheet and value(), and a regular flush() keeps the heap flat for a million rows. The reader returns a Stream of rows with typed getters that throw ExcelReaderException on the wrong cell type.
The library trades features for speed, so templates, charts and formula results still need Apache POI. For plain exports and imports, it is the shortest code we can write for big Excel files in Java.
9. References
- FastExcel on GitHub
- FastExcel writer Javadoc
- FastExcel reader Javadoc
- Excel specifications and limits
Happy Learning !!
could you please make a doc on Spring boot API to download Excel file for fast Excel
See if it helps: Spring Boot REST API for Excel File Download
Thank You it really helpful…!
Thanks for the Update, I tried for 4 hours to find a Solution to load 500k records it was completed in 160 Seconds
Thanks for the feedback. Much appreciated.