Written and maintained by CASRAI Editorial Board
Last updated
The decision that determines what your Leiden clustering returns is almost never the resolution parameter. It is made two steps earlier, when you build the neighbour graph, and it is compounded by a fact most pipelines never surface: Leiden will hand you a clean, well-separated set of clusters when the input contains exactly one cell population. The algorithm has no null hypothesis. It partitions whatever graph you give it, and it does so deterministically enough to look convincing and stochastically enough that a different random seed changes the answer.
So the practical question is not “what resolution should I use?” It is “which of these clusters would survive if I ran this again, on a different split of the data, with a different seed?” This guide covers the mechanism first, because the tuning only makes sense once you know what each knob is actually doing to the objective function.
What the Leiden algorithm actually does
Leiden was introduced by Traag, Waltman and van Eck in 2019 as a direct repair of the Louvain algorithm. It operates on a graph — in single-cell work, a k-nearest-neighbour (kNN) graph of cells built in a reduced-dimension space — and iterates three phases:
- Local moving of nodes. Each node is moved to the neighbouring community that most improves the quality function, repeatedly, until no single move helps.
- Refinement of the partition. Each community from phase 1 is re-partitioned internally, with moves chosen randomly among those that improve quality rather than always greedily. This is the phase Louvain does not have.
- Aggregation. The refined partition is collapsed into a smaller network whose nodes are the refined communities, and the process repeats on that network.
The refinement step is the whole point. Louvain optimises the same quality function but aggregates on the unrefined partition, and the 2019 paper shows this permits communities that are arbitrarily badly connected — and in the worst case internally disconnected, meaning a “community” whose members are not reachable from one another within it at all. In the authors’ empirical networks, up to 25% of Louvain communities were badly connected and up to 16% were disconnected. A disconnected community in a cell graph is a cluster label spanning two groups of cells with no similarity edge between them — a cell type that is not one thing.
What Leiden guarantees, and what it does not
The guarantees strengthen as the algorithm iterates. After each iteration, all communities are γ-separated and γ-connected. After an iteration in which nothing changes, all nodes are locally optimally assigned and all communities are subpartition γ-dense. Asymptotically, all communities are uniformly γ-dense and subset optimal.
Note what is absent from that list: nothing guarantees the partition is the global optimum, nothing guarantees it is stable across random seeds, and nothing guarantees the communities correspond to anything biological. Leiden is a better optimiser of a chosen objective. It is not a test.
It is also substantially faster than Louvain, which is why it became the default. On the Web UK network the 2019 paper reports Leiden running more than 20 times faster; on a benchmark network with a difficult partition structure, Louvain required almost 2.5 days where Leiden finished in fewer than 10 minutes.
The knobs, in order of how much they change your answer
1. The neighbour graph — decided before Leiden runs
Leiden never sees your expression matrix. It sees a graph, and everything about that graph — which cells are connected, how strongly, in what representation — is fixed by the preceding step. In Scanpy that is sc.pp.neighbors, whose default is n_neighbors=15, knn=True, method='umap'. The documentation describes the trade-off plainly: larger values give “more global views of the manifold, while smaller values result in more local data being preserved”, with a suggested range of 2 to 100.
A larger k connects more distant cells, thickens the graph, and merges structure that a sparser graph would have kept apart. Changing k from 15 to 50 typically moves the cluster count more than any resolution change you would have considered making, and it does so for a reason you can articulate — you changed what “neighbour” means. The upstream choices feed into this too: which cells passed QC, how many principal components you kept, whether batch correction was applied to the representation the graph is built on. Treat the graph as part of the clustering, and record its parameters alongside the resolution. The same discipline that applies to the rest of an RNA-seq experimental design and analysis plan applies here.
2. The quality function — and the resolution limit hiding inside it
Leiden optimises whatever quality function you hand it. The leidenalg package exposes six partition types: ModularityVertexPartition, RBConfigurationVertexPartition, RBERVertexPartition, CPMVertexPartition, SignificanceVertexPartition and SurpriseVertexPartition. Only three — RBConfiguration, RBER and CPM — accept a resolution parameter at all.
This matters because modularity has a resolution limit. Fortunato and Barthélemy showed in 2007 that modularity optimisation can fail to resolve modules smaller than a scale set by the total size of the network: for a module to be resolved in the maximum-modularity partition, its internal edge count must satisfy roughly ls ≥ √(L/2), where L is the total number of links. The consequence for cell data is specific and under-appreciated: the size of a cluster that can be resolved depends on how big your dataset is. A rare population that separates cleanly in a 5,000-cell experiment can be absorbed into a neighbour when you sequence 200,000 cells, with no change to any parameter you set.
The Constant Potts Model (CPM) avoids this. Its quality function is ℋ = ∑c [ ec − γ C(nc, 2) ], and the 2019 paper describes γ as functioning “as a sort of threshold: communities should have a density of at least γ, while the density between communities should be lower than γ”. That is an absolute density criterion rather than one relative to the whole network, which is why the leidenalg documentation labels CPM resolution-limit-free. If you are comparing clusterings across datasets of very different sizes, CPM is the more defensible objective — and it is not what your pipeline is using by default.
3. Resolution
Resolution is the knob everyone turns, and it is third on this list for a reason: it is a coarseness dial applied to an objective you probably did not choose, on a graph you probably did not tune. Scanpy’s sc.tl.leiden defaults to resolution=1 and describes it as “a parameter value controlling the coarseness of the clustering. Higher values lead to more clusters.” Seurat’s FindClusters defaults to resolution = 0.8, advising a value above or below 1.0 for more or fewer communities respectively.
Two things practitioners consistently get wrong here. First, resolution is not a number of clusters and does not map monotonically onto one in any stable way — nudging it can leave the count unchanged for a while and then jump. Second, and more damaging, resolution has no correct value that the data can reveal by itself. Scanning it and picking the elbow of a cluster-count curve is not model selection; it is choosing the answer you find aesthetically convincing. The sections below cover what to do instead.
4. Iterations and backend
The leidenalg implementation of find_partition defaults to n_iterations=2; a negative value runs the algorithm until an iteration produces no improvement. Scanpy’s sc.tl.leiden instead defaults to n_iterations=-1, i.e. run to convergence, which is slower on large datasets.
Scanpy 1.10.0 (released 2024-03-26) added an alternative backend: sc.tl.leiden() “now offers igraph’s implementation of the leiden algorithm via flavor when set to igraph. leidenalg’s implementation is still default, but discouraged.” Running with the current default emits a FutureWarning stating that the default backend will become igraph and that to get the future defaults you should pass flavor='igraph' and n_iterations=2, with directed also set to False — igraph’s implementation raises a ValueError on a directed graph.
The backends are not interchangeable in their objective. With flavor='leidenalg' and no explicit partition_type, Scanpy uses RBConfigurationVertexPartition and passes your value as resolution_parameter; with flavor='igraph' it sets objective_function='modularity' and passes the value as resolution. A resolution of 1.0 does not mean the same thing under both backends, so a pipeline that switches flavor is not reproducing its previous clusters even with every visible argument identical. Pin the flavor explicitly rather than inheriting whatever the installed version defaults to.
5. The random seed
Leiden’s refinement phase makes randomised moves, so the seed is a real parameter. Scanpy sets random_state=0 and Seurat sets random.seed = 0, which makes runs reproducible — but reproducible is not the same as stable, and a fixed default seed mainly hides the instability rather than removing it.
The scICE authors (Nature Communications, 2025) give a concrete demonstration on the standard Seurat tutorial dataset: with a random seed of 88, Leiden yields five T-cell sub-clusters; with seed 863, only four emerge, with one of the originally identified clusters no longer detected. Across 48 datasets they found that only about 30% of cluster numbers between 1 and 20 were consistent — roughly seven in ten of the cluster counts a resolution scan could hand you are not reliably reproducible.
Leiden clusters homogeneous data
This is the failure mode that invalidates whole analyses, and it is easy to demonstrate. Grabski, Street and Irizarry (Nature Methods, 2023) simulated 5,000 cells from a single distribution — one true population, no structure to find — and ran Seurat’s default graph-based clustering. At the default resolution of 0.8 it returned five clusters. Across resolutions from 0.1 to 1.0, it returned between two and eight clusters. None of them existed.
Their demonstration used Seurat’s default Louvain rather than Leiden, but the mechanism is not specific to Louvain’s aggregation step: both algorithms optimise the same family of modularity-style objectives, and neither compares the resulting partition against a null model in which no sub-population exists. Leiden optimises that objective better. On null data, optimising harder is not an advantage.
Two families of method exist to put a statistical statement behind a split. sc-SHC embeds hypothesis testing inside a hierarchical clustering procedure, so a proposed split is only accepted if it is significant against a fitted null; it also ships a post-hoc mode that evaluates clusters produced by any algorithm, and an extension accounting for batch structure. scICE takes the stability route: it evaluates label consistency across many random seeds using an inconsistency coefficient (IC), treating a clustering as consistent when the median IC falls below 1.005 — about 0.25% membership inconsistency, or 2 to 3 cells in 1,000. It reports up to a 30-fold speed improvement over consensus-clustering approaches such as multiK and chooseR, which makes running it a realistic default rather than a special-occasion audit.
The double-dipping trap in downstream marker testing
Having found clusters, the standard next move is to test genes for differential expression between them. Done naively, this is invalid, and the invalidity is severe rather than marginal.
The reason is stated cleanly in the sc-SHC paper: if you force a single population into two clusters, the algorithm assigns cells that are more similar to each other into the same group, “but the statistical test does not take this selection into account when considering the null hypothesis.” The clustering has already maximised the between-group difference on exactly the genes you are about to test. Neufeld and colleagues (Biostatistics, 2024) show the same effect in miniature: clustering 50 homogeneous cells with k-means at k=2 and then testing five genes yields all five p-values small, with the QQ plot departing sharply from uniform, purely because “the Wald test does not account for the fact that the clustering algorithm is designed to maximize the difference between the clusters.”
No amount of multiple-testing correction fixes this, because the p-values being corrected are already wrong — the machinery of FDR control described in our guide to q-values and the positive false discovery rate assumes p-values that are uniform under the null, and here they are not. The available repair is to stop reusing the same counts twice. Count splitting draws Binomial(Yij, π) variables to split the count matrix into two independent matrices under a Poisson assumption, clusters on the first, and tests on the second; the authors recommend π = 0.5 to balance estimation against inference, and a negative-binomial extension exists for overdispersed data. In a null application — 1,087 homogeneous Day 0 cardiomyocyte cells — count splitting maintained Type I error control while the double-dipped analysis produced false positives.
Defaults worth knowing before you compare two analyses
- Scanpy
sc.pp.neighbors:n_neighbors=15,knn=True,method='umap',random_state=0. - Scanpy
sc.tl.leiden:resolution=1,flavor=None,n_iterations=-1,use_weights=True,random_state=0,key_added='leiden'; partition typeRBConfigurationVertexPartitionwhen unspecified. - Seurat
FindClusters:resolution = 0.8,algorithm = 1— which is Louvain, not Leiden. Leiden isalgorithm = 4and requires theleidenalgPython package to be installed separately. Alson.start = 10,n.iter = 10,random.seed = 0,group.singletons = TRUE. - leidenalg
find_partition:n_iterations=2; negative values run until no improvement.
The Seurat default is the one that trips people up in methods sections. A paper that says “cells were clustered using Seurat’s FindClusters” without naming algorithm = 4 was almost certainly running Louvain, and inherits Louvain’s connectivity problem rather than Leiden’s guarantees. If you are reviewing or reproducing such an analysis, ask.
A tuning procedure that survives review
- Fix and report the graph. Representation, number of components,
n_neighbors, whether the graph is weighted and directed. Varyn_neighborsdeliberately and see how much of your structure is a property of the graph rather than the partition. - Choose the objective on purpose. Use CPM if cluster granularity must be comparable across datasets of different sizes; use RBConfiguration/modularity if you are staying within one dataset and understand that resolvable cluster size scales with dataset size.
- Pin the backend. Set
flavorandn_iterationsexplicitly. Do not let a Scanpy upgrade silently change your objective function. - Sweep resolution, but treat the sweep as a candidate list, not an answer. Every value in the sweep is a hypothesis about how many populations exist.
- Score candidates by stability across seeds, not by eye. Re-run each candidate over many random seeds and keep only cluster numbers whose labels are consistent — scICE’s IC with its 1.005 threshold is one concrete criterion; consensus approaches such as chooseR and multiK are older and slower alternatives.
- Test the splits you intend to report. Use sc-SHC, or its post-hoc mode on labels you already have, before describing a cluster as a distinct population.
- Split the counts before marker testing. Cluster on one split, test on the other, or state explicitly that reported marker p-values are descriptive and not valid inference.
- Validate outside the assay. A cluster that reproduces in an independent sample, or corresponds to a known surface marker or an orthogonal modality, is evidence in a way that an internal metric never is.
The same logic transfers to graph-based clustering of other single-cell modalities — cells profiled by ATAC-seq are clustered by the same Leiden call on a graph built from a different feature space, and inherit every caveat above. The broader analytical stack these choices sit in is covered in our overview of what bioinformatics is and where it is applied.
Frequently asked questions
What resolution should I use for Leiden clustering?
There is no value that is correct independent of your data and your question. Scanpy defaults to 1.0 and Seurat to 0.8, and both are starting points rather than recommendations. Because clustering 5,000 cells from a single simulated population produced five clusters at Seurat’s default resolution and between two and eight across the 0.1–1.0 range, a resolution chosen by eye carries no evidence about how many populations exist. Sweep it, then select among the candidates by seed-stability and a significance test rather than by picking the value whose UMAP looks best.
Is Leiden better than Louvain?
For the same objective function, yes, on grounds that are proven rather than empirical. Louvain can return communities that are arbitrarily badly connected or internally disconnected — in the 2019 paper’s experiments, up to 25% badly connected and up to 16% disconnected — while Leiden’s refinement phase guarantees connected communities and progressively stronger optimality properties as it iterates. It is also much faster on large graphs. What it does not do is make the clusters more real: both optimise the same quality function and neither tests whether the structure exists.
Why do I get different clusters every time I run Leiden?
Because the refinement phase makes randomised moves, so the partition depends on the random seed. Scanpy and Seurat both fix a default seed, which makes a single pipeline reproducible while concealing how unstable the result is. On the standard Seurat tutorial dataset, seed 88 gives five T-cell sub-clusters and seed 863 gives four. Across 48 datasets, only around 30% of cluster numbers between 1 and 20 were found to be consistent across seeds.
Should I use CPM or modularity as the quality function?
Use CPM when cluster granularity has to mean the same thing across datasets of different sizes. Modularity carries a resolution limit — modules smaller than a scale set by the total number of links in the network cannot be resolved — so the smallest population you can detect shrinks and grows with your cell count. CPM’s resolution parameter is instead an absolute density threshold: communities should be at least γ-dense internally and less than γ-dense between each other. In leidenalg, only RBConfigurationVertexPartition, RBERVertexPartition and CPMVertexPartition accept a resolution parameter at all.
Can I test for marker genes between the clusters Leiden found?
Not with standard tests on the same counts used to build the clusters. The clustering has already selected for the between-group differences you are testing, so p-values are anti-conservative — in a five-gene demonstration on 50 homogeneous cells, all five p-values came out small under the null. FDR correction does not repair this, because it assumes valid p-values as input. Count splitting (cluster on one binomially thinned split at π = 0.5, test on the other) restores Type I error control under a Poisson assumption, and sc-SHC provides a post-hoc significance assessment for clusters produced by any algorithm.
Does Seurat’s FindClusters run Leiden by default?
No. FindClusters defaults to algorithm = 1, the original Louvain algorithm. Leiden is algorithm = 4 and additionally requires the leidenalg Python package to be installed. A methods section that names FindClusters without specifying the algorithm is describing Louvain.
Why did my cluster count change when I upgraded Scanpy?
Most likely the backend. Since version 1.10.0, sc.tl.leiden can run igraph’s implementation via flavor='igraph', and the FutureWarning states the default will change from leidenalg to igraph. The two do not treat resolution identically: with leidenalg and no explicit partition type Scanpy uses RBConfigurationVertexPartition and passes resolution_parameter, whereas the igraph path sets objective_function='modularity' and passes resolution. Default iterations differ too — Scanpy uses n_iterations=-1 (run to convergence), while leidenalg’s own find_partition default is 2. Pin flavor, n_iterations and resolution explicitly.
How many neighbours should the kNN graph have?
Scanpy’s default is 15, with the documentation suggesting a range of 2 to 100 and noting that larger values give a more global view of the manifold while smaller values preserve local structure. There is no data-independent optimum; what matters is that you vary it deliberately and report it, because it changes the cluster structure at least as much as resolution does, and it does so before Leiden is ever called.
References
- Traag VA, Waltman L, van Eck NJ. From Louvain to Leiden: guaranteeing well-connected communities. Scientific Reports 9, 5233 (2019).
- Fortunato S, Barthélemy M. Resolution limit in community detection. PNAS 104(1):36–41 (2007).
- Grabski IN, Street K, Irizarry RA. Significance analysis for clustering with single-cell RNA-sequencing data. Nature Methods 20:1196–1202 (2023).
- Neufeld A, Gao LL, Popp J, Battle A, Witten D. Inference after latent variable estimation for single-cell RNA sequencing data. Biostatistics 25(1):270–287 (2024).
- Kim H et al. scICE: enhancing clustering reliability and efficiency of scRNA-seq data with multi-cluster label consistency evaluation. Nature Communications (2025).
- scanpy.tl.leiden and scanpy.pp.neighbors API documentation.
- Scanpy 1.10.0 release notes (igraph flavor for
tl.leiden). - leidenalg reference documentation (partition types, resolution parameters,
find_partition). - Seurat
FindClustersreference.








