diff --git a/processor/formatters.go b/processor/formatters.go
index 1d6f787..c970005 100644
--- a/processor/formatters.go
+++ b/processor/formatters.go
@@ -3,10 +3,12 @@
 package processor
 
 import (
+	"bufio"
 	"bytes"
 	"cmp"
 	"encoding/csv"
 	"fmt"
+	"io"
 	"math"
 	"os"
 	"path/filepath"
@@ -495,34 +497,95 @@ func toOpenMetricsFiles(input chan *FileJob) string {
 	return sb.String()
 }
 
+const csvStreamHeader = "Language,Provider,Filename,Lines,Code,Comments,Blanks,Complexity,Bytes,Uloc"
+
+var csvStreamQuoteRegex = regexp.MustCompile("\"")
+
+func csvStreamQuote(s string) string {
+	// Escape quotes then surround with quotes.
+	return "\"" + csvStreamQuoteRegex.ReplaceAllString(s, "\"\"") + "\""
+}
+
+// csvStreamRow builds the raw (unquoted) fields of a csv-stream row. The column
+// order matches toCSVFiles so the same sort functions can be reused for both.
+func csvStreamRow(result *FileJob) []string {
+	return []string{
+		result.Language,
+		result.Location,
+		result.Filename,
+		strconv.FormatInt(result.Lines, 10),
+		strconv.FormatInt(result.Code, 10),
+		strconv.FormatInt(result.Comment, 10),
+		strconv.FormatInt(result.Blank, 10),
+		strconv.FormatInt(result.Complexity, 10),
+		strconv.FormatInt(result.Bytes, 10),
+		strconv.Itoa(result.Uloc),
+	}
+}
+
+func writeCSVStreamRow(w io.Writer, row []string) {
+	_, _ = fmt.Fprintf(w, "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
+		row[0],
+		csvStreamQuote(row[1]),
+		csvStreamQuote(row[2]),
+		row[3],
+		row[4],
+		row[5],
+		row[6],
+		row[7],
+		row[8],
+		row[9],
+	)
+}
+
+// getCSVStreamSortFunc orders csv-stream rows using the same primary key as csv
+// file output and then falls back to comparing every column so the emitted order
+// is fully deterministic regardless of the order rows were discovered in.
+func getCSVStreamSortFunc(sortBy string) func(a, b []string) int {
+	primary := getCSVFilesSortFunc(sortBy)
+	return func(a, b []string) int {
+		if order := primary(a, b); order != 0 {
+			return order
+		}
+		return slices.Compare(a, b)
+	}
+}
+
 // For very large repositories CSV stream can be used which prints results out as they come in
 // with the express idea of lowering memory usage, see https://github.com/boyter/scc/issues/210 for
 // the background on why this might be needed
 func toCSVStream(input chan *FileJob) string {
-	fmt.Println("Language,Provider,Filename,Lines,Code,Comments,Blanks,Complexity,Bytes,Uloc")
+	_ = toCSVStreamWriter(input, os.Stdout)
 
-	var quoteRegex = regexp.MustCompile("\"")
+	return ""
+}
+
+// toCSVStreamWriter writes csv-stream output into the supplied writer. When a sort
+// has been explicitly requested rows are gathered and emitted in that sorted order,
+// otherwise they are streamed out as they arrive to keep memory usage low.
+func toCSVStreamWriter(input chan *FileJob, output io.Writer) error {
+	w := bufio.NewWriter(output)
+	_, _ = fmt.Fprintln(w, csvStreamHeader)
+
+	if !SortBySet {
+		for result := range input {
+			writeCSVStreamRow(w, csvStreamRow(result))
+		}
+
+		return w.Flush()
+	}
 
+	rows := [][]string{}
 	for result := range input {
-		// Escape quotes in location and filename then surround with quotes.
-		var location = "\"" + quoteRegex.ReplaceAllString(result.Location, "\"\"") + "\""
-		var filename = "\"" + quoteRegex.ReplaceAllString(result.Filename, "\"\"") + "\""
+		rows = append(rows, csvStreamRow(result))
+	}
 
-		fmt.Printf("%s,%s,%s,%d,%d,%d,%d,%d,%d,%d\n",
-			result.Language,
-			location,
-			filename,
-			result.Lines,
-			result.Code,
-			result.Comment,
-			result.Blank,
-			result.Complexity,
-			result.Bytes,
-			result.Uloc,
-		)
+	slices.SortFunc(rows, getCSVStreamSortFunc(SortBy))
+	for _, row := range rows {
+		writeCSVStreamRow(w, row)
 	}
 
-	return ""
+	return w.Flush()
 }
 
 func toHtml(input chan *FileJob) string {
@@ -828,6 +891,10 @@ func fileSummarize(input chan *FileJob) string {
 // both to files and to stdout. Not the most efficient way to do it in terms of memory
 // but seeing as the files are just summaries by this point it shouldn't be too bad
 func fileSummarizeMulti(input chan *FileJob) string {
+	if BoundedMemory {
+		return fileSummarizeMultiBounded(input)
+	}
+
 	// collect all the results
 	var results []*FileJob
 	for res := range input {
