Comparison · differential expression
edgeR vs limma-voom: Which One Should You Use?
Same lab, same Bioconductor ecosystem, but a negative-binomial GLM and a voom-transformed linear model bite in different places once your design gets complicated or your cohort gets big.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 4 min read
The verdict
Default to edgeR's quasi-likelihood pipeline (glmQLFit/glmQLFTest) for the standard bulk RNA-seq case most readers here actually run: 3-vs-3 to 6-vs-6 replicates, a single or simple factorial fixed-effect design, no repeated measures. It's the pipeline edgeR's own authors made the default as of v4, it stays close to unbiased at small n and low counts, and TMM normalization feeds directly into the same NB offset the test uses. You don't need anything more complex until your design or your sample size tells you otherwise.
Switch to limma-voom the moment either of two things is true: your design has a blocking or repeated-measures structure, paired samples, the same subject across timepoints, that needs duplicateCorrelation(), or your cohort is large enough (rough rule of thumb: north of 20-30 samples per group, definitely once you're in the hundreds) that speed and the mature limma ecosystem (arrayWeights, gene set testing, flexible contrasts) start to matter more than NB precision at the low-count end. At that scale the two converge in sensitivity anyway, so switching costs you nothing statistically.
edgeR models counts directly. It fits a negative binomial distribution to each gene, uses TMM normalization to set library-size offsets, and tests with a quasi-likelihood F-test (glmQLFit/glmQLFTest) that moderates gene-wise dispersion the way limma moderates variance. Nothing about the count data gets transformed away; the discreteness and the mean-variance relationship of counts are the thing being modeled.
limma-voom takes the opposite route. voom() converts counts to log-CPM, estimates a mean-variance trend across genes, and assigns each observation a precision weight that captures how noisy it is. That weighted, continuous matrix then goes straight into limma's linear-model machinery, lmFit, contrasts.fit, eBayes, the same engine originally built for microarrays. The count-specific problem gets solved once, up front, so everything downstream is ordinary linear regression.
Both came out of the same authors' lab, and at large sample sizes they converge: the negative binomial assumption matters less once you have enough replicates, and limma-voom's linear framework catches up to edgeR's sensitivity while running faster. The real choice isn't about which is "more accurate", it's about design complexity and how sparse your counts are.
Head to head
| Criterion | edgeR | limma-voom | Edge |
|---|---|---|---|
| Statistical foundation | Models counts directly with a negative binomial distribution; dispersion is estimated per gene and tested with a quasi-likelihood F-test (glmQLFit/glmQLFTest). | Transforms counts to log-CPM, fits a mean-variance trend, and assigns each observation a precision weight so ordinary linear-model math (lmFit/eBayes) applies. | Tie Both are empirical-Bayes methods from the same lab; they land on the same genes for most designs. |
| Small-n / sparse-count handling | Built for the 2-vs-2 to 6-vs-6 world: bias-adjusted deviances in the QL fit stay close to unbiased even with small counts and few replicates. | The voom weight model needs enough samples per group to fit a stable mean-variance trend; with very small n and sparse counts that trend line gets noisy. | edgeR |
| Complex designs: blocking, repeated measures, nested subjects | The GLM framework has no native random-effects term, so you're limited to fixed-effect design matrices. | duplicateCorrelation() lets you block on subject or donor and estimate a consensus correlation for repeated-measures or paired samples before fitting. | limma-voom |
| Behavior at large sample size (n > 20-30/group, hundreds of samples) | Still correct at scale, but iteratively fitting a full NB GLM per gene is slower as sample count grows. | Runs faster on large cohorts, and sensitivity converges with edgeR's QL results once n is large enough that the NB assumption stops mattering. | limma-voom |
| Normalization | TMM normalization (calcNormFactors) is the native default and feeds directly into the NB offset. | voom() typically consumes the same TMM-normalized library sizes from an edgeR DGEList before transforming to log-CPM, the normalization step is shared, not tool-specific. | Tie |
| Low-expression filtering | filterByExpr() removes lowly-expressed genes before fitting, tuned to group sizes. | The published limma-voom workflow calls the same filterByExpr() on the DGEList before voom(), identical logic, not a differentiator. | Tie |
| A default that bites | Forgetting robust=TRUE on glmQLFit lets a handful of outlier genes distort the empirical Bayes dispersion trend for everyone else. | Skipping the plot=TRUE check on voom() means you never see whether the mean-variance trend collapsed from over-permissive filtering or uneven depth. | Tie |
| Release cadence and current defaults | v4 (October 2023) added fractional-count support and made the QL pipeline the default recommended route; current release 4.10.5. | limma 3.68.5 is the more mature, slower-moving core; recent RNA-seq-specific gains came from edgeR v4, not from limma itself. | edgeR |
| Ecosystem and interoperability | Downstream tooling is edgeR- or Glimma-specific for interactive exploration of NB-based results. | A voom object is a standard limma EList, so every limma downstream feature, arrayWeights, duplicateCorrelation, treat(), gene set tests like camera, works without translation. | limma-voom |
Use edgeR when
- You have small replicate numbers, 2-4 per group, with sparse, low-count genes and no blocking structure.
- Your design is a simple fixed-effect comparison, single factor or basic factorial, with no repeated measures.
- You want the current Bioconductor-recommended default pipeline (edgeR v4's quasi-likelihood route).
- Sequencing depth varies across samples and you want that variance modeled directly through the NB offset rather than a separate weight.
- You need glmTreat()-style fold-change thresholding built into the same NB framework you're already using for the main test.
Use limma-voom when
- Your design has repeated measures or paired samples, same subject across timepoints or treatments, that need duplicateCorrelation().
- Your cohort is large: north of 20-30 samples per group, or into the hundreds, where speed and FDR behavior at scale matter more than small-n precision.
- You need limma-specific downstream tools: gene set testing (camera, roast), arrayWeights for down-weighting outlier samples, or flexible contrast structures.
- You have outlier samples that need explicit down-weighting rather than being folded into a single per-gene dispersion estimate.
- Your model includes continuous covariates or interaction terms you'd rather express through ordinary linear-model syntax than a count-specific design matrix.
Switching between them
Switching pipelines means switching object types partway through, not just swapping function names. Both start from the same DGEList after calcNormFactors(TMM) and filterByExpr(), keep that shared setup identical so you're not accidentally changing what "normalized" means between runs. From there edgeR feeds the DGEList straight into glmQLFit(), while limma-voom converts it to an EList via voom() and hands that to lmFit(); don't run calcNormFactors twice or you'll double-correct for depth, and don't skip it before voom() or you'll lose the depth correction entirely. Contrasts are built with the same makeContrasts() syntax in both, but the test call differs: glmQLFTest(fit, contrast=...) for edgeR versus contrasts.fit() + eBayes() + topTable() for limma-voom. The output statistics are not numerically interchangeable, edgeR's QL F-statistic and p-value are not the same quantity as limma's moderated t/F-statistic, so don't reuse a p-value or fold-change threshold you validated in one tool as if it means the same thing in the other. If you're moving a repeated-measures analysis from edgeR to limma-voom, budget time to add duplicateCorrelation() and rerun voom() with the resulting correlation estimate, it's not a drop-in swap.
Pitfalls with either
- Running voom() without plot=TRUE hides whether the mean-variance trend collapsed from over-permissive filtering or badly uneven depth; always inspect the plot and refilter if the curve hooks upward at the low-count end.
- Treating edgeR's glmLRT() output as equivalent to glmQLFTest() understates error at small n because the likelihood ratio test skips the quasi-likelihood variance inflation; default to the QL pipeline per the v4 guidance.
- Running calcNormFactors() twice, or skipping it entirely before voom(), either double-corrects for library depth or leaves it uncorrected; normalize the DGEList once and pass that same object into both pipelines.
- Assuming limma-voom's speed advantage makes it the safer default everywhere loses the small-n precision edgeR was built for; reserve limma-voom for larger cohorts and let edgeR's QL model carry the small-sample cases.
- Analyzing a repeated-measures or paired design in edgeR without any blocking term ignores within-subject correlation and inflates false positives; route paired designs through limma-voom's duplicateCorrelation() instead.
Questions people ask
- Do edgeR and limma-voom give different results on the same data?
For most genes, no. On real datasets, edgeR's quasi-likelihood pipeline and limma-voom perform almost identically. Differences concentrate at the low end: sparse counts and small replicate numbers can push specific genes across the significance threshold in one tool but not the other.
- Which is faster, edgeR or limma-voom?
limma-voom is faster on large cohorts, hundreds of samples, because it works on a transformed continuous matrix instead of iteratively fitting a negative binomial GLM per gene. For a typical 3-vs-3 or 4-vs-4 experiment the speed difference is irrelevant.
- Can edgeR handle repeated-measures or paired designs?
Not natively. edgeR's GLM framework has no random-effects term, so paired or repeated-measures designs need limma-voom's
duplicateCorrelation()to model within-subject correlation. Forcing edgeR to handle this with only a fixed blocking factor is a weaker approximation.- Should I switch tools mid-project to double-check my results?
Only if your design or sample size actually changed, for example you added a repeated-measures arm or your cohort grew past 20-30 samples per group. Re-running the same design through both tools just to compare rarely changes the biological conclusion and mostly burns time.
- Does the tool choice change normalization?
Not really. Both pipelines typically start from the same TMM-normalized DGEList produced by
calcNormFactors(), so normalization is shared infrastructure, not a point of differentiation between edgeR and limma-voom.
Related pages
- Compare · DESeq2 vs edgeR: Which One Should You Use?
- Compare · DESeq2 vs limma-voom: Which One Should You Use?
- Guide · How to Detect Batch Effects in Bulk RNA-seq
- Guide · How to Handle Multiple Testing and FDR in Bulk RNA-seq
- Guide · How to Log-Transform Counts Without Fooling Yourself in Bulk RNA-seq
- Glossary · P-value
- Glossary · Negative binomial model
Related reading on the blog
Sources
- When to use edgeR or limma — Design/sample-size guidance for choosing between the two pipelines
- RNA-seq analysis is easy as 1-2-3 with limma, Glimma and edgeR — Shared filterByExpr/TMM workflow and code for both pipelines
- edgeR v4: powerful differential analysis of sequencing data with expanded functionality and improved support for small counts and larger datasets — v4 quasi-likelihood default and fractional-count support
- Performance of different flavors of limma and edgeR as the sample size goes up — Convergence and voom weight behavior at large n
- glmQLFTest function - RDocumentation — glmQLFit/glmQLFTest parameters, robust and abundance.trend options