skip to content

In GitHub Issues, when is a milestone the right tool instead of a label?

level: middleimportance: should knowfreq 38%

answer

  1. Count how many an issue can hold
  2. One has a due date and a bar
  3. Classification versus completion
  4. Both stop at the repository edge

basics

~20 s

Use a GitHub milestone when the question is "is this batch of work done yet" — it holds one due date and shows a completion percentage. Use labels for everything an issue can be several of at once, like type or component.

solid answer

~50 s

The structural difference is cardinality. A GitHub issue carries **at most one milestone** but **any number of labels**. That single-valued slot is what lets a milestone mean something: it has an optional **due date**, an open/closed state, and a progress bar derived from its open versus closed issue counts, so it answers "how much of the 2.5 release is left". Labels are an unordered, repo-scoped tag set — good for `bug`, `area:auth`, `good first issue`, and for filtering — but they cannot express completion, because nothing says a labelled set is ever finished. In practice: milestones for a release or a fixed-window batch, labels for classification, and GitHub Projects when you need more than one axis of state at once. Both milestones and labels are scoped to a single repository, which is where they stop scaling.

code

json · 9 lines
json
{
  "number": 12,
  "title": "v2.5.0",
  "state": "open",
  "description": "Auth rewrite and rate-limit work",
  "open_issues": 7,
  "closed_issues": 23,
  "due_on": "2026-09-30T07:00:00Z"
}

go deeper

for a junior

Remember the cardinality: a GitHub issue takes many labels but only one milestone, and the milestone is the one with a due date and a progress bar.

for a middle

Explain why single-valuedness is what makes a progress percentage meaningful, and give the decision rule — milestones for groupings that end, labels for classification that does not.

for a senior

Show where these primitives run out: repository scoping, no automation on close, no weighting by size, and the point at which you move state tracking into a Projects field instead.

for a principal

Decide how much tracking structure the organisation should mandate at all: milestones and labels are free and readable by anyone with repository access, while a richer project schema buys reporting at the cost of a second place work has to be maintained.

## The two primitives GitHub gives an issue three lightweight organising handles: **labels**, a **milestone**, and **assignees**. Labels and milestones look similar from the sidebar but have deliberately different shapes. **Labels** are many-per-issue, unordered, and repository-scoped. A label is a name, a colour, and an optional description. GitHub seeds a new repository with a default set (`bug`, `documentation`, `enhancement`, `good first issue`, `help wanted`, and a few more), which you are free to replace. Labels are pure classification: they say what an issue *is* or *touches*, never how far along it is. **Milestones** are one-per-issue. A milestone has a title, an optional description, an optional **due date**, and an open or closed state. Because an issue can belong to at most one, GitHub can compute a meaningful **progress bar**: closed issues over total issues in that milestone. The milestone page lists everything in it, split into open and closed, and the repository's milestone list shows each one's percentage and how long until — or how far past — the due date. ## The decision rule Ask whether the grouping has an *end*. - "Everything that must ship in 2.5" ends. It is a milestone. - "Everything that is a bug" never ends. It is a label. A second test is whether an issue could legitimately be in two of them. `area:auth` and `bug` coexist happily on one issue; "in the 2.5 release" and "in the 2.6 release" cannot. Anything mutually exclusive and finite fits the milestone slot; anything overlapping fits labels. ## Where each one is used in practice Milestones are typically one of two things: a **release** (`v2.5.0`), or a **time box** (`Sprint 34`, due Friday). Both give the same read: a burn-down you get for free without any project configuration, visible to anyone with read access and to the API. Labels do more jobs than classification alone. They drive filtering (`is:open label:bug label:area:auth`), they are the input other GitHub mechanisms read — automatically generated release notes group merged pull requests by label, and issue forms can apply labels on creation — and they carry social meaning on public repos, where `good first issue` and `help wanted` are surfaced by GitHub's own contributor discovery pages. A common anti-pattern is encoding a *state machine* in labels: `status:todo`, `status:in-progress`, `status:review`. It sort of works, but nothing enforces that exactly one is present, so issues drift into having two or none, and there is no ordering. That job belongs to a GitHub Projects single-select field, which is genuinely single-valued and orderable. ## The limits Both are **repository-scoped**. There is no organisation-level milestone spanning ten repositories, and labels do not inherit — two repos with a `bug` label have two unrelated labels that merely share a name and probably not a colour. As soon as work spans repositories, milestones stop answering the question, and an organisation-level Project with an iteration or date field is the tool that does. Milestones also have no automation of their own: closing a milestone does not close its issues, and closing every issue does not close the milestone. It reaches 100% and sits there until someone closes it. Treat the close as a human act that means "we are done arguing about this release". ## Reading them through the API A milestone object exposes `title`, `state`, `open_issues`, `closed_issues`, `due_on`, and `number`. Those five fields are the whole reporting surface — a script that wants "percent complete for the current release" reads `closed_issues` over the sum, and nothing more. That is a fair summary of the feature's ambition: it is small on purpose, and when you need more axes you have outgrown it.

  • A team encodes workflow state as labels like status:in-progress. What goes wrong?
    Nothing enforces that exactly one status label is present, so issues accumulate two or lose all of them, and the set has no order you can sort a board by. A GitHub Projects single-select field is genuinely single-valued and ordered, which is what a state machine needs.
  • Does closing a GitHub milestone close the issues inside it?
    No, and the reverse is also true — a milestone at 100% stays open until someone closes it. There is no built-in automation on milestones at all. Closing one is a human statement that the batch is settled; any issues still open in it need moving to the next milestone by hand or by script.
  • Your release spans four repositories. Why do milestones stop helping?
    Milestones are repository-scoped, so four repos means four separate milestones and four separate progress bars with no combined view. An organisation-level GitHub Project can hold issues from all four with a shared iteration or target-date field, which is the only cross-repo answer GitHub offers.

saying these in an interview costs you the question

  • Thinks an issue can carry several milestones
  • Uses labels as an exclusive workflow state machine
  • Expects closing a milestone to close its issues
  • Assumes labels are shared across an organisation
  • Believes milestone progress accounts for issue size

context