BED File Coordinate Systems: 0-Based vs 1-Based
By Dr. Zubair Khalid, DVM, MS, PhD ·

The most common cause of a BED coordinate error is a 1-based start position copied directly into a BED file without subtracting 1. The first thing to check is a feature you can verify independently, such as a gene promoter, a known exon boundary, or a single-base variant. If that feature begins at base 100 in a 1-based annotation but appears at BED start 100 instead of 99, the file is not in BED coordinates or it was converted incorrectly.
BED is a 0-based, half-open format. The start coordinate is included, and the end coordinate is excluded. GFF3, GTF, VCF, and SAM text use 1-based coordinates. BAM and CRAM store 0-based coordinates internally. That mix is the source of most off-by-one errors in interval pipelines. The error is small in bytes and large in consequences. A one-base shift can move a transcription start site, drop the first base of an exon, change overlap counts, and alter downstream statistics.
This guide explains the coordinate model, the causes of 0-based versus 1-based failures, a diagnostic sequence, practical fixes, and prevention practices. It treats BED files as what they are: tab-delimited interval records whose meaning depends on a convention that is often undocumented in the file itself.
The one rule that prevents most errors: the start moves, the end stays
The conversion between BED and a 1-based inclusive format changes only the start coordinate. The end coordinate stays the same.
For a feature that occupies bases 100 through 105 on chromosome 1, a 1-based inclusive record is chr1 100 105. The same feature in BED is chr1 99 105. The BED interval covers 0-based positions 99 through 104, which correspond to 1-based bases 100 through 105. Its length is 105 minus 99, which is 6. The 1-based inclusive length is 105 minus 100 plus 1, which is also 6.
| Feature | 1-based inclusive | BED 0-based half-open | Length |
|---|---|---|---|
| Bases 100 to 105 | chr1 100 105 | chr1 99 105 | 6 |
| Single base 100 | chr1 100 100 | chr1 99 100 | 1 |
| First base of chromosome | chr1 1 1 | chr1 0 1 | 1 |
| Last base of a 1,000 bp chromosome | chr1 1000 1000 | chr1 999 1000 | 1 |
The rule is simple. Convert 1-based start to BED start by subtracting 1. Convert BED start to 1-based start by adding 1. Leave the end alone. The common advice to add 1 to both start and end is wrong. It increases every interval by one base and can push the end past the chromosome length. A feature at the last base of a 1,000 bp chromosome has BED end 1000. Adding 1 produces 1001, which is outside the chromosome.
The half-open convention is not an accident. It makes interval arithmetic clean. The length of a BED interval is end minus start. Two intervals that meet end to start do not overlap. An interval from 0 to 100 and another from 100 to 200 tile 200 bases without a gap or a double-counted base. That property is useful for coverage, overlap, and set operations.
Why BED and annotation formats disagree
BED came from the UCSC Genome Browser as a compact way to display intervals. Its 0-based half-open convention fits array indexing and browser rendering. Gene annotation formats such as GFF3 and GTF grew from biological curation, where saying "the exon runs from base 100 to base 105" is natural. VCF uses a 1-based position because the reference allele starts at that position. SAM text uses 1-based positions for human readability, while BAM stores 0-based positions for compact binary records. These choices are historical and internally consistent. Problems appear when records cross format boundaries.
The BED format itself has a long history of informal specification. A 2022 study created a formal specification from the original BED documentation and tested 80 Bioconda packages that parsed BED files [1]. The authors found interoperability problems that could waste time or, at worst, produce undetected errors in scientific results. That finding matters here because a coordinate convention that lives only in documentation, not in the file, is easy to lose.
| Format | Coordinate base | End convention | Typical content | Practical note |
|---|---|---|---|---|
| BED | 0 | Exclusive | Intervals, peaks, exons | Start included, end excluded |
| GFF3 | 1 | Inclusive | Gene models, features | Start and end both included |
| GTF | 1 | Inclusive | Gene models, transcripts | Same convention as GFF3 |
| VCF | 1 | Position plus REF | Variants | Indels need REF length and normalization |
| SAM | 1 | Position plus CIGAR | Alignments | POS is the leftmost mapped base |
| BAM | 0 | Internal binary | Alignments | Libraries may expose 0-based positions |
| CRAM | 0 | Internal binary | Compressed alignments | Same coordinate logic as BAM |
| narrowPeak | 0 | Exclusive | ChIP-seq and ATAC-seq peaks | BED6 plus four extra columns |
| broadPeak | 0 | Exclusive | Broad chromatin peaks | BED6 plus three extra columns |
| FASTA | None | None | Reference sequences | No coordinate columns |
| FASTQ | None | None | Reads with quality scores | No coordinate columns |
Tools that accept multiple formats must convert internally. GenoMetric Space Explorer reads BED, BroadPeak, NarrowPeak, GTF, and general tab-delimited files with numerical features [2]. PanDepth accepts sorted or unsorted BAM and CRAM files plus GTF, GFF, and BED interval files or a specific window size [3]. PeakLocus accepts bed-like and tabular interval files with a GTF reference and detects coordinates and strand structure [4]. That flexibility is useful, and it is also a risk. A tool that guesses the format can guess wrong.
FASTQ vs FASTA: neither carries coordinates
FASTA and FASTQ are sequence formats, not interval formats. FASTA stores a header and a sequence. FASTQ stores a header, a sequence, a separator, and a per-base quality string. Modern FASTQ commonly uses Phred+33 quality encoding, where each character represents a quality score offset by 33. Neither format has a chromosome, start, or end column. The 0-based versus 1-based question does not apply until reads are aligned to a reference and written as SAM, BAM, or CRAM, or until features are annotated as BED, GFF3, GTF, or VCF. If a pipeline mixes FASTQ and BED files, the coordinate problem enters at the alignment or annotation step, not in the FASTQ file itself.
Causes of BED file 0-based versus 1-based failures, from most common to rarest
Cause 1: a 1-based start copied directly into BED
This is the most common cause. A GFF3 or GTF record for bases 100 through 105 is copied as chr1 100 105. BED interprets the start as 0-based, so the interval begins at 1-based base 101 and ends at base 105. The feature is shifted one base to the right and is one base shorter. The symptom is subtle. Overlap with a known annotation decreases at the left boundary. A motif at the start of the feature disappears. A peak summit shifts.
The fix is to subtract 1 from the start only. Do not subtract 1 from the end. If you subtract 1 from both, the interval becomes 99 through 104, which shifts the feature one base left and drops the last base.
Cause 2: double conversion
A file that is already BED is treated as if it were 1-based, and the start is decremented again. A correct BED interval of 99 through 105 becomes 98 through 105. The feature shifts one base left and gains one base on the left. Double conversion often happens when a pipeline has two conversion steps and nobody checks whether the first step already produced BED.
The symptom is the mirror image of Cause 1. Features start one base earlier and are one base longer. A round-trip test catches it. Convert the file to 1-based and back to BED, then compare checksums on a data-only extract. If the original and the round-trip file differ, a conversion is being applied twice or a comment line is being parsed as data.
Cause 3: mixed formats in one pipeline
A directory may contain GFF3 annotation, VCF variants, and BED peaks. A script may accept all three and apply one conversion rule. Some tools auto-detect format from the first few lines. Auto-detection can succeed on one file and fail on another with a different header or column count. PeakLocus detects coordinates and strand structure from bed-like and tabular files, which shows that format detection is a real design problem, not a trivial one [4].
The fix is to make the format explicit at every step. Convert once at import, write the format name into the filename, and keep a single internal representation. If a tool accepts multiple formats, read its documentation and confirm how it treats each one.
Cause 4: spreadsheet editing
Spreadsheet software can change numbers without warning. It may convert a coordinate to scientific notation, strip leading zeros, round a large integer, or interpret part of a row as a date. Sorting a selected range instead of the whole table can detach coordinates from names. Copying and pasting can shift columns.
BED files should be edited as plain text with a script or a text editor, not in a spreadsheet. If a spreadsheet is unavoidable, import all columns as text and export as tab-delimited text, then validate the result with a script.
Cause 5: mishandled BED12 block fields
BED12 adds thickStart, thickEnd, itemRgb, blockCount, blockSizes, and blockStarts. The blockStarts values are offsets relative to chromStart, not absolute chromosome coordinates. A naive conversion that adds 1 to every numeric column will corrupt the block structure. The correct operation depends on the target representation.
If the target keeps blockStarts as relative offsets, add 1 to chromStart, thickStart, and thickEnd, and leave the relative blockStarts unchanged. If the target requires absolute block starts, compute each absolute start as chromStart plus blockStart, add 1 to each absolute value, and then recompute the offsets relative to the new chromStart. The second approach is easy to get wrong. Rebuilding the BED12 record from exon-level coordinates is safer.
Cause 6: VCF indels and anchor bases
VCF POS is 1-based and points to the first base of the REF allele. For a single-nucleotide variant at position 100, a BED interval of 99 to 100 covers the variant base. For a deletion, VCF includes an anchor base before the deleted sequence. A deletion of bases 101 through 105 may be represented with POS 100 and a REF allele that starts at base 100. Converting POS to BED start and POS to BED end captures only the anchor base, not the deleted span.
The correct interval depends on what you want to represent. If you want the span of the REF allele, compute BED start as POS minus 1 and BED end as POS minus 1 plus the length of REF. If you want the affected bases after the anchor, you need to parse the REF and ALT alleles and apply the variant normalization rules of your target format. Left-align and normalize indels with a tool such as bcftools norm -f reference.fa before conversion. Do not assume that every VCF record represents a single base.
Cause 7: lifting over between assemblies
A liftOver run takes BED and returns BED, so the coordinate system does not change. Boundary effects can still produce one-base differences. An interval can be split, dropped, or mapped to a different chromosome. A chain file can map one endpoint and not the other. If the input was 1-based and was treated as BED before lifting, the error is compounded.
Check the unmapped output, validate several known loci against the target assembly, and inspect intervals that land at the edge of a chain block. Do not use liftOver as a coordinate converter. It is an assembly converter.
Cause 8: hand-parsing SAM or BAM
SAM POS is 1-based. If you read SAM text and write POS directly into BED, every alignment shifts one base right. BAM stores 0-based positions internally, but a library may expose either 0-based or 1-based coordinates depending on its API. The CIGAR string determines the reference span, not the read length. A spliced read can span a large genomic interval while covering only a few exonic bases.
Use a converter that documents its behavior, such as bedtools bamtobed, or use a library that models alignments and intervals. Pygenomics supports BAM, BED, GFF3, and VCF and provides interval operations through a documented API [5]. That is safer than string arithmetic on SAM columns.
Cause 9: zero-length intervals
BED can represent a point with start equal to end. A zero-length interval is useful for insertion sites, cut sites, and some boundary features. A 1-based inclusive format cannot represent a zero-length interval without a convention. Converting a zero-length BED interval by adding 1 to the start produces start greater than end, which is invalid. Some tools reject it, some silently swap the values, and some treat it as a one-base interval.
Keep zero-length intervals in BED when the downstream tool supports them. If the target format has no defined point representation, decide explicitly whether the point becomes the base before, the base after, or a one-base interval. Document the choice.
Cause 10: sorting and chromosome naming
Sorting is not a coordinate-base issue, but it produces symptoms that look like one. Many interval operations require sorted input. Lexicographic sorting gives chr1, chr10, chr2, which is not natural chromosome order. A merge on lexicographically sorted input can produce fragmented or incorrect intervals. Chromosome naming differences, such as chr1 versus 1, produce zero overlaps. That can be misread as a coordinate shift when the real problem is a naming mismatch.
Check both before changing coordinates. List the chromosome names in each file and compare them. Confirm the sort order with a strict check.
Troubleshooting table for BED coordinate failures
| Symptom | Likely cause | How to confirm | Fix | |
|---|---|---|---|---|
| Every feature starts one base later than expected and is one base shorter | 1-based start copied into BED | A known feature at 1-based 100 appears at BED start 100, and its length is one less than expected | Subtract 1 from start only, leave end unchanged | |
| Every feature starts one base earlier and is one base longer | Double conversion | Round-trip BED to 1-based and back, then compare checksums | Restore from the source and convert exactly once | |
| End coordinate equals chromosome length plus one | Added 1 to the end | Compare ends to a chrom.sizes file | Leave end unchanged, clamp only if the source is genuinely out of range | |
| First base of each chromosome is missing | 1-based position 1 copied as BED start 1 instead of 0 | Inspect features that begin at the first base of a chromosome | Subtract 1 from start | |
| Overlap counts are slightly lower at boundaries | One-base truncation from a bad conversion | Inspect a known overlapping pair and compare interval lengths | Correct the conversion and rerun the overlap | |
| BED12 blocks or exons are misaligned | blockStarts treated as absolute coordinates or shifted twice | Compare absolute block starts to exon coordinates in GFF3 or GTF | Rebuild BED12 from exon-level coordinates | |
| VCF-derived intervals cover only the anchor base for deletions | REF length ignored | Inspect a deletion record and compare BED span to REF length | Set end to POS minus 1 plus the length of REF, and normalize indels first | |
| Coverage pileup is shifted upstream by one | SAM POS written as BED start without subtracting 1 | Compare depth at a known position with an independent tool | Use bedtools bamtobed or subtract 1 from POS | |
| Zero overlaps and no coordinate shift | Chromosome naming mismatch, such as chr1 versus 1 | List chromosome names in both files with `cut -f1 file.bed | sort -u` | Harmonize names before any coordinate work |
| Tool rejects records with start greater than end | Zero-length interval converted to 1-based inclusive | Find lines where start equals end | Keep BED or apply a documented point convention | |
| Merge produces fragmented intervals | Input is not sorted in chromosome and start order | Run a strict sort check | Sort with bedtools sort or a natural chromosome sort | |
| One-base differences appear after liftOver | Chain boundary effects or mixed conventions before lifting | Validate known loci and inspect the unmapped output | Re-lift from a correctly converted source and inspect boundaries |
A diagnostic sequence you can run in minutes
- Inspect the first data line. Use
grep -v '^#' file.bed | head -n 1and confirm that columns are tab-separated. Count the fields withawk '!/^#/ {print NF; exit}' file.bed. A three-column file and a twelve-column file need different handling.
- Identify the source format. A GFF3 file has nine columns. A VCF has a header and a different column structure. A BED file has no header requirement. Do not infer the format from the file extension alone.
- Find a feature whose position you know independently. A gene promoter, a restriction site, or a variant from a published record works. Compare its 1-based position to the BED start. If a feature at 1-based base 1 appears at BED start 1, the file is not in BED coordinates.
- Compute the implied length both ways. For BED, length is end minus start. For a 1-based inclusive format, length is end minus start plus 1. If the implied length matches the known feature length under only one formula, you know which convention the file uses.
- Validate coordinates against chromosome sizes. With a two-column chrom.sizes file, run
awk 'NR==FNR{s[$1]=$2; next} $3>s[$1]{print}' chrom.sizes file.bed. This flags ends that exceed the chromosome length. Also flag negative starts and ends smaller than starts withawk '!/^#/ && ($2<0 || $3<$2) {print}' file.bed.
- Compare with an independent interval tool.
bedtools intersect -a file.bed -b known.bed -wa -wbshows overlapping records. ProOvErlap provides another route to quantify overlap and proximity between BED feature sets [6]. If one tool reports overlaps and another reports none, check chromosome naming before changing coordinates.
- Run a round-trip test on a data-only extract. Convert BED to 1-based by adding 1 to the start, convert back by subtracting 1, and compare with
cmp. A difference means the conversion is not symmetric or non-data lines are being parsed.
- Test the edges. Include a feature at the first base of a chromosome, a feature at the last base, a single-base feature, a zero-length feature, and a BED12 record with multiple blocks. Acidbio's BED tests were designed around edge cases and unexpected inputs [1]. A parser that passes only simple three-column records is not validated.
- Use property-based tests for any custom parser. Pygenomics was built with a property-based testing framework and handles BED, GFF3, VCF, and BAM through a documented API [5]. If you write your own parser, test that parsing then writing preserves interval length and coordinates for randomly generated valid records.
Fixes that work
Fix 1: convert with awk and change only the start
For a BED file that needs to become 1-based, add 1 to the start and leave the end unchanged.
awk 'BEGIN{OFS="\t"} !/^#/ && !/^track/ && !/^browser/ && NF>=3 {$2=$2+1; print}' input.bed > output.1based.bed
For a 1-based tabular file that needs to become BED, subtract 1 from the start and leave the end unchanged.
awk 'BEGIN{OFS="\t"} !/^#/ && NF>=3 {$2=$2-1; print}' input.1based.tsv > output.bed
For GFF3 or GTF, the start and end are columns 4 and 5. This example extracts exon features and writes BED6.
awk 'BEGIN{OFS="\t"} $3=="exon" {print $1, $4-1, $5, $3, ($6=="."?0:$6), $7}' annotation.gff3 > exons.bed
For VCF, use a query tool to avoid parsing the header by hand. This example writes an interval spanning the REF allele.
bcftools query -f '%CHROM\t%POS\t%REF\n' variants.vcf | awk 'BEGIN{OFS="\t"} {print $1, $2-1, $2-1+length($3)}' > variants.ref_span.bed
The VCF example is deliberately limited. It spans the REF allele, which is not always the same as the biological interval you want. Normalize and left-align indels first with bcftools norm -f reference.fa. Then decide whether the interval should represent the anchor base, the REF span, or the affected bases after the anchor.
Fix 2: use an interval library instead of string arithmetic
Pygenomics implements interval operations and supports reading and writing BAM, BED, GFF3, and VCF [5]. Its API and command-line interface are designed for scripts and pipelines, and its source code includes type annotations and documentation. Using a library does not remove the need to understand the coordinate model, but it reduces the chance of a hand-written parser treating a header as data or shifting the wrong column.
Fix 3: rebuild BED12 records from exon-level coordinates
Do not add 1 to every numeric column in a BED12 record. Rebuild the record from exon coordinates. For each transcript, collect the exon starts and ends in BED coordinates, compute blockSizes as end minus start for each exon, and compute blockStarts as exon start minus transcript start. Set thickStart and thickEnd according to the coding region. If the target format is 1-based, convert the transcript start, transcript end, thickStart, and thickEnd, and keep blockStarts relative to the new transcript start. A short validation script that compares the rebuilt blocks to the source annotation catches most errors.
Fix 4: validate against a formal specification and a test suite
The BED format has enough edge cases that a custom validator is worth the effort. Acidbio created a formal specification from the original BED documentation and tested 80 Bioconda packages that parsed BED [1]. Use that work as a model. Test empty files, comment lines, track lines, three-column records, twelve-column records, zero-length intervals, and intervals that end at the chromosome boundary. A parser that passes those cases is more trustworthy than one that passes a single happy-path file.
Fix 5: regenerate from the source when a file is suspect
If a BED file has an unknown history, regenerate it from the original GFF3, VCF, or BAM instead of patching coordinates. A patch can compound an earlier double conversion. Regeneration from a documented converter gives you a known starting point. Keep the original file, record the converter version, and compare the regenerated file to the suspect file at a few known loci.
Fix 6: use perturbed interval sets as regression tests
Bedshift perturbs BED files by randomly shifting, adding, and dropping regions [7]. That makes it useful for testing whether your pipeline responds sensibly to small coordinate changes. The Bedshift study also highlights differences between similarity metrics. Jaccard score was most sensitive to added or dropped regions, while coverage score was most sensitive to shifted regions [7]. If a one-base shift changes your metric dramatically, your intervals may be very small or your metric may be more sensitive than you expect. Build a small regression set with known shifts and confirm that your pipeline detects them.
Common Mistakes and Limitations
The most common mistake is adding 1 to both the start and the end when converting BED to a 1-based format. That changes the interval length and can create an end coordinate that exceeds the chromosome. The correct conversion adds 1 to the start only.
A second mistake is assuming that a file extension tells you the coordinate system. A .bed file can contain GFF-like coordinates if someone wrote it by hand. A .txt file can contain valid BED. The convention lives in the data, not the name.
A third mistake is ignoring the assembly. A BED file has no mandatory assembly field. Coordinates from GRCh37 and GRCh38 look identical in structure and mean different things. Record the assembly in the filename, the metadata, and the pipeline configuration.
A fourth mistake is treating VCF POS as a complete interval. POS is a single 1-based anchor. Indels need REF length and normalization. A deletion can span several bases while POS points to the base before the deletion.
A fifth mistake is assuming that every tool handles zero-length intervals the same way. Some tools accept start equal to end. Some reject it. Some convert it to a one-base interval. Check the tool's behavior before converting.
A sixth mistake is validating only one locus. A single known feature can confirm the convention, but it cannot catch BED12 block errors, chromosome boundary errors, or sorting problems. Use several features across different chromosomes and different interval sizes.
The limitations are real. BED has multiple accepted column counts and a history of informal specification. Tools differ on edge cases, and a conversion that is mathematically correct can still be semantically wrong for a particular analysis. The 2022 interoperability study found that BED parsing varied across packages and that formal testing exposed problems that simple use cases did not [1]. Treat every conversion as a code change that needs a test.
Safety note for coordinate edits
Coordinate conversion is destructive when written in place. Work on a copy, keep the original file, and record the command and tool version. BED files can contain human genomic data, so handle them according to your institution's data protection rules. A checksum before and after a conversion is a cheap way to prove that only the intended columns changed.
Prevention practices for BED file coordinate systems
Choose one internal coordinate system and convert at the boundaries. BED 0-based half-open is a good internal choice because interval length is end minus start and overlap arithmetic is clean. Convert GFF3, GTF, VCF, and SAM records to BED at import time. Convert back only when a downstream tool requires a 1-based format.
Put the convention and the assembly in the filename. A name such as sample.GRCh38.0based.bed is more useful than sample_final_v2.bed. Record the conversion command in a pipeline script or a README. If a file is regenerated later, the command is available.
Keep a chrom.sizes file for every assembly and validate every BED file against it. Check that starts are zero or greater, ends are not smaller than starts, and ends do not exceed the chromosome length. Flag zero-length intervals separately so that they are reviewed rather than silently accepted or rejected.
Write assertions into custom code. A simple check before writing a BED record can catch most errors.
def validate_bed(chrom, start, end, chrom_length):
assert start >= 0
assert end >= start
assert end <= chrom_length
Add round-trip tests to the pipeline. Convert a small BED file to 1-based and back, then compare checksums. Add edge-case tests for the first base, the last base, a single base, a zero-length interval, and a BED12 record. Property-based testing, as used in pygenomics, generates many valid records and checks that parsing and writing preserve the interval [5].
Use a formal specification or a validated parser when possible. Acidbio's formal BED specification and test suite are a practical reference for edge-case behavior [1]. A parser that passes a broad test suite is safer than one that has only been tried on one project's files.
Benchmark overlap and coverage metrics with perturbed interval sets. Bedshift can shift, add, and drop regions to create test files [7]. If your pipeline is stable, a one-base shift should produce a predictable change, not a catastrophic drop in overlap. That kind of regression test catches coordinate errors before they reach a biological conclusion.
Frequently Asked Questions
Is BED 0-based or 1-based?
BED is 0-based with a half-open end. The start coordinate is included and the end coordinate is excluded.
How do I convert a 1-based file to BED?
Subtract 1 from the start coordinate and leave the end coordinate unchanged. Do not subtract 1 from both columns.
Why is my BED file shifted by one base?
The most likely cause is a 1-based start copied into BED without subtracting 1. A double conversion produces the opposite shift, where features start one base earlier and are one base longer.
Should I add 1 to both start and end when converting BED to 1-based?
No. Add 1 to the start only. Adding 1 to the end increases the interval length and can push the end beyond the chromosome.
Does bedtools use 0-based coordinates?
Yes. Bedtools uses BED-style 0-based half-open intervals and converts other input formats to that representation internally.
Are GFF and GTF 1-based?
Yes. GFF3 and GTF use 1-based inclusive coordinates, so both the start and end positions are included.
Is VCF 1-based?
Yes. VCF POS is 1-based. Indels need REF length and normalization before you convert them to BED intervals.
Do FASTA and FASTQ files have coordinates?
No. FASTA and FASTQ store sequences, and FASTQ also stores per-base quality scores. They have no chromosome, start, or end fields, so the 0-based versus 1-based question applies only after alignment or annotation.
Sources
- Assessing and assuring interoperability of a genomics file format
- Explorative visual analytics on interval-based genomic data and their metadata
- PanDepth, an ultrafast and efficient genomic tool for coverage calculation
- PeakLocus: A transcript-aware toolkit for RNA-Centric Genomic Annotation
- Pygenomics: manipulating genomic intervals and data files in Python
- ProOvErlap: Assessing feature proximity/overlap and testing statistical significance from genomic intervals
- Bedshift: perturbation of genomic interval sets