# How to Create a Tabix Index for a VCF File: A Step-by-Step Guide

By the end of this guide you will be able to create a Tabix index for a VCF file, verify that the index is correct, and pull any genomic region out of a multi-gigabyte variant file in a fraction of a second. You will also know how to diagnose the handful of failures that produce broken or silently wrong indexes.

You need a POSIX shell, htslib (the library that ships `bgzip` and `tabix`), and a VCF file. `bcftools` is optional but makes sorting and validation much easier. The whole workflow to create a Tabix index for a VCF file is three steps long: sort the VCF, compress it with `bgzip`, and index it with `tabix -p vcf`. Everything else in this article is about doing those three steps correctly and proving that the result works.

## What a Tabix Index Is and Why a VCF File Needs One

The Variant Call Format (VCF) is a tab-delimited text format. Each data line describes one variant with fields for chromosome, position, identifier, reference allele, alternate allele, quality, filter status, INFO annotations, FORMAT, and per-sample genotypes. The file begins with `##` metadata lines and a single `#CHROM` header line. A VCF is readable, diffable, and easy to inspect, and that is exactly why it is a poor format for random queries at scale.

Without an index, finding every variant in `chr1:1000000-2000000` means opening the file at byte zero, decompressing every block, parsing every line, and discarding almost all of it. A whole-genome VCF from a few hundred samples can run to tens of gigabytes compressed. Scanning it for one locus is wasteful on a laptop and unacceptable on a shared server. The index solves that problem by recording, for every genomic interval, the byte offset of the compressed block where matching records begin.

A Tabix index is that sidecar file. For an input named `cohort.vcf.gz`, tabix writes `cohort.vcf.gz.tbi`. The `.tbi` file is small, usually a few kilobytes to a few megabytes, and it contains three things: a list of sequence names, a binning structure that assigns genomic intervals to hierarchical bins, and a set of virtual file offsets that point into the compressed data. A virtual offset combines the compressed block address with the offset inside the decompressed block. That combination is how htslib jumps directly to a record without decompressing everything before it.

Two compression details matter. First, the VCF must be compressed with BGZF (Blocked GNU Zip Format), not with ordinary gzip. BGZF is a gzip-compatible stream split into independently decompressible blocks, each with its own header and a checksum. Ordinary gzip produces a single continuous stream with no block boundaries, so there is no place for an index to point. Second, BGZF blocks are the unit of random access. When tabix records an offset, it records a block address plus an in-block offset, and a reader can decompress just that block.

The value of this design is easy to see in adjacent formats. BigWig and BigBed files use the same principle, and only the data needed for the current browser view is transmitted rather than the entire file [1]. The same BGZIP plus tabix pattern has been extended to pangenome annotation formats, where GAF files are sorted, compressed with BGZIP, and indexed with HTSlib/tabix so that annotations overlapping a subgraph can be extracted quickly [2]. Tabix is not a VCF-specific trick. It is a general indexing layer for position-sorted, block-compressed, tab-delimited data.

The practical problem is common enough that it has been studied directly. Large VCF files are frequently distributed as stand-alone single files with no companion index, which forces client tools to retrieve and scan more data than the query needs [3]. Building the index once, at the point of publication, removes that cost for every downstream user. The bgzip plus `.tbi` combination is also the reference point against which newer compression schemes are measured, including summary-statistic-aware tools that aim to beat it on both index size and query speed [4].

Two index formats exist. The classic Tabix index (`.tbi`) stores coordinates in a fixed binning scheme that tops out at 512 megabases per sequence. The Coordinate Sorted Index (`.csi`) uses a more flexible binning scheme and supports much larger chromosomes, which matters for wheat, salamander, and other genomes with multi-gigabase chromosomes. For human, mouse, and most common reference genomes, `.tbi` is sufficient. For anything with a contig longer than 512 Mb, use CSI.

### What tabix actually does when you run it

When you run `tabix -p vcf input.vcf.gz`, htslib opens the BGZF stream, reads the header to learn the sequence names, then streams the records once. For each record it computes the bin and the virtual offset, and it stores those in memory. At the end it writes the `.tbi`. The input is read once and never modified. The index reflects the file exactly as it existed at that moment, which is why editing or recompressing the VCF after indexing invalidates the `.tbi`.

That last point causes more silent failures than any other. An index is not a fingerprint. It does not check that the file it points into is the same file it was built from. If you replace `cohort.vcf.gz` with a re-sorted version and leave the old `.tbi` in place, queries will return wrong records or fail with a BGZF read error.

### Why random access needs blocks

A useful mental model is a book with chapter markers. If every page is stacked in one continuous scroll, finding page 412 still requires unrolling everything before it. If the scroll is cut into numbered pages, a reader can jump to page 412 directly. BGZF cuts the compressed VCF into blocks, and the `.tbi` is the list of page numbers. The virtual offset is the page number plus the line position on that page.

This is why `gzip` output cannot be indexed, and why `bgzip` exists. Both produce files that decompress with the same libraries, but only BGZF provides the block boundaries that an index can address.

## Where VCF Fits Among Bioinformatics File Formats

VCF is one member of a small family of formats that bioinformatics work touches constantly. Knowing what each one holds makes the indexing step easier to reason about, and it clarifies why only some of them are indexable with tabix.

**FASTA** stores biological sequence without per-base quality. A record begins with a `>` header line followed by the sequence, wrapped at some line length. FASTA is used for reference genomes, transcriptomes, protein sequences, and consensus assemblies. It has no quality scores and no alignment information. Large FASTA collections are heavy: a set of 1,092 human diploid genomes amounts to about 6.7 TB of uncompressed FASTA, which specialist compressors can reduce to roughly 700 MB by exploiting similarity between genomes [5].

**FASTQ** stores raw sequencing reads. Each record has four lines: an `@` header, the read sequence, a `+` separator, and a quality string with one character per base. The quality string encodes the per-base error probability produced by the sequencer. Most of the entropy in a FASTQ file sits in those quality scores, which makes them both the hardest part to compress and the part that indexing schemes for FASTQ focus on [6][7]. General-purpose gzip was not designed for high-throughput sequencing data and does not exploit the limited alphabet or the similarity between reads [8].

The practical FASTQ versus FASTA distinction is simple. FASTQ is raw data with quality, and FASTA is sequence without quality. FASTQ is what comes off the instrument. FASTA is what you align against or what you produce after assembly and consensus calling. Neither one is a variant format, and neither is what tabix indexes for variant work. A reference FASTA can be indexed with a different tool (`.fai` from `samtools faidx`), but that is a separate index with a separate purpose.

**VCF** stores called variants, not reads. It is position-sorted text, which is what makes it indexable. **BCF** is the binary equivalent, produced by `bcftools`, and can also be indexed with tabix or bcftools. **BED** stores genomic intervals in a zero-based, half-open coordinate system. **GFF and GTF** store feature annotations in a one-based system. BED and GFF files can all be tabix-indexed, with the right preset or column flags, which is why the tabix presets include `bed`, `gff`, `sam`, `vcf`, and `bcf`.

The coordinate convention is worth memorizing because it causes off-by-one bugs. VCF and GFF are one-based, so position 100 means the 100th base. BED is zero-based and half-open, so a BED interval `chr1 99 100` covers the same single base as VCF position 100. When you index a BED file with tabix, the `-0` flag tells tabix that the coordinates are zero-based. The `-p bed` preset already does this, and mixing presets and flags is a reliable way to produce an index that returns shifted results.

### Indexing BED and GFF files with the same tool

The commands are nearly identical once the preset is right.

```
tabix -p bed peaks.bed.gz
tabix -p gff annotations.gff3.gz
```

For a custom tab-delimited file with no preset, specify the columns explicitly:

```
tabix -s 1 -b 2 -e 3 -c '#' custom_regions.tsv.gz
```

Here `-s` is the sequence column, `-b` is the begin column, `-e` is the end column, and `-c` names the comment character. Column numbers are one-based, which trips up people who are used to zero-based arrays in scripting languages.

## Prerequisites and Installation

You need three things: a shell, htslib, and a VCF.

`bgzip` and `tabix` both ship with htslib. The old standalone tabix package is obsolete, and on modern systems the `tabix` binary is the htslib build. Check what you have by running `bgzip -h` and `tabix` with no arguments. Each prints a usage summary. If the shell reports "command not found," install htslib.

Common installation routes:

- Bioconda: `conda install -c bioconda htslib`
- Debian and Ubuntu: `sudo apt install tabix` (the package name is still `tabix` even though the binary comes from htslib)
- macOS with Homebrew: `brew install htslib`
- From source: build htslib from the release tarball and put `bgzip` and `tabix` on your `PATH`

Install `bcftools` as well if you plan to sort, validate, or query. `bcftools` is a separate package in the same family and depends on htslib.

Storage is not a concern. The `.tbi` file is typically a few kilobytes for a small exome VCF and a few megabytes for a whole-genome cohort VCF. The compressed VCF itself is the large object. Plan for twice the VCF size in temporary space during sorting, because `bcftools sort` writes intermediate files before merging them.

## Step-by-Step: Create a Tabix Index for a VCF File

The steps below assume a file called `cohort.vcf.gz`. Adjust names as needed.

### Step 1: Inspect the VCF header and a few records

Before indexing anything, confirm what is in the file and whether it is already sorted.

```
bcftools view -h cohort.vcf.gz | head -30
bcftools view -H cohort.vcf.gz | head -5
```

The header should contain `##fileformat=VCFv4.x`, one `##contig=<ID=...,length=...>` line per sequence, and a `#CHROM` line. The first few records show the column layout. If the file is plain text rather than compressed, drop the `.gz` suffix and use `head` directly.

A quick sort check on a compressed VCF:

```
zcat cohort.vcf.gz | grep -v '^#' | cut -f1,2 | head -20
```

Positions should increase within each chromosome, and chromosomes should appear in the order declared in the header. If they do not, sort before indexing. Tabix will not fix an unsorted file, and it will usually refuse to index one.

### Step 2: Sort the VCF if needed

`bcftools sort` is the standard tool. It reads the header, uses the contig order declared in `##contig` lines, and writes a coordinate-sorted VCF.

```
bcftools sort -Oz -o cohort.sorted.vcf.gz cohort.vcf.gz
```

The `-Oz` flag writes compressed VCF output in BGZF format. That matters because BGZF output from `bcftools` is already suitable for tabix, and you do not need a separate `bgzip` pass. If you prefer plain text output, use `-Ov` and compress afterward.

For very large files, give `bcftools sort` a temporary directory on a fast disk with `-T /scratch/tmp`. Sorting a whole-genome VCF can require several times the file size in temporary space. Sorting is also where most of the wall-clock time goes. Indexing itself is fast because it is a single streaming pass.

### Step 3: Confirm contig names and order

```
bcftools view -h cohort.sorted.vcf.gz | grep '^##contig'
```

The IDs printed here are the names you will use in queries and the names tabix records in the index. `chr1` and `1` are different strings. If the VCF uses `1` and you query `chr1`, tabix returns nothing and no error. Decide on one naming convention before you build the index and stick to it across the project.

If the header lacks `##contig` lines, add them before sorting. Tools that emit VCF without contig declarations are common enough that this is a routine fix. Without those lines, `bcftools sort` falls back to the order in which contigs appear in the data, and tabix records whatever names it encounters.

### Step 4: Compress with bgzip

If the file is already BGZF-compressed (for example, output from `bcftools -Oz`), skip this step. If it is plain VCF, compress it now.

```
bgzip -c cohort.sorted.vcf > cohort.sorted.vcf.gz
```

The `-c` flag writes to standard output, which preserves the original file. Without `-c`, `bgzip` replaces the input with the compressed version. Add `-@ 4` to use four threads and `-l 9` for maximum compression if disk space matters more than time. The default compression level is a reasonable compromise between speed and size.

Do not use `gzip`. A gzip-compressed VCF is not indexable. If you already ran `gzip`, decompress and recompress with `bgzip`.

```
gzip -dc cohort.vcf.gz | bgzip -c > cohort.bgzf.vcf.gz
```

### Step 5: Run tabix

```
tabix -p vcf cohort.sorted.vcf.gz
```

There is no output on success. The command creates `cohort.sorted.vcf.gz.tbi` next to the input. The `-p vcf` preset sets the sequence column to 1, the begin column to 2, the comment character to `#`, and the coordinate system to one-based. Those defaults are correct for VCF, which is why the preset exists.

If an index already exists and you want to rebuild it, add `-f`.

```
tabix -f -p vcf cohort.sorted.vcf.gz
```

If your contigs exceed 512 Mb, build a CSI instead.

```
tabix -C -p vcf cohort.sorted.vcf.gz
```

The `-C` flag writes `cohort.sorted.vcf.gz.csi` instead of a `.tbi`.

### Step 6: Verify the index

```
ls -l cohort.sorted.vcf.gz cohort.sorted.vcf.gz.tbi
```

You should see two files, with the `.tbi` much smaller than the data file. An index that is zero bytes means the build failed.

List the sequences recorded in the index:

```
tabix -l cohort.sorted.vcf.gz
```

The output is one sequence name per line, in the order stored in the index. If the list is empty or missing chromosomes you expected, the header was wrong or the file was not sorted.

Query a known region:

```
tabix cohort.sorted.vcf.gz chr1:10000-20000
```

You should see the VCF data lines that fall in that interval, in the same tab-delimited form as the original file. The header is not printed by default. Add `-h` if you want it:

```
tabix -h cohort.sorted.vcf.gz chr1:10000-20000
```

A record-count sanity check is worth doing once per project. Count the records on a chromosome in the compressed file and compare with what tabix returns for the whole chromosome.

```
zcat cohort.sorted.vcf.gz | awk '$1=="chr1"' | wc -l
tabix cohort.sorted.vcf.gz chr1 | wc -l
```

The two numbers should match. If the first is larger, the index is stale or the file was edited after indexing.

### Step 7: Query the way you will actually use it

Single region:

```
tabix cohort.sorted.vcf.gz chr1:1000000-2000000
```

Whole chromosome:

```
tabix cohort.sorted.vcf.gz chr1
```

Multiple regions from a file, one per line in `regions.txt`:

```
tabix -R regions.txt cohort.sorted.vcf.gz
```

With bcftools, which reads the `.tbi` automatically:

```
bcftools view -r chr1:1000000-2000000 cohort.sorted.vcf.gz
```

If bcftools cannot find the index, it reports `[E::idx_find_and_load] Could not retrieve index file for 'cohort.sorted.vcf.gz'`. That message almost always means the `.tbi` is missing, misnamed, or in a different directory from the VCF.

## tabix Options That Matter

| Option | What it does | When to use it |
|--|--|--|
| `-p vcf` | Applies the VCF preset: sequence column 1, begin column 2, comment `#`, one-based | Every VCF and BCF index build |
| `-p bed` | BED preset: zero-based, sequence 1, begin 2, end 3 | BED interval files |
| `-p gff` | GFF preset: one-based, sequence 1, begin 4, end 5 | GFF and GTF annotation files |
| `-f` | Overwrites an existing index | Rebuilding after recompression |
| `-C` | Writes a CSI index instead of a TBI | Contigs longer than 512 Mb |
| `-s`, `-b`, `-e` | Sets sequence, begin, and end columns manually | Custom tab-delimited files with no preset |
| `-c` | Sets the comment character | Files with `#` headers that are not VCF |
| `-0` | Declares zero-based coordinates | BED-like files |
| `-l` (query mode) | Lists sequence names in the index | Verification |
| `-h` (query mode) | Prints header lines with query results | When downstream code expects the VCF header |
| `-R` (query mode) | Reads regions from a file | Batch queries |

Two flags that are easy to confuse: `-b` sets the begin column and `-e` sets the end column. For a VCF, end equals begin because each variant is a single base, and the preset handles that. For interval formats, you need both. Passing `-p vcf` and then also passing custom column flags usually produces an index that is subtly wrong, because the explicit flags override the preset.

## A Worked Example with a Small VCF

Create a three-record VCF called `example.vcf`:

```
##fileformat=VCFv4.2
##contig=<ID=chr1,length=248956422>
##contig=<ID=chr2,length=242193529>
#CHROM	POS	ID	REF	ALT	QUAL	FILTER	INFO
chr1	10000	rs1	A	G	50	PASS	.
chr1	20000	rs2	C	T	60	PASS	.
chr2	15000	rs3	G	A	40	PASS	.
```

Compress and index:

```
bgzip -c example.vcf > example.vcf.gz
tabix -p vcf example.vcf.gz
```

Both commands are silent. Confirm the files:

```
ls -l example.vcf.gz example.vcf.gz.tbi
```

The data file is a few hundred bytes and the index is smaller. Query `chr1:1-15000`:

```
tabix example.vcf.gz chr1:1-15000
```

Expected output:

```
chr1	10000	rs1	A	G	50	PASS	.
```

Only one of the two `chr1` records falls in the requested interval. The `chr2` record is not returned. Add `-h` and the header lines appear above the record.

List the sequences:

```
tabix -l example.vcf.gz
```

Expected output:

```
chr1
chr2
```

This example is small enough that a full scan would also work, but the commands are identical at whole-genome scale. The index does the same job, just over a much larger address space.

## How to Check the Index Is Correct

A correct index satisfies four properties. Check all four before you publish or share a VCF.

1. **The index file exists and is non-empty.** `ls -l` should show a plausible size. A zero-byte `.tbi` is a failed build.
2. **`tabix -l` returns every contig you expect.** Missing contigs usually mean missing `##contig` header lines or an unsorted region that tabix skipped.
3. **A region query returns the records you can see in the file.** Pick a region with a known variant from the header or from a previous run and confirm that `tabix file.vcf.gz chr:start-end` returns it.
4. **Record counts match.** `tabix file.vcf.gz chr1 | wc -l` should equal the number of `chr1` data lines in the decompressed file.

For a stronger check, use `htsfile`, which is also part of htslib:

```
htsfile example.vcf.gz
```

The output identifies the format and compression, for example `VCF compressed with BGZF`. If `htsfile` reports plain

## Sources

1. [BigWig and BigBed: enabling browsing of large distributed datasets.](https://pubmed.ncbi.nlm.nih.gov/20639541/)
2. [Efficient indexing and querying of annotations in a pangenome graph.](https://pubmed.ncbi.nlm.nih.gov/39464141/)
3. [FAIR privacy-preserving operation of large genomic variant calling format (VCF) data without download or installation.](https://pubmed.ncbi.nlm.nih.gov/38827109/)
4. [STABIX: summary-statistic-based GWAS indexing and compression.](https://pubmed.ncbi.nlm.nih.gov/40315153/)
5. [GDC 2: Compression of large collections of genomes.](https://pubmed.ncbi.nlm.nih.gov/26108279/)
6. [Indexing k-mers in linear space for quality value compression.](https://pubmed.ncbi.nlm.nih.gov/31856669/)
7. [LCQS: an efficient lossless compression tool of quality scores with random access functionality.](https://pubmed.ncbi.nlm.nih.gov/32183707/)
8. [SCALCE: boosting sequence compression algorithms using locally consistent encoding.](https://pubmed.ncbi.nlm.nih.gov/23047557/)