Sanity check · Single-Cell ATAC-seq
How to Choose Cell QC Thresholds in Single-Cell ATAC-seq
The QC axes that matter in scATAC-seq aren't the ones your scRNA-seq muscle memory reaches for, and a copy-pasted vignette cutoff will quietly delete a real cell population.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 6 min read
You've run Signac or ArchR tutorials on 10x scRNA-seq before, so when the ATAC or Multiome data lands you reach for the same instinct: filter on counts, filter on a mito-like metric, move on to clustering. Then you paste the PBMC vignette's nCount_peaks > 9000 & TSS.enrichment > 4 into your own dataset and either lose 80% of your cells or keep almost everything, and you can't tell which number is wrong or why.
The stakes are not cosmetic. In scATAC-seq the QC metrics that actually separate real nuclei from debris, TSS enrichment, nucleosome signal, fragments per cell, are all sequencing-depth and chromatin-state dependent. A threshold tuned on a deeply sequenced 10x PBMC dataset encodes that dataset's depth and PBMC's accessibility profile, not a universal biological constant. Apply it somewhere else and you don't just lose junk: you can delete an entire rare cluster and never know it happened, because the cells are just gone before you ever cluster.
This page gives you a short, ordered set of checks to run before you trust any cutoff, starting with confirming which metrics even apply to your assay, plus concrete fixes for the most common failure modes (Multiome vs ATAC-only, tumor or rare-cell-type samples, pooled peak calling). Budget about an hour: most of it is looking at plots you'd otherwise skip.
What it looks like when it's happening
- You paste Signac's PBMC vignette filter (`nCount_peaks` 9,000, 100,000, `TSS.enrichment > 4`) into your own object and lose most of a sample, or barely lose anything, with no obvious reason why.
- Your object doesn't have a `percent.mt` column at all, or when you compute mitochondrial fragment fraction it doesn't cleanly separate low- from high-quality cells the way it did in scRNA-seq.
- The TSS enrichment or nucleosome signal distribution is visibly bimodal, and a single cutoff you picked slices through the middle of what looks like a real, coherent population.
- After filtering with vignette defaults, a cell type you know should be present (plasma cells, a rare progenitor, a small quiescent population) is missing entirely from clustering.
- The genome-wide fragment size histogram is flat or smeared instead of showing the expected ~150 bp nucleosome periodicity, but only for a subset of cells.
- On Multiome data, applying the ATAC-only PBMC thresholds throws out almost every cell because per-cell ATAC depth is inherently lower when RNA and ATAC share the same nucleus capture.
- A UMAP or LSI embedding colored by pass/fail QC shows one entire cluster marked 'removed' rather than diffuse debris at the periphery.
Why it happens
scATAC-seq doesn't measure transcripts, so the scRNA-seq QC vocabulary, nFeature_RNA, percent.mt from mitochondrial transcript fraction, has no direct equivalent. What Tn5 transposase actually does is cut accessible (nucleosome-free) DNA and insert sequencing adapters. Per-cell library quality is therefore captured by different metrics: fragment counts (nCount_peaks / nFrags, library complexity), TSS enrichment (ratio of Tn5 insertions at annotated transcription start sites versus flanking background, since real open chromatin concentrates around active promoters), and nucleosome signal (the ratio of mononucleosomal 147, 294 bp fragments to nucleosome-free <147 bp fragments). A dying or lysed cell has de-chromatinized, exposed DNA everywhere, so Tn5 cuts it near-randomly across the genome: TSS enrichment goes flat and the fragment-length periodicity disappears.
The thresholds you find in tutorials are fit to one dataset's depth and chromatin background, not derived from first principles. Signac's PBMC vignette values (nCount_peaks 9,000, 100,000, pct_reads_in_peaks > 40, TSS.enrichment > 4, nucleosome_signal < 4) come from a deeply sequenced 10x PBMC run. Signac's own Multiome vignette relaxes these substantially (nCount_ATAC 1,800, 100,000, TSS.enrichment > 1, nucleosome_signal < 2) because paired RNA+ATAC capture inherently yields lower per-cell ATAC depth. ArchR's defaults (minTSS = 4, minFrags = 1000, maxFrags = 100000) encode a third set of assumptions again. None of these numbers transfer cleanly to a tumor biopsy, a low-input tissue, or a different sequencing depth, and the Signac maintainers say this explicitly: there is little consensus on scATAC-seq QC metrics, and thresholds should come from your dataset's own distributions, not a copied number.
Biology compounds the technical problem the same way it does in scRNA-seq mito filtering. Cell types differ systematically in chromatin accessibility, TSS enrichment, and library complexity, tumor cells and cardiomyocytes run hot on mitochondrial fraction in the RNA world; in the ATAC world, quiescent or low-input cell types (plasma cells, some stem/progenitor populations) naturally sit lower on fragment counts and TSS enrichment without being technically bad. Filtering each metric independently against a single global cutoff, without checking who you're removing, is exactly the failure mode described for scRNA-seq mito thresholds: you protect specificity for the majority population and quietly delete a minority one.
There's a second-order version of this problem in pooled analyses: per-cell sequencing depth differences across samples or batches are a major technical confound, and if your fragment-count cutoff is set once on the pooled matrix, cells from a shallower-sequenced batch fail QC not because they're bad but because their whole batch was sequenced less deeply. That looks like a biological difference in downstream clustering when it's actually a depth artifact.
The checks
Run them in order. Each one tells you what healthy looks like and what the problem looks like.
0/9 checked · saved in this browser
Before filtering anything, inspect
pbmc@meta.data(Signac) orgetCellColData(proj)(ArchR) and list the columns you actually have. Don't assumepercent.mtornFeature_RNAexist or mean anything here, ATAC objects carrynCount_peaks/nFrags,TSS.enrichment,nucleosome_signal,pct_reads_in_peaks, andblacklist_ratioinstead.- Healthy
- A metadata table with the ATAC-specific columns above, and if it's a Multiome object, separate `nCount_ATAC` and `nCount_RNA` columns rather than one merged count.
- Red flag
- You're about to filter on `percent.mt` or `nFeature_RNA` values that were computed for the RNA modality, or that don't exist and got silently recycled from an old script.
Violin- or density-plot each QC metric separately, per sample, before applying any filter.
rVlnPlot( object = pbmc, features = c("nCount_peaks", "TSS.enrichment", "nucleosome_signal", "pct_reads_in_peaks", "blacklist_ratio"), pt.size = 0, ncol = 5 )- Healthy
- Each metric shows a roughly unimodal distribution (often with a long right tail on fragment counts), with a center that reflects this sample's own sequencing depth.
- Red flag
- A clearly bimodal distribution on TSS enrichment or nucleosome signal, a single cutoff placed at the midpoint will slice through the boundary between two real populations, not just remove a tail of junk.
Run the Signac PBMC-vignette filter and the ArchR default filter as reference points, then check how many cells each keeps versus your own distributions. In ArchR the equivalent call is
createArrowFiles(inputFiles, sampleNames = names(inputFiles), minTSS = 4, minFrags = 1000, maxFrags = 100000).rpbmc_default <- subset( x = pbmc, subset = nCount_peaks > 9000 & nCount_peaks < 100000 & pct_reads_in_peaks > 40 & blacklist_ratio < 0.01 & nucleosome_signal < 4 & TSS.enrichment > 4 ) ncol(pbmc); ncol(pbmc_default)- Healthy
- The default filter keeps a plausible majority of cells (roughly consistent with your expected cell-loading and viability), and the fraction retained is in the same ballpark whether you use Signac's or ArchR's defaults.
- Red flag
- The vignette defaults keep under half your cells, or keep almost all of them regardless of quality, either way, the defaults don't fit this dataset's depth and you need your own numbers, not theirs.
Plot the genome-wide fragment length distribution with
FragmentHistogram()(Signac) orplotFragmentSizes()(ArchR), split by sample.rFragmentHistogram(object = pbmc, group.by = "orig.ident")- Healthy
- Clear periodicity at ~150 bp intervals (the nucleosome ladder) plus a nucleosome-free peak below 147 bp, the signature of clean Tn5 cutting in properly chromatinized nuclei.
- Red flag
- A flat or smeared distribution with no periodicity across the whole sample points to a degraded-library or protocol problem, not something a per-cell count cutoff will fix; filtering individual cells won't rescue a bad library prep.
Split cells into provisional high/low groups at your candidate TSS threshold and plot the aggregate TSS enrichment profile for each group with
TSSPlot().rpbmc$high.tss <- ifelse(pbmc$TSS.enrichment > 2, "High", "Low") TSSPlot(pbmc, group.by = "high.tss") + NoLegend()- Healthy
- The high group shows a sharp peak centered on the TSS; the low group is flat with no enrichment, a clean separation between real signal and background.
- Red flag
- The "low" group still shows a visible, if smaller, peak at the TSS. That means your cutoff is cutting into genuine chromatin signal, not just removing background.
On the log scale, compute the median and median absolute deviation (MAD) of each metric and set thresholds at a fixed number of MADs from the median, per sample rather than on the pooled matrix.
rqc <- pbmc@meta.data log_counts <- log10(qc$nCount_peaks) med <- median(log_counts) mad_val <- mad(log_counts) lower <- 10^(med - 3 * mad_val) upper <- 10^(med + 3 * mad_val) c(lower = lower, upper = upper)- Healthy
- Thresholds land in a plausible range, adapt automatically when you rerun this on a new sample or batch, and roughly track (without exactly matching) the published vignette defaults.
- Red flag
- The MAD-derived range diverges wildly from any published default, that's a signal the sample itself has a depth or quality problem a fixed cutoff would have silently absorbed as "normal."
Flag pass/fail status on the existing (or a quick provisional) LSI/UMAP embedding and color by it.
rpbmc$qc_pass <- pbmc$nCount_peaks > lower & pbmc$nCount_peaks < upper & pbmc$TSS.enrichment > 4 & pbmc$nucleosome_signal < 4 DimPlot(pbmc, group.by = "qc_pass")- Healthy
- Failed cells scatter diffusely at the periphery of the embedding, the debris zone, rather than concentrating inside one well-formed cluster.
- Red flag
- An entire, otherwise coherent cluster is marked failed. You just deleted a real cell population; go back and loosen the metric that's cutting through it, per the Signac maintainers' own guidance to inspect QC in low-dimensional space before trusting a cutoff.
For the cells your filter removed, look at gene activity scores (Signac's
GeneActivity()) or, in Multiome data, the paired RNA profile, and ask whether they carry a real, specific signature rather than noise.- Healthy
- Removed cells show no coherent marker signal, consistent with debris, doublets, or dying cells.
- Red flag
- Removed cells show a clean, specific gene-activity or RNA signature (e.g., plasma-cell or quiescent-cell markers). That's biology you're throwing away, not junk.
Re-run the filter at values roughly 20% above and below your chosen cutoff for each metric and compare resulting cell counts and cluster composition.
- Healthy
- Cell counts and downstream cluster structure change gradually and proportionally as you nudge the threshold.
- Red flag
- Crossing one specific value makes an entire cluster appear or vanish. That threshold is doing biological filtering, not just removing technical junk, and deserves a manual look before you lock it in.
What to do about it
Derive per-sample MAD-based thresholds instead of copying vignette numbers
When: The default Signac PBMC or ArchR thresholds clearly don't fit your sequencing depth or tissue (Multiome data, shallow sequencing, non-PBMC tissue).
Compute median and MAD on the log scale for each QC metric (fragment counts, TSS enrichment) separately per sample or batch, and set cutoffs at a fixed number of MADs from that sample's own median, rather than applying one global number across pooled samples.
Caveat: MAD assumes a roughly unimodal distribution. If a sample truly contains two distinct quality populations (not just one tail), MAD on the pooled distribution still miscalls one of them, check the histogram shape (Check 2) before trusting the MAD number blindly.
Rescue whole clusters that fail QC by checking them against biology first
When: Overlaying pass/fail status on the embedding (Check 7) shows an entire coherent cluster marked failed.
Pull gene activity scores or paired RNA markers for that cluster; if it carries a specific, plausible signature, manually raise or lower the specific metric that's cutting through it for that population, instead of changing the global cutoff for everyone.
Caveat: This is a manual, subjective call. Document exactly which cells you rescued and the metric/threshold you adjusted, it directly affects reproducibility and is the first thing a reviewer or collaborator will ask about.
Use Multiome-specific relaxed thresholds when RNA and ATAC are paired
When: You're analyzing 10x Multiome data (paired ATAC + RNA from the same nucleus), not ATAC-only.
Start from Signac's Multiome vignette thresholds (nCount_ATAC 1,800, 100,000, TSS.enrichment > 1, nucleosome_signal < 2) rather than the ATAC-only PBMC defaults, since paired capture inherently lowers per-cell ATAC depth.
Caveat: Don't relax every metric uniformly just because it's Multiome, check your own per-cell depth distribution first, since different Multiome chemistries and cell-loading targets shift the depth profile differently.
Recall peaks per cluster before finalizing QC on rare cell types
When: Cells that look chronically "low quality" on fragment count belong to a rare population whose accessible regions weren't captured by peaks called on the pooled pseudo-bulk.
Run an initial coarse clustering, call peaks per cluster, requantify the peak-by-cell matrix against the union peak set, and only then finalize your QC thresholds.
Caveat: This is an iterative loop (cluster → call peaks → requantify → re-cluster), not a one-pass fix, budget for at least one extra round of analysis, and expect thresholds to shift once you're quantifying against the better peak set.
Reserve fixed vignette thresholds for a first-pass sanity check, not the final filter
When: You need a quick initial look at a new dataset before investing time in per-sample tuning.
Apply the published Signac or ArchR defaults as a first pass, but treat the resulting cell count and the checks above (distribution shape, embedding overlay) as a gate, if they fail, move to MAD-based per-sample thresholds before you cluster for real.
Caveat: Never report results from the first-pass filter as final. It's a diagnostic step, and treating it as the answer is exactly the mistake this page is about.
Consider threshold-free, periodicity-based QC for large or heterogeneous cohorts
When: You're processing many samples and want QC calls that don't depend on a manually chosen cutoff per sample.
Evaluate a periodicity-based tool such as PEAKQC, which infers per-cell quality from fragment-length periodicity via wavelet denoising instead of a predefined threshold, and reconcile its calls against your Signac/ArchR filtered object.
Caveat: Published in 2025 and not yet integrated into Signac or ArchR pipelines, expect to run it as a separate pass and manually cross-check disagreements rather than dropping it in as a drop-in replacement.
When not to "fix" it
If a cluster that fails your QC cutoff turns out to carry a real, specific gene-activity or paired-RNA signature (a known rare cell type, a tumor population with genuinely altered chromatin, a quiescent population with naturally lower library complexity), don't force it to match the reference thresholds, that's biology, and filtering it out to match a tutorial's cell count is the exact mistake this page warns about. Similarly, if depth differences across samples or batches are something you plan to model explicitly downstream (a depth or batch covariate in your statistical model) rather than remove, don't chase perfectly matched per-sample thresholds designed to erase that variation, over-aggressive per-batch filtering can itself distort composition and create the appearance of a batch effect where sequencing depth was already going to be handled correctly by your model.
Five things experienced analysts do here
- Never filter on a cutoff computed from the pooled matrix across samples with different sequencing depths, compute and apply thresholds per sample or per batch, then merge.
- Plot the raw distribution of every QC metric before you set a single cutoff number; deciding the threshold first and checking the plot after is backwards and it shows in the results.
- Treat `percent.mt` as scRNA-seq baggage in ATAC-only data, confirm your pipeline even reports a mitochondrial fragment fraction before letting it override TSS enrichment or nucleosome signal decisions.
- Always visualize which specific cells got removed by overlaying pass/fail on an embedding, not just how many cells were removed, a count alone hides whether you deleted debris or a real cluster.
- Write down the exact thresholds and the reasoning behind them per sample in your analysis notebook; rerunning the same script on a new sample without revisiting these numbers is how this mistake gets repeated in the next project.
Questions people ask
- What mitochondrial percentage cutoff should I use for scATAC-seq?
There usually isn't a standard mitochondrial-fraction QC metric in ATAC-only pipelines the way there is
percent.mtin scRNA-seq, Signac and ArchR QC is built around TSS enrichment, nucleosome signal, and fragment counts instead. If your pipeline does report a mitochondrial fragment fraction, treat it as a secondary check, not your primary filter, and inspect its distribution per sample before setting any number.- How many fragments per cell should I require in scATAC-seq?
Signac's PBMC vignette uses
nCount_peaksbetween 9,000 and 100,000; ArchR's defaults areminFrags = 1000andmaxFrags = 100000; Signac's Multiome vignette relaxes the ATAC-only floor down to 1,800. None of these are universal, they reflect each dataset's own sequencing depth, so compute your own distribution (Check 2) and set a threshold, ideally MAD-based, from that.- What TSS enrichment score counts as a good cell in scATAC-seq?
Signac's PBMC vignette uses
TSS.enrichment > 4, ArchR's default isminTSS = 4, and Signac's own Multiome vignette relaxes this toTSS.enrichment > 1because paired capture reduces per-cell ATAC depth. Pick a value by looking at the aggregate TSS profile split at that cutoff (Check 5) rather than adopting a number from a different assay configuration.- Can I reuse my scRNA-seq QC thresholds for a 10x Multiome experiment's ATAC data?
No. Multiome's shared-nucleus capture inherently produces lower per-cell ATAC depth than ATAC-only assays, which is why Signac's Multiome vignette uses relaxed thresholds (
nCount_ATAC1,800, 100,000,TSS.enrichment > 1,nucleosome_signal < 2) instead of the PBMC ATAC-only defaults. Start from the Multiome-specific values and adjust from your own distributions.- How do I know if my QC filter deleted a real cell type instead of junk?
Overlay pass/fail status on a UMAP or LSI embedding: debris and dying cells scatter diffusely at the periphery, while a real deleted population shows up as one coherent cluster colored 'removed.' Cross-check any suspicious cluster against gene activity scores or a paired RNA modality before finalizing the cutoff.
Related pages
- Guide · How to Avoid Pseudoreplication in Single-Cell ATAC-seq
- Guide · How to Call Peaks You Can Trust in Single-Cell ATAC-seq
- Guide · How to Detect Batch Effects in Single-Cell ATAC-seq
- Guide · How to Find and Remove Doublets in Single-Cell ATAC-seq
- Guide · How to Sanity-Check Marker Genes and Cell Type Labels in Single-Cell ATAC-seq
- Glossary · FRiP (fraction of reads in peaks)
- Glossary · Peak calling
- Glossary · Pseudobulk
Sources
- Signac PBMC scATAC-seq Analysis — Source of the standard nCount_peaks/pct_reads_in_peaks/blacklist_ratio/nucleosome_signal/TSS.enrichment filter values and the caution that thresholds must be adjusted per dataset.
- Signac 10x Multiome (RNA + ATAC) Analysis — Relaxed Multiome ATAC thresholds and the definition of nucleosome signal as mono- to nucleosome-free fragment ratio.
- Best practices for filtering and QC with scATAC-seq data (GitHub Discussion) — Maintainer guidance to inspect QC metrics in low-dimensional space and to set thresholds from your own data's distribution rather than a universal cutoff.
- ArchR createArrowFiles Reference Documentation — minTSS/minFrags/maxFrags defaults and fragment-size filtering behavior used in Check 3 and Fix 3.
- NBIS Epigenomics Workshop: scATAC-seq Lab — Alternative published threshold set (TSS > 2, nucleosome signal < 4, fraction reads in peaks > 15%) used to illustrate that defaults vary across labs.
- VSN-Pipelines scATAC-seq QC Documentation — Example of per-sample-adjustable threshold philosophy referenced in Fix 1.
- PEAKQC: Periodicity Evaluation in scATAC-seq data for quality assessment — Threshold-free, periodicity-based QC approach referenced in Fix 6.
Part of the QC thresholds series.