Sanity check · Single-Cell RNA-seq
How to Choose Cell QC Thresholds in Single-Cell RNA-seq
The percent.mt < 5 you copied from a PBMC vignette is quietly deleting the tumor cells, cardiomyocytes, or plasma cells you were funded to study.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 5 min read
You've got Cell Ranger output for a handful of tumor, kidney, or heart samples. You open the Seurat PBMC tutorial, copy percent.mt < 5 and nFeature_RNA between 200 and 2500 straight into subset(), and run the pipeline. Clustering finishes clean, UMAP looks fine, and you move on. Then someone asks where the tumor cluster went, or why there are no cardiomyocytes in a heart sample, or why the plasma cell population that flow cytometry confirmed is missing from your single-cell data.
The stakes are not cosmetic. Mitochondrial content is a proxy for cell stress, but it is also a direct readout of oxidative metabolism, and cells that run oxidative phosphorylation hard for a living, malignant cells, cardiomyocytes, hepatocytes, kidney epithelium, are naturally high in mitochondrial transcripts while being completely viable. A fixed threshold from a PBMC tutorial does not know that. In one published ovarian cancer dataset, a standard 10% mito cutoff removed 88% of cells; an adaptive model removed 29.7%. That difference is the malignant population you came to characterize.
This page gives you a way to set thresholds that are specific to your sample instead of borrowed from someone else's tissue, and a set of checks to run before you trust any cutoff, so that in the next hour you can look at what your filter actually threw away and defend the number in a review.
What it looks like when it's happening
- The percent.mt violin plot per sample shows one continuous, right-skewed distribution, not two clean clusters, a hard 5% line slices straight through the middle of a real, living population.
- After filtering, a cluster you expected biologically (tumor cells, cardiomyocytes, hepatocytes, kidney epithelium) is missing entirely or is a fraction of the size dissociation should have yielded.
- nFeature_RNA has a long right tail past 2500 that gets truncated the moment you copy the PBMC tutorial's upper bound, even though your tissue legitimately has higher-complexity cells.
- Plotting QC metrics with group.by = "orig.ident" shows one sample's median percent.mt is roughly double another sample's, yet you're about to apply one fixed cutoff to both.
- Plasma cell or neutrophil clusters vanish after filtering because those cell types are naturally low in RNA content and total counts, not because they're dying.
- Filtering removes 60-90% of a sample's cells in one step, matching the pattern reported when a fixed 10% mito threshold was applied to a tumor sample instead of an adaptive one.
Why it happens
Mitochondrial reads climb in dying cells because the plasma membrane ruptures during dissociation and cytoplasmic mRNA leaks out of the droplet, while transcripts still sitting inside the double-membraned mitochondria survive and get captured. That's the biological logic behind using percent.mt as a damage proxy at all. But mitochondrial transcript abundance is also a baseline property of a cell's metabolic rate, completely independent of whether the membrane is intact. Kidney tissue runs a mean mitochondrial fraction around 31% with a standard deviation of 16%, nowhere close to the 5% that works for PBMCs. Cardiomyocytes and hepatocytes are similarly mitochondria-dense because their normal function demands heavy oxidative phosphorylation. Malignant cells frequently show elevated percent.mt tied to metabolic dysregulation itself, a biological signal with documented associations to drug response and clinical features, not an artifact of dissociation stress.
The same logic runs in reverse for count-based floors. Plasma cells and neutrophils are naturally low in total RNA and detected genes. A fixed nFeature_RNA floor built to exclude empty droplets and debris in PBMCs will exclude these cell types too, because from the matrix's point of view a metabolically quiet, low-RNA-content live cell looks identical to a low-quality one.
The technical cause of the bad default is traceable: percent.mt < 5 and nFeature_RNA between 200 and 2500 come from one specific worked example, the Seurat v3 PBMC tutorial, run on one specific low-mito, high-cell-diversity tissue. Those numbers are correct for that dataset by construction. They are wrong everywhere else not because anyone made an error copying them, but because nobody went back and asked whether PBMC assumptions hold for their tissue.
Batch and sample variability compound this. 10x Genomics' own QC guidance is explicit that thresholds should be set per sample before merging, because capture efficiency, dissociation protocol, and tissue prep shift the whole distribution between batches, sometimes even between replicates of the same tissue processed weeks apart. Pooling samples first and applying one global cutoff bakes that batch effect into your filtering decision.
The checks
Run them in order. Each one tells you what healthy looks like and what the problem looks like.
0/8 checked · saved in this browser
Before you touch subset(), plot nFeature_RNA, nCount_RNA, and percent.mt as violins split by sample so you see each distribution's actual shape, not a pooled average.
rVlnPlot(pbmc, features = c("nFeature_RNA", "nCount_RNA", "percent.mt"), group.by = "orig.ident", ncol = 3)- Healthy
- Each sample has its own roughly unimodal (or clearly bimodal, low-quality-tail-plus-real-population) distribution, and you can see where the tail of debris/empty droplets separates from the main cell population for that specific sample.
- Red flag
- Distributions differ sharply between samples (one median percent.mt double another's), or the distribution is continuous with no visible separation point, meaning a single fixed number can't be the right cutoff for all of them.
Calculate mitochondrial percentage with the standard gene-symbol pattern, then look at the raw range (min, max, quantiles) before deciding on any cutoff.
rpbmc[["percent.mt"]] <- PercentageFeatureSet(pbmc, pattern = "^MT-") quantile(pbmc$percent.mt, probs = seq(0, 1, 0.1))- Healthy
- A distribution whose upper quantiles are plausible for the tissue you're working with, low double digits for PBMC-like tissue, potentially 25-80% for kidney, elevated for tumor, cardiomyocyte, or hepatocyte-rich samples.
- Red flag
- A large fraction of cells sit right at or just above your intended cutoff (e.g., a wall of cells clustered near 5% when your median is already close to that), meaning the cutoff is arbitrary relative to the data's own structure.
Tabulate median and IQR of percent.mt and nFeature_RNA per sample (or per tissue if multi-tissue) side by side, before deciding whether one global cutoff is even appropriate.
- Healthy
- Samples from the same tissue and protocol cluster together; any sample far outside that range flags a technical (dissociation, capture) difference worth investigating, not just a QC number to force into line.
- Red flag
- A single cutoff you're about to apply falls comfortably within the normal range for one sample and near the extreme tail for another, meaning it will filter unevenly and bias downstream cell-type proportions between samples.
Instead of a hardcoded number, compute an outlier boundary from each sample's own median and median absolute deviation, which is robust to skew and doesn't assume a PBMC-shaped distribution.
rmt <- pbmc$percent.mt[pbmc$orig.ident == "sample1"] threshold <- median(mt) + 3 * mad(mt) sum(mt > threshold)- Healthy
- A threshold that moves with each sample's own center and spread, higher for a naturally high-mito tissue, lower for a naturally low-mito one, and a manageable, explainable number of cells flagged as outliers.
- Red flag
- The MAD-derived threshold ends up close to a suspiciously round number like 5% or 10% for every sample regardless of tissue, which usually means the underlying distribution is too skewed or multimodal for a simple median+MAD rule and needs a mixture-model approach instead.
Fit miQC's joint mixture model of percent.mt versus gene count (or ddqc's MAD-based filtering across gene complexity, UMI complexity, mito%, and ribo%) per sample, and let the model assign a posterior probability of being a compromised cell rather than thresholding one metric in isolation.
- Healthy
- A per-sample, per-cell probability of being intact versus compromised that tracks the joint relationship between library complexity and mito%, so a high-mito cell with otherwise healthy gene counts isn't automatically discarded.
- Red flag
- The model removes a similarly extreme fraction of cells as your fixed cutoff did (e.g., still near 90% in a tumor sample), that's a sign the sample itself may have real quality problems, or the model didn't converge and needs more cells or a sanity check on its component fit.
Log cell counts before and after each QC step, grouped by sample, and look for any sample or expected cell type disproportionately depleted relative to the others.
- Healthy
- Removal rates in the same single-digit-to-low-teens percentage range across samples from a comparable tissue and protocol.
- Red flag
- One sample loses a much larger fraction of cells than its siblings, or removal rate correlates suspiciously well with a known biological covariate (tumor sample vs normal, heart vs blood) instead of technical quality.
Before finalizing thresholds, cluster or at least run marker detection on the cells about to be excluded, and check whether they express expected cell-type markers (tumor markers, cardiac genes like TNNT2, hepatocyte markers like ALB) rather than stress/apoptosis markers alone.
- Healthy
- Excluded cells look like genuine debris or lysed cells: low complexity, no coherent marker signature, scattered rather than clustering.
- Red flag
- Excluded cells express clear, coherent cell-type markers and would form their own cluster if you kept them, that's real biology being thrown away, not noise.
Mito% and count floors address dying cells and empty droplets, but not background contamination or multiplets. Run scDblFinder or AMULET for doublets, and CellBender, DecontX, or SoupX for ambient RNA correction on the raw (unfiltered) matrix, independently of your mito/count QC decision.
- Healthy
- Doublet rate consistent with your loading concentration and expected multiplet rate for the 10x kit used; ambient RNA correction improves marker gene specificity without changing overall cell count dramatically.
- Red flag
- You're tuning percent.mt or nFeature_RNA thresholds to try to indirectly remove doublets or ambient contamination, these are different problems with dedicated tools, and Cell Ranger does not filter either for you.
What to do about it
Switch from a fixed cutoff to per-sample MAD-based thresholds
When: You're applying one vignette-derived number (like percent.mt < 5) across multiple samples or a non-PBMC tissue, and violin plots show meaningfully different distributions per sample.
Compute median(percent.mt) + k*mad(percent.mt) (commonly k around 3, but sanity-check against your own distribution) separately for each sample, and filter each sample against its own threshold before merging.
Caveat: A MAD rule still assumes a roughly unimodal distribution within a sample. For a genuinely bimodal sample (a real high-mito tumor population plus real dying cells), a single MAD cutoff can still cut through the wrong place, so pair it with the removed-cells marker check.
Use miQC for a joint, adaptive model instead of thresholding mito% alone
When: You want a principled cutoff rather than an ad hoc multiplier, and especially when high-mito cells with otherwise good gene counts might be real (tumor, cardiomyocyte, hepatocyte samples).
Fit miQC's two-component mixture model relating percent.mt to gene count per sample, and keep cells assigned to the intact component above your chosen posterior probability threshold rather than a hard percent.mt line.
Caveat: miQC needs enough cells per sample for the mixture model to converge reliably, and it's an R/Bioconductor tool, so it fits naturally into a Seurat workflow but requires a bridge if your pipeline is Scanpy-based.
Use ddqc for multi-metric, per-cluster adaptive filtering
When: You need thresholds that vary not just by sample but by cell type or cluster, and a single mito/count relationship (as miQC models) isn't capturing enough of the picture.
Run an initial coarse clustering, then apply MAD-based adaptive thresholds across gene complexity, UMI complexity, and mitochondrial and ribosomal fractions within each cluster, so a naturally low-count cluster (plasma cells, neutrophils) gets its own floor instead of the global one.
Caveat: Requires a clustering pass before final QC, which introduces some circularity if the initial clustering is itself distorted by unfiltered debris; treat the first clustering as provisional and re-cluster after filtering.
Run ambient RNA correction before finalizing any mito/count-based decision
When: You suspect background noise is inflating apparent mitochondrial or low-complexity signal across many cells rather than a subset being genuinely compromised.
Run CellBender (most precise background estimates, largest marker gene detection improvement per the available comparison) or SoupX/DecontX on the raw, unfiltered barcode matrix, then recompute QC metrics on the corrected counts.
Caveat: CellBender needs the raw unfiltered matrix, not just Cell Ranger's filtered barcodes, and is computationally heavier (GPU helps); factor that into your pipeline before treating it as a quick fix.
Always filter per sample before merging, never on the pooled object
When: You're working with more than one sample or batch, which is the normal case.
Run QC metric calculation and filtering inside a per-sample loop (or per-sample Seurat/AnnData object) using that sample's own distribution and threshold, then merge the already-filtered objects.
Caveat: This multiplies the manual review time across samples; script the plotting and threshold computation so it's reproducible per sample rather than eyeballing each one from scratch.
When not to "fix" it
Don't tighten or "fix" your mito threshold just because it looks high relative to a PBMC vignette. Kidney tissue has a mean mitochondrial fraction around 31% with a standard deviation of 16%, so a kidney sample sitting at 40-50% mito in a chunk of otherwise healthy-looking cells is normal biology, not a QC failure. The same applies to cardiomyocytes and hepatocytes, whose baseline oxidative metabolism keeps mitochondrial transcript fraction elevated in viable cells. In cancer, elevated percent.mt in malignant cells has been shown to reflect real metabolic dysregulation with documented links to drug response and clinical features, largely independent of dissociation stress, so aggressively filtering high-mito tumor cells can strip out the exact subpopulation a study is designed to characterize. On the low-count side, plasma cells and neutrophils are legitimately low in total RNA content; forcing a PBMC-derived nFeature_RNA floor onto them removes real cell types, not debris. If your removed-cells marker check (see the checks above) shows a coherent, biologically sensible expression signature rather than stress or apoptosis genes, the "problem" is the tissue, not the data.
Five things experienced analysts do here
- Never carry percent.mt < 5 or nFeature_RNA 200-2500 into a new project without checking the tutorial's tissue first, those numbers are specific to one PBMC dataset, not a scRNA-seq universal.
- Plot every QC metric split by sample before you filter anything, since 10x's own guidance is that thresholds shift meaningfully between batches even within the same tissue and protocol.
- Always look at what a filter removed, not just what it kept, running marker detection on the excluded cells takes a few minutes and catches real cell types before they're gone for good.
- Treat mitochondrial percentage as continuous evidence about a cell's state, not a binary pass/fail switch; a mixture model like miQC uses that continuity, a hard cutoff throws it away.
- Keep ambient RNA correction and doublet detection as separate steps from mito/count QC, they solve different problems, and folding one into the other by fudging a threshold hides which issue you actually have.
Questions people ask
- What mitochondrial percentage cutoff should I use for scRNA-seq QC?
There isn't one universal number. PBMC-like tissue tolerates roughly 5%, but kidney runs 25-80%, and tumor, cardiomyocyte, and hepatocyte samples naturally run higher because of their metabolic activity. Compute a per-sample MAD-based threshold or fit miQC instead of copying a vignette value.
- What's a reasonable nFeature_RNA cutoff for filtering low-quality cells?
Look at your own sample's distribution rather than reusing 200-2500 from the PBMC tutorial. Low-RNA-content cell types like plasma cells and neutrophils will fall below a PBMC-derived floor while being completely viable, so set the floor from where your own debris/empty-droplet tail separates from the real cell population.
- How does MAD-based outlier detection work for single-cell QC?
You compute the median and median absolute deviation (MAD) of a QC metric like percent.mt within a sample, then flag cells beyond median + k*MAD (commonly around 3) as outliers. It's robust to the skewed, non-normal shape typical of these distributions, and it adapts automatically to each sample's own center and spread instead of applying one fixed number everywhere.
- Should doublet and ambient RNA filtering happen at the same step as mito/count QC?
No. Cell Ranger does not detect doublets or remove ambient RNA on its own. Use scDblFinder or AMULET for doublets and CellBender, SoupX, or DecontX for ambient RNA correction as separate steps, ideally on the raw unfiltered matrix, rather than trying to tune a mito or count threshold to indirectly catch them.
- What is miQC and when should I use it instead of a fixed cutoff?
miQC is an adaptive probabilistic framework that jointly models mitochondrial percentage and gene count using a two-component mixture of linear regressions to separate compromised cells from intact ones. Use it when a fixed mito threshold is removing cells you suspect are real, high-metabolism biology, like tumor or cardiac samples, since it adjusts to your dataset's own pattern instead of applying a borrowed number.
Related pages
- Guide · How to Choose Cell QC Thresholds in Spatial Transcriptomics
- Guide · How to Find and Remove Doublets in Single-Cell RNA-seq
- Guide · How to Log-Transform Counts Without Fooling Yourself in Single-Cell RNA-seq
- Guide · How to Sanity-Check Marker Genes and Cell Type Labels in Single-Cell RNA-seq
- Guide · How to Tell If You Overclustered in Single-Cell RNA-seq
- Glossary · Spatial transcriptomics
Sources
- miQC: An adaptive probabilistic framework for quality control of single-cell RNA-sequencing data — Adaptive joint model of mito% and gene count; ovarian cancer 88% vs 29.7% filtering comparison
- Seurat Guided Clustering Tutorial (v3.0) — Source of the percent.mt < 5 and nFeature_RNA 200-2500 PBMC example thresholds and VlnPlot/subset commands
- Biology-inspired data-driven quality control for scientific discovery in single-cell transcriptomics — ddqc MAD-based adaptive filtering across four metrics
- Preprocessing and clustering - Scanpy documentation — Permissive initial filtering and per-batch QC recommendation
- Common Considerations for Quality Control Filters for Single Cell RNA-seq Data — Per-sample QC filtering recommendation before integration
- Best Practices for Analysis of 10x Genomics Single Cell RNA-seq Data — Cell Ranger does not filter doublets or ambient RNA; third-party tools required
- Filtering cells with high mitochondrial content depletes viable metabolically altered malignant cell populations in cancer single-cell studies — Fixed mito thresholds remove viable tumor cells with clinical associations
Part of the QC thresholds series.