How to Compress SAM to BAM: A Step-by-Step Guide Using samtools

By Dr. Zubair Khalid, DVM, MS, PhD ·

How to Compress SAM to BAM: A Step-by-Step Guide Using samtools

You have a SAM file, the human-readable text dump of aligned sequencing reads, and you need it in BAM form so sorting, indexing, and downstream tools can work with it efficiently. By the end of this guide you will be able to run the conversion, prove that no records were lost, sort and index the output, and tune compression so the file fits your storage budget without wasting hours of CPU time. You will also know exactly what each error message means when a step fails.

The core workflow needs four things. A working samtools installation (version 1.16 or newer is a safe baseline), the SAM file itself, free disk space roughly equal to two or three times the final BAM size for temporary sort files, and a realistic estimate of available RAM. Nothing else is required. A reference genome is optional for BAM and mandatory only if you later choose CRAM.

What SAM and BAM Actually Are

SAM (Sequence Alignment/Map) is a tab-delimited text format. Every alignment record carries eleven mandatory fields in a fixed order: read name (QNAME), bitwise flag (FLAG), reference name (RNAME), leftmost position (POS), mapping quality (MAPQ), CIGAR string, mate reference (RNEXT), mate position (PNEXT), template length (TLEN), the read sequence (SEQ), and per-base quality scores (QUAL). Optional tags follow as TAG:TYPE:VALUE triplets. Header lines start with @ and describe the file version, the reference sequences (@SQ), read groups (@RG), and the programs that touched the file (@PG).

BAM is the binary equivalent of SAM. It stores the same records and the same header in a compact binary encoding, with bases packed four to a byte and qualities stored as raw bytes rather than ASCII characters. That binary layer is then wrapped in BGZF, the Blocked GNU Zip Format. BGZF is gzip-compatible but splits the stream into independent blocks, each with its own compressed payload and virtual offset. That block structure is the reason a BAM file can be indexed and queried by genomic region. A plain gzip file cannot do that because you must decompress everything from the start.

The size reduction from SAM to BAM therefore comes from two sources: the binary encoding and the gzip compression inside each BGZF block. Both are lossless. A BAM file decompresses back to a SAM file with identical records, identical flags, identical qualities, and identical tags, provided the conversion was done correctly.

Working with these formats is memory-intensive by nature. The elPrep authors, who built a multithreaded framework for processing SAM and BAM files, describe memory management as a key engineering challenge because such programs must manipulate large volumes of alignment data at once [1]. That constraint explains most of the flags in this guide, particularly the sort memory and thread options.

What You Need Before You Start

Check your installation first and record the version. Reproducibility guides almost always skip this, and then months later nobody can explain why the same commands produced different files.

samtools --version

Example output:

samtools 1.19.2
Using htslib 1.19.2
Copyright (C) 2024 [Genome Research](/blog/guides/genome-research) Ltd.

The samtools version and the htslib version are printed separately. They normally match. If they do not, samtools is linking against a different htslib than the one it shipped with, which is common after a partial conda update.

You also need a shell with enough disk. The conversion step needs space for one BAM. The sort step needs space for a temporary copy plus the final sorted BAM. Plan for roughly three times the expected BAM size in free space, and put the temporary directory on fast local disk rather than a network mount.

Step-by-Step: Compress SAM to BAM with samtools

Step 1: Inspect the SAM file

Do not convert blind. Look at the header and count the records first, because those two numbers are your proof of correctness later.

ls -lh reads.sam
grep -c '^@SQ' reads.sam
samtools view -c reads.sam
samtools view -H reads.sam | head -n 5

Example output (your numbers will differ):

-rw-r--r-- 1 user grp 2.4G Nov 10 09:12 reads.sam
25
21430012
@HD	VN:1.6	SO:unsorted
@SQ	SN:chr1	LN:248956422
@SQ	SN:chr2	LN:242193529
@RG	ID:lane1	SM:sampleA	PL:ILLUMINA	LB:lib1

Two things matter here. First, there must be @SQ lines, one per reference sequence. A BAM without @SQ lines cannot be coordinate-sorted or indexed because the tool has no dictionary of reference names and lengths. Second, note the record count (21,430,012 in this example) and the sort order declared in @HD. If the header says SO:coordinate but the records are not actually in coordinate order, downstream steps will fail and the header is lying to you.

If the SAM file has no header at all, stop and fix the aligner invocation that produced it. Converting a headerless SAM yields a BAM that looks fine until you try to index it.

Step 2: Convert to BAM

This is the compression step itself. The view subcommand reads SAM and writes BAM when you pass -b.

samtools view -@ 4 -b -o reads.unsorted.bam reads.sam

The flags:

  • -b selects BAM output. Modern samtools also accepts -O bam, but -b is shorter and universally understood.
  • -o names the output file. Writing through -o is preferred over a shell redirect because it avoids a truncated file if the command fails midway.
  • -@ 4 adds four worker threads for BGZF compression. The main thread still does the parsing.

A successful run prints nothing. That silence is correct. Confirm the file exists and check its size.

ls -lh reads.unsorted.bam

Example output:

-rw-r--r-- 1 user grp 780M Nov 10 09:19 reads.unsorted.bam

The exact ratio depends on how compressible your data are. Read names, quality strings, and repetitive alignment patterns all compress differently. What you can rely on is that the output is smaller than the input and that it round-trips exactly.

One detail worth knowing: samtools writes a @PG line into the output header recording the exact command it ran. That line is your provenance trail. Leave it in place unless you have a specific reason to produce byte-identical headers across runs.

Step 3: Verify the conversion

Never delete the SAM until this step passes. The check has two parts, structural integrity and record count.

samtools quickcheck -v reads.unsorted.bam
echo $?
samtools view -c reads.unsorted.bam

Expected result: quickcheck prints nothing, the exit status is 0, and the count matches the 21,430,012 from Step 1.

samtools quickcheck verifies that the file is a valid BGZF stream and that the end-of-file marker is present. That marker is what catches an interrupted copy or a failed transfer over a network. A truncated BAM often still opens and even returns some records, which is why a record count alone is not sufficient evidence.

You can also compare a few records directly. This is overkill for routine work but useful when you suspect a format handling problem.

samtools view reads.sam | head -n 2 > /tmp/sam_head.txt
samtools view reads.unsorted.bam | head -n 2 > /tmp/bam_head.txt
diff /tmp/sam_head.txt /tmp/bam_head.txt

No output from diff means the first two alignment records are byte-identical as text.

Step 4: Sort the BAM

An unsorted BAM is valid but nearly useless. Most tools expect coordinate order, and indexing requires it.

samtools sort -@ 4 -m 1G -T /scratch/samtools_tmp -o reads.sorted.bam reads.unsorted.bam

The flags:

  • -@ 4 sets four threads. In sort, this covers both the compression of output and the merging of temporary files.
  • -m 1G sets the maximum memory per thread before spilling to a temporary file. Total memory consumption is roughly the thread count multiplied by this value, plus overhead. With -@ 4 -m 1G you should budget around 4 to 5 GB.
  • -T /scratch/samtools_tmp sets the prefix for temporary files. Change this whenever /tmp is small or RAM-backed, because sort will fill it.
  • -o writes the final sorted BAM directly.

Recent samtools releases default -m to 768M per thread. Raising it reduces the number of temporary files and the amount of merge work. Lowering it is the fix when sort dies with a memory allocation failure from bam_sort_core.

Confirm the header now declares coordinate order.

samtools view -H reads.sorted.bam | head -n 1
@HD	VN:1.6	SO:coordinate

If SO:coordinate is still not there, you sorted a file whose header was malformed, or the sort did not complete.

A name sort (-n) is a different operation used for paired-end fix-up tools. A name-sorted BAM cannot be indexed for region queries, so do not substitute it here.

Step 5: Index the sorted BAM

An index is a separate small file that maps genomic intervals to virtual offsets inside the BGZF stream. It is not embedded in the BAM.

samtools index -@ 4 reads.sorted.bam
ls reads.sorted.bam*

Example output:

reads.sorted.bam  reads.sorted.bam.bai

The default index format is .bai, which is limited to references shorter than 512 megabases. For very long genomes or for references with many contigs, build a CSI index instead.

samtools index -c reads.sorted.bam

That produces reads.sorted.bam.csi. Both formats work with samtools view region, but tools differ in which they accept, so check the requirements of whatever consumes the file next.

Test the index immediately with a region query.

samtools view -c reads.sorted.bam chr1:1000000-2000000

If the index is missing or wrong, you get a message like this:

[E::idx_find_and_load] Could not retrieve index file for 'reads.sorted.bam'

That message means the .bai is absent, has the wrong name, or sits in a different directory than the BAM. Indexes are located by filename convention, so moving a BAM without its index breaks region queries even though the BAM itself is intact.

Step 6: Quality-check and archive

Run the standard summary reports and save them next to the BAM. They cost seconds and answer most questions that come up later.

samtools flagstat -@ 4 reads.sorted.bam > reads.sorted.flagstat.txt
samtools idxstats reads.sorted.bam > reads.sorted.idxstats.txt
samtools stats -@ 4 reads.sorted.bam > reads.sorted.stats.txt

samtools flagstat reports totals split by flag category. idxstats reports mapped and unmapped counts per reference and requires the index. samtools stats produces a long, detailed text report that includes insert size distributions and per-cycle quality.

Example flagstat output, truncated:

21430012 + 0 in total (QC-passed reads + QC-failed reads)
0 + 0 secondary
0 + 0 supplementary
20881402 + 0 mapped (97.44% : N/A)
548610 + 0 unmapped (2.56% : N/A)

Note that the total includes secondary and supplementary alignments, which are reported on their own lines. If you need a count of primary reads only, use samtools view -c -F 0x900 reads.sorted.bam. The mask 0x900 removes both secondary (0x100) and supplementary (0x800) records.

Finally, record checksums and clean up.

md5sum reads.sorted.bam reads.sorted.bam.bai > reads.sorted.md5
rm reads.sam reads.unsorted.bam

Keep the unsorted BAM until the sorted and indexed file passes quickcheck and the region query works. Then it can go.

The One-Pass Piped Workflow

For routine production, skip the intermediate files entirely. Use -u, which writes uncompressed BAM, because the next stage is going to compress anyway.

set -euo pipefail
samtools view -@ 4 -u reads.sam \
  | samtools sort -@ 4 -m 1G -T /scratch/tmp_sort -o reads.sorted.bam -
samtools index -@ 4 reads.sorted.bam

The trailing - tells samtools sort to read from standard input. The set -euo pipefail line matters here: without pipefail, a failure in the first command is masked by the success of the second, and you can end up with an empty or partial BAM that looks plausible.

This pipeline saves one full write and read of an intermediate file. On large inputs that is a meaningful fraction of total wall time. It also removes the chance of leaving a stale unsorted BAM on disk and accidentally sorting the wrong file later.

Compression, Threads, and Memory: The Settings That Matter

Compression level

BAM compression level runs from 0 to 9. Level 0 stores uncompressed blocks inside BGZF framing, so the file is large but decompresses almost instantly. Level 1 is fast compression. Level 9 is the smallest file at the highest CPU cost. The default is 6, which sits on the sensible part of the curve for most work.

In samtools view, pass -1 through -9 to set the level. In samtools sort, use -l with the number.

samtools view -@ 4 -b -1 -o reads.fast.bam reads.sam
samtools view -@ 4 -b -9 -o reads.small.bam reads.sam

The practical rule: use level 1 or 2 when the file is an intermediate that will be read once more, and level 6 or above for files you will archive or distribute. The difference between level 6 and level 9 is usually modest in size and substantial in time. Benchmark on one real file rather than trusting a general claim.

Threads

-@ controls BGZF compression and decompression threads. It is a throughput setting, not a content setting. Two runs with different thread counts produce files that decompress to identical content, though the raw bytes may differ because BGZF block boundaries shift with parallel writers. Do not use a byte-level checksum comparison to argue that two BAMs from different thread counts are different files. Compare decompressed content instead.

Keep -@ at or below the number of cores you actually have allocated. Oversubscribing on a shared cluster node slows everything down, including other users' jobs.

Memory

The single most common resource mistake is setting -m high and -@ high at the same time without doing the multiplication. -@ 16 -m 4G requests roughly 64 GB of sort memory. On an 8 GB laptop that fails immediately. On a shared node it may trigger the out-of-memory killer, which looks like a silent crash rather than an error message.

Estimate first: pick the thread count you can actually run, then choose -m so the product fits inside your memory allocation with a margin. If the data are large and memory is tight, accept more temporary files. That is what -T is for.

Key options at a glance

TaskFlagWhat it doesNotes
BAM output from view-bSelects BAM formatHeader included automatically
Output file-oWrites to a named fileSafer than shell redirect
Threads-@ NBGZF worker threadsContent unchanged
Compression level-1 to -9Sets BGZF level in viewDefault is 6
Fast pipe output-uUncompressed BAMUse when piping to sort
Sort memory-mRAM per thread before spillingMultiply by thread count
Temp directory-TPrefix for sort temp filesPut on fast local disk
Sort compression-lOutput level for sortSeparate from view
Name sort-nSorts by read nameCannot be indexed for regions
CSI index-cBuilds .csi instead of .baiNeeded for long references
CRAM output-CWrites CRAMRequires -T ref.fa

Optional: go smaller with CRAM

CRAM is a reference-based format. It stores each read as a description of how it differs from the reference sequence, which typically makes it smaller than BAM for the same alignments.

samtools view -@ 4 -C -T reference.fa -o reads.sorted.cram reads.sorted.bam
samtools index reads.sorted.cram

The -T flag supplies the reference used for encoding. Decoding requires the same reference, and if it is unavailable or different, the data cannot be fully reconstructed. That dependency is the tradeoff. CRAM is excellent for archival storage where the reference is guaranteed to be kept, and a liability where it is not. BAM has no such dependency. It stands alone.

FASTQ vs FASTA: Where SAM and BAM Sit

These four formats are easy to confuse and they serve different roles in the same pipeline.

FASTA is the simplest. Each record starts with a > header line, followed by sequence lines with no quality information. FASTA holds reference genomes, transcript sets, and protein sequences. It answers the question "what is the sequence" and nothing else.

FASTQ holds raw sequencing reads. Each record is four lines: an @ header, the sequence, a + separator line, and a quality string with exactly one character per base. The quality character encodes a Phred score as an ASCII offset. The letter I has ASCII code 73, and with the standard offset of 33 that is a Phred score of 40, meaning a 1 in 10,000 chance that the base call is wrong. FASTQ answers "what did the sequencer see, and how confident was it."

SAM and BAM answer a different question: "where did each read align, and how well." They carry the sequence and qualities from the FASTQ plus the alignment coordinates, CIGAR string, mapping quality, and flags describing pairing and strand. The typical path is FASTQ into an aligner such as BWA-MEM2, minimap2, or STAR, SAM out of the aligner, then BAM for everything downstream.

Two small traps. First, the @ character starts a header line in both FASTQ and SAM, but in SAM it appears in a fixed set of header record types (@HD, @SQ, @RG, @PG, @CO), not in read names. Second, a FASTQ file has no alignment information at all, so converting FASTQ directly to BAM is not a compression step. The reads have to be aligned first.

How to Confirm the Result Is Right

A conversion is correct when four conditions hold.

  1. samtools quickcheck -v exits with status 0 and prints nothing.
  2. The alignment record count in the BAM matches the SAM count from Step 1.
  3. The header contains the full @SQ dictionary and a @PG line recording the conversion.
  4. A region query on the indexed BAM returns a plausible nonzero count for a region you know contains reads.

For a stricter check, compare decompressed records directly with samtools view on both files and cmp or diff. For routine work the four conditions above are sufficient and take under a minute.

Save the flagstat, idxstats, and stats outputs alongside the BAM. When someone asks six months later whether the mapping rate changed between batches, those files answer it without re-running anything.

Common Mistakes and Limitations

Gzipping a BAM file

This is the most common error by a wide margin. Running gzip reads.sorted.bam produces reads.sorted.bam.gz, which samtools cannot index and cannot query by region. You have added a second layer of compression on top of data that is already compressed, gaining almost nothing in size and losing random access entirely. BAM is already compressed. Leave it alone.

Forgetting -b

samtools view reads.sam > out.bam without -b writes SAM text into a file named .bam. The name does not set the format, the flag does. The result is a large text file that some tools will refuse and others will silently misread.

Losing the header

The header carries the reference dictionary. If you convert BAM to SAM without -h, the header is dropped, and converting that SAM back to BAM produces a file that cannot be indexed. When you write SAM output, always confirm the header is present by checking for a leading @HD or @SQ line.

Indexing an unsorted BAM

The error looks like this:

[E::hts_idx_push] Unsorted positions on sequence #1: 14822 followed by 12003
samtools index: failed to create index for "reads.bam"

The fix is to run samtools sort first. The message names the exact reference and positions where order breaks down, which is useful if you suspect the file was only partially sorted.

Sorting by name and then indexing

samtools sort -n orders records by read name, which is correct for paired-end reconciliation tools and wrong for everything else. A name-sorted BAM has no coordinate order, so indexing fails or produces an unusable index.

Moving a BAM without its index

Copies made with cp need the .bai or .csi file copied too. Region queries fail with Could not retrieve index file when the index is missing, even though the BAM is perfectly valid. Copy the pair together or re-index at the destination.

Underestimating sort memory

A sort that runs out of memory gives a low-level allocation failure rather than a friendly message. Compute threads times -m before you submit the job. On shared systems, also check whether the scheduler limits memory per process rather than per job.

Treating BAM as an archival replacement for FASTQ

BAM is a derived product. Aligners change, reference assemblies get revised, and mapping decisions get revisited. Keep the raw FASTQ for anything you might need to re-analyze, and treat the BAM as a working artifact. The same logic applies to CRAM, with the added requirement that the reference be archived alongside it.

Expecting samtools to fix bad data

samtools converts and reports. It does not correct malformed CIGAR strings, repair a header that references sequences absent from the file, remove PCR duplicates, or recompute mapping qualities. Those are separate tools or separate steps. If the input SAM is wrong, the BAM will faithfully preserve the error.

Limitations of the format itself

BAM records the alignment as the aligner produced it, including any ambiguity or error. It does not store the original read order once sorted, so name-sorted and coordinate-sorted copies of the same data are not interchangeable. It also cannot represent more than one alignment per record, which is why secondary and supplementary alignments appear as extra records with specific flags.

For any specific dataset, the right compression choice, memory setting, and downstream toolchain depend on the experiment. Reviewing a particular pipeline against your own data and resources is a job for whoever runs that analysis, not something a general guide can settle.

Frequently Asked Questions

Is BAM smaller than SAM?

Yes, typically several times smaller. The reduction comes from binary encoding plus BGZF compression, and both are lossless. The exact ratio depends on your data, so measure it on one real file rather than assuming a number.

Do I have to sort a BAM before indexing it?

Yes. Indexing requires coordinate order, and samtools index fails on an unsorted file. The conversion itself does not require sorting, so you can produce an unsorted BAM and sort it later.

What does the -@ option do in samtools?

It adds worker threads for BGZF compression and decompression. It changes speed, not content. Files produced with different thread counts decompress to identical data even if the raw bytes differ.

Can I convert a BAM back to SAM?

Yes. Run samtools view -h reads.sorted.bam > reads.sam. The -h flag is required for SAM output, otherwise the header is dropped and the resulting SAM cannot be converted back into an indexable BAM.

Why does samtools index say "Unsorted positions"?

The records are not in coordinate order. Sort the file with samtools sort and index the sorted output. The message names the reference and the two positions where the order breaks.

Should I gzip or zip a BAM file?

No. BAM is already compressed in the BGZF container. Adding gzip or zip breaks random access and indexing while saving almost nothing. To shrink a BAM further, consider CRAM with a reference genome.

What is the difference between BAM and CRAM?

CRAM stores alignments relative to a reference sequence and is usually smaller. BAM is self-contained and needs no reference to decode. CRAM requires the exact same reference at read time, so archive that reference with the file.

How do I check that a BAM file is not corrupt?

Run samtools quickcheck -v file.bam and check the exit status. A silent run with status 0 means the BGZF structure and end-of-file marker are intact. Pair it with a record count comparison against the source SAM.

Sources

  1. Comparing Ease of Programming in C++, Go, and Java for Implementing a Next-Generation Sequencing Tool.