How to Convert BAM to CRAM: A Step-by-Step Guide

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

How to Convert BAM to CRAM: A Step-by-Step Guide

By the end of this guide you will have a CRAM file that decodes back to exactly the same alignment records as the BAM you started with, a working index next to it, a short validation log that proves the round trip, and a clear decision about whether to keep the original BAM. You will also know which options are safe, which ones quietly destroy information, and what to do when samtools refuses to read your file three months later.

You need four things on hand before you start:

  • A recent samtools build (1.10 or newer, ideally 1.16 or newer) with htslib compiled in.
  • The exact reference FASTA that the reads were aligned against, plus its .fai index and a .dict sequence dictionary.
  • Free disk space roughly equal to the BAM plus the new CRAM, since both exist during conversion.
  • A checksum utility such as md5sum or sha256sum, and a place to write a small validation log.

Everything below assumes a coordinate-sorted BAM named sample.bam and a GRCh38 FASTA named GRCh38.fa. Substitute your own paths. The commands are the same whether you are converting one file or ten thousand.

What BAM and CRAM Actually Store

SAM is the plain-text alignment format. Each record is one tab-delimited line with 11 mandatory fields (read name, flag, reference name, position, mapping quality, CIGAR, mate reference, mate position, template length, sequence, quality) plus optional tags. Header lines begin with @. SAM is readable by eye and enormous on disk. The SAM/BAM specification is one of the most widely adopted formats in genomics, and most aligners emit it directly [1].

BAM is the same information compressed with BGZF, which is gzip split into blocks. Blocks make random access possible: an index (.bai or .csi) stores virtual file offsets, so a tool can jump to a genomic region without reading the whole file. Nothing about the alignments changes between SAM and BAM. It is a container swap.

CRAM is different in kind. It is not a compressed SAM. CRAM stores each read as a description of how it differs from the reference genome. For a well-mapped read, the encoder records the position, the differences (substitutions, insertions, deletions), the read name, the flags, the quality scores, and the tags. The full read sequence is reconstructed at decode time by walking the reference. This is why CRAM needs a reference and why it is usually much smaller than BAM for the same data.

A CRAM file is organized into containers, each holding slices, and each slice covers a genomic region. Different blocks inside a slice use different codecs: gzip, bzip2, LZMA, and arithmetic coding. Quality scores get their own codec because they compress well on their own, separately from the sequence differences.

The reference is identified by an MD5 checksum recorded in the M5 tag of each @SQ header line. That tag is the MD5 of the uppercased sequence with whitespace removed. When a decoder opens a CRAM, it reads the M5 tag and looks for a reference sequence with a matching checksum. If the checksums disagree, decoding fails. If no checksum is present and no reference is supplied, decoding also fails. This mechanism is what prevents the classic disaster of decoding a GRCh38 CRAM against GRCh37 and producing silently wrong bases.

FeatureSAMBAMCRAM
RepresentationPlain textBGZF-compressed text recordsReference-based encoding
Needs reference to readNoNoYes, unless embedded or reference-free
Typical sizeLargestLargeSmallest for well-mapped short reads
Random access index.sai (rare).bai or .csi.crai or .csi
Human readableYesNoNo
Widely supportedYesYes, by nearly everythingYes, but not by every legacy tool

Format design always involves this trade. Binary and compressed representations are smaller and faster to parse, at the cost of requiring the right library and the right companion files [2]. The same principle shows up across bioinformatics, from k-mer sets to mass spectrometry data, where lossless compression lets a file be reconverted without penalty [3][4].

FASTQ vs FASTA, and why it matters here

These two formats get confused constantly, and the confusion causes real conversion errors.

FASTQ holds sequencing reads before alignment. Each record is four lines: a header starting with @, the nucleotide sequence, a + separator, and one quality character per base. The quality string is the point. FASTQ is what a sequencer produces and what an aligner consumes.

FASTA holds sequences without quality scores. Each record is a header starting with > followed by sequence lines. Reference genomes, transcriptomes, and protein sets are distributed as FASTA.

The practical consequence is simple. To convert BAM to CRAM you need the reference in FASTA format, not the reads in FASTQ format. A CRAM file does not and cannot use the original FASTQ as its reference. If you have a BAM and only the FASTQ files it came from, you cannot reconstruct a CRAM without also finding the reference genome that was used.

Before You Convert: One-Time Setup

Install and check samtools

conda install -c bioconda samtools
samtools --version

Expected output begins with a line like:

samtools 1.19 using htslib 1.19

Version numbers vary. What matters is that you have a samtools 1.10 or newer, because the -O cram,option=value syntax for output format options was introduced in that series. If you are on a shared cluster, load the module instead of installing. If you need bit-for-bit reproducibility across years, pin the tool inside a container image so the version does not drift under you, which is the reproducibility argument behind container generators for bioinformatics [5].

Prepare the reference once

samtools faidx GRCh38.fa
samtools dict GRCh38.fa -o GRCh38.dict
ls -l GRCh38.fa.fai GRCh38.dict

faidx builds the .fai index that gives samtools random access into the FASTA. dict writes a sequence dictionary that includes the M5 checksums and UR locations for every contig. Both files must live next to the FASTA with the standard names, or you must pass them explicitly.

Do this once per reference build per project. Keep the FASTA and both index files for as long as you keep any CRAM derived from them.

Inspect the BAM header

samtools view -H sample.bam | head -5

Expected output resembles:

@HD	VN:1.6	SO:coordinate
@SQ	SN:chr1	LN:248956422
@SQ	SN:chr2	LN:242193529
@PG	ID:bwa	PN:bwa	VN:0.7.17	CL:bwa mem -t 16 GRCh38.fa R1.fq R2.fq

Three things matter in that output. SO:coordinate tells you the file is sorted and therefore indexable. The SN values are contig names, which must match the FASTA exactly. The presence or absence of M5 on each @SQ line determines how strictly the reference will be checked later.

The Step-by-Step Workflow

Step 1: Inventory the input and record a baseline

samtools quickcheck -v sample.bam && echo "quickcheck passed"
samtools view -c sample.bam
samtools flagstat sample.bam > sample.bam.flagstat.txt
ls -l sample.bam
md5sum sample.bam > sample.bam.md5

quickcheck -v is silent on success and prints a diagnostic on failure. It catches truncated files, which are common after an interrupted copy. If it reports a missing EOF marker, re-download or re-copy the file before going further. Converting a truncated BAM produces a truncated CRAM.

Record the read count. You will compare against it later. For a typical whole-genome BAM the number will be in the hundreds of millions, and that is fine.

Step 2: Prove the reference matches before you convert

This step is the one most people skip, and it is the one that decides whether the output is usable.

samtools view -H sample.bam \
  | awk -F'\t' '/^@SQ/{sn=$2; ln=$3; sub("SN:","",sn); sub("LN:","",ln); print sn"\t"ln}' \
  | sort > bam.contigs.tsv

cut -f1,2 GRCh38.fa.fai | sort > ref.contigs.tsv

diff bam.contigs.tsv ref.contigs.tsv && echo "contig names and lengths match"

Expected output is the echoed confirmation line with no diff output above it. Any diff lines mean a name mismatch (chr1 versus 1), a length mismatch, or extra contigs on one side. All three break reference-based encoding.

If contig names differ only by the chr prefix, fix it in the FASTA headers and rebuild the .fai, or use the exact FASTA the aligner used. Do not hand-edit the BAM header to force a match unless you also understand which contigs are affected.

Step 3: Run the conversion

samtools view -@ 8 -T GRCh38.fa -C -o sample.cram sample.bam

That is the whole conversion. Each flag does one thing:

  • -@ 8 uses 8 threads for compression and I/O.
  • -T GRCh38.fa supplies the reference FASTA.
  • -C writes CRAM output.
  • -o sample.cram sets the output path.

On success samtools prints nothing. Check the exit status and the file size:

echo $?
ls -l sample.bam sample.cram

The same command with explicit format options looks like this:

samtools view -@ 8 -T GRCh38.fa -O cram,level=6 -o sample.cram sample.bam

-C and -O cram are equivalent. Use -O when you need to pass options. level=6 sets the compression level, roughly the same trade curve as gzip levels: higher saves space, lower saves time. Levels run from 0 to 9. A cold archive can use 9. A working file usually does not.

samtools view streams records and never reorders them. The output CRAM has the same record order as the input BAM. That property is what makes the record-level checksum in Step 6 valid.

Step 4: Index the CRAM

samtools index -@ 8 sample.cram
ls -l sample.cram.crai

The default index extension for CRAM is .crai. If your downstream tools require the newer CSI index, add -c:

samtools index -c sample.cram

Indexing a CRAM does not require the reference because the index stores container offsets, not sequence. Indexing does require coordinate sorting. If the BAM was not sorted, sort it first with samtools sort and then convert.

Step 5: Validate structure and counts

samtools quickcheck -v sample.cram && echo "cram quickcheck passed"
htsfile sample.cram
samtools view -T GRCh38.fa -c sample.cram
samtools view -c sample.bam

htsfile prints a line identifying the format and CRAM version, for example sample.cram: CRAM version 3.0. The two view -c counts must be identical. If they differ, stop and investigate before deleting anything. A count mismatch usually means a contig name problem, an unmapped-read handling problem, or a truncated input.

For flag-level statistics that also exercise decoding:

samtools view -T GRCh38.fa -b sample.cram | samtools flagstat - > sample.cram.flagstat.txt
diff sample.bam.flagstat.txt sample.cram.flagstat.txt && echo "flagstat identical"

Piping through samtools view -T guarantees the reference is supplied, which sidesteps any dependency on the reference cache.

Step 6: Prove the round trip at record level

Counts alone do not prove that bases, qualities, and tags survived. Compare the actual records.

samtools view sample.bam | md5sum
samtools view -T GRCh38.fa sample.cram | md5sum

Expected output is two lines with the same 32-character hexadecimal digest. If the digests match, every record, every base, every quality character, and every optional tag in the file is identical between the two formats. If they differ, you either used a lossy option (see Step 7) or you have found a genuine problem.

Two caveats keep this test honest. Header lines are excluded because samtools view without -h emits records only. And record order is preserved by streaming conversion, so a mismatch is not explained away by sorting.

For a human-readable header comparison that ignores the added provenance line:

samtools view -H sample.bam | grep -v '^@PG' > bam.header.txt
samtools view -H sample.cram | grep -v '^@PG' > cram.header.txt
diff bam.header.txt cram.header.txt && echo "headers match apart from provenance"

Step 7: Decide deliberately about lossy options

Every option below shrinks the file further and every one of them breaks something specific.

  • lossy_names=1 compresses read names by storing only the parts that differ. It is safe for coverage and variant calling. It breaks any workflow that joins on read names, including many UMI-based deduplication schemes and some fusion callers.
  • no_ref=1 writes a reference-free CRAM that decodes without any external FASTA. The file is self-contained, which is good for data sharing and bad for storage cost, since the reference-derived savings largely disappear.
  • embed_ref=1 embeds only the reference slices that are actually touched by reads. It is a middle path: the file decodes standalone, and the size penalty is smaller than no_ref.

Example combining a conservative compression setting with embedded reference segments:

samtools view -@ 8 -T GRCh38.fa -O cram,level=6,embed_ref=1 -o sample.cram sample.bam

If you use any lossy or reference-free option, say so in your README and in your validation log. The next person to open the file will not be able to tell by looking.

Step 8: Record provenance and retire the BAM

samtools view -H sample.cram | grep '^@PG' > sample.cram.pg.txt
sha256sum sample.cram > sample.cram.sha256

Expected output shows at least two @PG lines: the original aligner invocation and a new samtools line recording the conversion command and version. That provenance chain is the difference between a reproducible archive and an unexplained binary blob. Workflow systems that log an audit trace for every output file solve the same problem at a higher level [6].

Delete the BAM only after Steps 5 and 6 pass. Move the reference FASTA, .fai, and .dict into the same archive. A CRAM without its reference is an unreadable file with a convincing name.

Key Options at a Glance

OptionEffectWhen to change it
-@ NThreads for compression and I/ORaise for large files, lower on shared nodes
-T ref.faSupplies the reference at encode timeAlways set it explicitly
-C or -O cramSelects CRAM outputRequired
level=0 to level=9Compression effort5 or 6 for working files, 9 for cold archive
embed_ref=1Embeds touched reference slicesWhen the CRAM must decode standalone
no_ref=1Writes reference-free CRAMWhen sharing data without a reference
use_lzma=1Enables LZMA codecSmall size gain, slower, needs liblzma
lossy_names=1Compresses read namesOnly if no tool downstream uses read names
version=2.1Writes older CRAM versionOnly for legacy readers
-RRestricts to one read groupSplitting a multi-sample BAM

How Reference Resolution Actually Works

When you pass -T, samtools reads the FASTA through its .fai and encodes against it. The output CRAM records the M5 checksum of each contig it used. That is the reliable path and the one this guide recommends.

When you omit -T, htslib tries a lookup chain. First it looks at the M5 tag in the @SQ line. Then it searches for a matching sequence through the REF_PATH and REF_CACHE environment variables. Left at defaults, REF_PATH may point to a remote reference registry, which means a decoder will attempt a network request. On an air-gapped cluster, that request fails and you get a reference error even though the file is perfectly healthy.

Set a local cache on any shared system:

export REF_CACHE=/shared/ref_cache/%2s/%2s/%s
export REF_PATH=/shared/ref_cache/%2s/%2s/%s
mkdir -p /shared/ref_cache

The %2s placeholders shard the cache by the first characters of the MD5, which prevents one directory from holding millions of files. Populate the cache by converting with -T on a machine that has the FASTA, then copying the cache to the cluster.

samtools view -T ref.fa -C ... always takes precedence over the cache, so when in doubt, pass the reference.

Batch Conversion Across Many Files

Sequential conversion is fine for one file and wasteful for a thousand. Parallelize across files, not just within them, because each samtools view process is limited by compression speed and disk throughput.

ls *.bam | parallel -j 4 'samtools view -@ 4 -T GRCh38.fa -C -o {.}.cram {}'

That runs four files at a time with four threads each, for roughly 16 compression threads in total. Match -j times -@ to the physical core count, not the hyperthread count, or you will spend your time context switching.

Then validate in bulk:

for f in *.cram
do
  b="${f%.cram}.bam"
  a=$(samtools view -c "$b")
  c=$(samtools view -T GRCh38.fa -c "$f")
  if [ "$a" = "$c" ]
  then
    echo "OK $f $a"
  else
    echo "MISMATCH $f bam=$a cram=$c"
  fi
done | tee conversion_audit.txt

Any MISMATCH line is a file to investigate before the source BAM is removed.

For timing and memory on a single file, use the system time:

/usr/bin/time -v samtools view -@ 8 -T GRCh38.fa -C -o sample.cram sample.bam 2> time.log

Read Maximum resident set size and Elapsed (wall clock) time from the log. Memory use for streaming conversion is modest. The dominant costs are compression CPU time and reference I/O.

Common Errors and Their Fixes

[E::cram_get_ref] Failed to populate reference for id 0

No reference was found for the first contig. Pass -T ref.fa, populate REF_PATH and REF_CACHE, or re-encode with embed_ref=1.

A message containing MD5 mismatch while opening a CRAM

The FASTA you supplied does not match the build the CRAM was encoded against. Check the assembly name, the patch level, and whether the FASTA includes alt contigs and decoys. A GRCh38 file with the analysis set is not the same reference as GRCh38 with all alt sequences.

An error naming the .fai file

The FASTA index is missing or stale. Run samtools faidx ref.fa again and confirm the .fai timestamp is newer than the FASTA.

EOF marker is absent. The input is probably truncated

The BAM or CRAM is incomplete, usually from a killed transfer. Re-copy or re-download. Do not convert a file that fails this check.

[E::hts_idx_load3] Could not load local index file

The index is missing or named wrong. For CRAM, the default is .crai. Some tools look for .csi. Regenerate with samtools index and confirm the exact filename.

Conversion succeeds but decoding fails later

The classic cause is a BAM with no M5 tags converted without -T. htslib cannot verify what it encoded against, and the resulting file is only decodable with the exact same FASTA, which nobody recorded. Always pass -T.

Records lost or contigs renamed

Contig naming mismatch between the BAM header and the FASTA. Run the Step 2 diff.

Common Mistakes and Limitations

Converting without an explicit reference. The conversion may appear to succeed because htslib found something in a cache or fell back to an UR tag. Six months later, on a different machine, the file will not open. Pass -T every time and record the path in your log.

Using the wrong assembly. If the BAM has M5 tags, a build mismatch is caught immediately with a checksum error. That is the good case. If the BAM has no M5 tags, there is nothing to compare against, and htslib will happily encode GRCh37 coordinates against GRCh38 sequence. The file decodes, the bases are wrong, and no tool warns you. Fix the BAM header to carry M5 tags before conversion, which you can do by regenerating the header with the correct dictionary.

Deleting the BAM before the checksum comparison. Conversion takes minutes. Verification takes minutes. Deleting early can cost you a re-download of hundreds of gigabytes.

Discarding the reference FASTA. A CRAM archive is a two-file archive at minimum, plus the .fai and .dict. Store them together, with a README naming the assembly and patch level. This is the reference-based counterpart to the interoperability problems that appear when file formats lack formal specifications and tools do not test their handling of edge cases [7].

Assuming CRAM is always smaller. For small BAMs, amplicon panels, and files dominated by unmapped reads, CRAM container overhead can erase the savings. The reference-based benefit scales with how well reads map. Measure before you commit a pipeline to CRAM-only storage.

Assuming every tool reads CRAM. Most current aligners, variant callers, and viewers do. Legacy wrappers, some visualization tools, and older pipeline scripts may not. Test your actual downstream commands against one converted file before batch converting a cohort. Software that parses a shared format incorrectly can produce undetected errors in results, which is why round-trip verification matters more than a successful exit code [7].

Using lossy_names=1 in UMI workflows. Read names carry the UMI in many library preparations. Compressing them destroys the ability to group reads by molecule. Check your dedup tool before enabling this.

Forgetting that CRAM decoding needs random access to the reference. If the FASTA lives on a slow network mount and the .fai is missing, every region query becomes a linear scan. Keep the reference on local fast storage or warm the page cache.

Encryption without a key plan. CRAM, like SAM, BAM, and VCF, can be wrapped in Crypt4GH for encrypted storage and byte-level random access without decrypting to disk [8]. That solves the exposure problem and creates a key management problem. Decide who holds the keys before the archive exists, not after.

Changing a validated pipeline without revalidation. If a clinical or regulatory workflow was validated on BAM, switching the storage format is a change to the pipeline. Treat it as one.

Limitations of this guide. The commands here are standard samtools usage and hold across recent versions, but exact error text varies between htslib releases, and every pipeline has its own tool constraints. For a specific production dataset, confirm behavior on one representative file and consult whoever maintains the downstream tools. Individual project decisions need that local expertise.

Frequently Asked Questions

How do I convert a BAM to CRAM without losing data?

Run samtools view -@ 8 -T ref.fa -C -o out.cram in.bam and then verify with a record-level checksum comparison. The default settings are lossless, so the only ways to lose data are to enable lossy options or to supply the wrong reference.

Is CRAM smaller than BAM?

Yes for well-mapped short-read data, usually by a large margin, because only differences from the reference are stored. The savings shrink for poorly mapped reads, unmapped reads, small files, and amplicon data where container overhead matters.

Do I need the reference genome to read a CRAM file?

Yes, unless the file was written with embed_ref=1 or no_ref=1. The reference is identified by an MD5 checksum in the header, and a mismatch is reported as an error rather than silently producing wrong bases.

What is the difference between FASTQ and FASTA?

FASTQ stores sequencing reads with one quality character per base, in four lines per record. FASTA stores sequences without quality scores, in two lines per record. CRAM needs the reference genome in FASTA format, not the reads in FASTQ format.

Can I convert CRAM back to BAM?

Yes. Use samtools view -T ref.fa -b -o out.bam in.cram and re-index with samtools index out.bam. A lossless round trip returns identical records. A lossy round trip returns records with compressed read names that cannot be restored.

Why does samtools say it cannot find the reference?

The M5 checksums in the CRAM header did not match anything in the supplied FASTA, the reference cache, or the REF_PATH search locations. Pass -T ref.fa explicitly or populate REF_CACHE with the correct sequences.

Should I archive BAM or CRAM?

Archive CRAM for well-mapped short-read data, and archive the reference FASTA alongside it. Keep BAM when downstream tools require it, when the data are long-read or sparsely mapped, or when the workflow depends on exact read names.

Does converting to CRAM change read names or quality scores?

Not with default settings. Both are preserved exactly, which the record-level md5sum comparison confirms. Read names change only if you set lossy_names=1, and quality scores can be transformed only if you explicitly request a lossy quality scheme.

Sources

  1. SamQL: a structured query language and filtering tool for the SAM/BAM file format
  2. MMTF-An efficient file format for the transmission, visualization, and analysis of macromolecular structures
  3. The K-mer File Format: a standardized and compact disk representation of sets of k-mers
  4. Toffee - a highly efficient, lossless file format for DIA-MS
  5. CREDO: a friendly Customizable, REproducible, DOcker file generator for bioinformatics applications
  6. SciPipe: A workflow library for agile development of complex and dynamic bioinformatics pipelines
  7. Assessing and assuring interoperability of a genomics file format
  8. Crypt4GH: a file format standard enabling native access to encrypted data