Is early-onset colorectal cancer rising in the Netherlands?

Author
Published

October 24, 2026

Doi

Early-onset colorectal cancer has risen markedly in many Western countries among cohorts born since the 1950s and 60s (Siegel et al. 2017; Sung et al. 2025). However, not everyone is immediately convinced of such statistics: a priori you should probably believe that many increases reflect changes in data collection processes rather than genuine changes in disease burden; greater diagnostic scrutiny, evolving case definitions, and the introduction of screening can all increase recorded incidence without any true rise in disease. The Netherlands may provide useful information to separate artefact from reality here. Its cancer registry, the Netherlands Cancer Registry (NKR), run by IKNL, has covered the whole country1 since 1989 and, crucially, makes their detailed, up-to-date, and historical data openly available through an API. I use those data here in two ways: to examine trends in the Netherlands, and to shed broader light on artefacts in early-onset colorectal cancer incidence figures.

NoteAt a glance

The trend

  • Under 50, the incidence of colon and rectum cancer rose by 1.2% a year over the past 30 years, and by 1.0% a year over the past 10.
  • The rise appears steeper in men than in women (+1.3% against +1.1% a year over 30 years, +1.4% against +0.6% over 10).
  • The rise is in the colon (+1.5% a year over 30 years, +2.3% over 10); rectal cancer rose more slowly over 30 years (+0.8%) and fell over the last 10 (-1.0%).
  • At 40-44, people born around 1975 had a 40% higher incidence than people born around 1960 had at the same age, and a 25% higher death rate.
  • From 50 onwards the picture is the reverse: since screening began in 2014, incidence at 50-74 has fallen below its 1989 level. Screening can save(s) (your) live(s)!
  • The vast majority of cases still occurs later in life: 95% of the colon and rectum cancers in 2020-24 were diagnosed at 50 or older.

Is it real?

  • Neuroendocrine tumours and the appendix were excluded from the analysis and do not contribute to these numbers. Based on these Dutch numbers, a registry that does include these would show a steeper rise under 50: +1.6% a year over 30 years instead of +1.2%, and +1.4% over 10 instead of +1.0%.
  • It’s unlikely screening contributes to this rise as no one under 55 is invited, and the rise began long before 2014.
  • The rise is also not caused by a greater diagnosis of small tumours: under 50, stage IV rose by 2.3% a year over the past 30 years and by 1.1% over the past 10, while stage I barely changed (+0.3% and +0.5%).
  • Contrary to incidence, deaths under 50 have not risen (-0.3% a year since 1996, -1.6% over the past 10; over 10 years -3.6% in women and +0.3% in men). However, five-year survival at 18-44 went from 55% for patients diagnosed in 1990-94 to 66% in 2015-19, though it has not improved since 2005-09 (67%). This makes the decrease in deaths per year over the past 10 years harder to attribute to a real increase in disease burden.

Data

To comprehensively describe the trends in colorectal cancer for the Netherlands, I use three sources:

  • Incidence and survival: NKR-cijfers. They provide incidence cases per year, sex and five-year age group from 1989 to 2024 for the colon, the appendix, and the rectosigmoid and rectum (C19-C20). Importantly, they do not only provide this by site, but also by morphology such that we can separate colorectal carcinomas from neuroendocrine tumours of the digestive tract (more on why that is important later). Moreover, they provide detailed stage and relative survival information. The most recent year with data available, 2025, is still provisional, and I leave it out.
  • Deaths: the WHO mortality database, which holds the causes of death of Statistics Netherlands (CBS) at four ICD-10 characters, from 1996 – the first year the Netherlands coded its deaths in ICD-10.
  • Population: CBS StatLine.2

Could we explain away this increase?

Is it the neuroendocrine tumours or the appendix?

Neuroendocrine tumours (NETs) of the digestive tract are mostly small, slow-growing tumours, found at colonoscopy or in an appendix removed for appendicitis. However, they have a different histology and response to treatment. Therefore, they are usually not of interest in colorectal cancer epidemiology and are usually studied separately. Importantly, changes in classification and improved imaging have resulted in sharp increases in their incidence rates (Siegel et al. 2020); in the Netherlands, the reported incidence of colorectal NETs doubled between 2006 and 2011, most of them found at colonoscopy (Kooyker et al. 2020). In France, a change in how registries classify them largely explains the rise in colorectal cancer at 15-39 years, while the rate of adenocarcinoma did not change (Jooste et al. 2025).

The incidence figures above include neither NETs nor appendix cancer, as NKR reports them separately. Not all registries do, however, so I think it’s still useful to look at these trends.

Code: trends of the NETs, appendix and colorectal carcinomas
GROUP_BANDS <- list(`20-34` = 5:7, `35-49` = 8:10, `50-74` = 11:15)
GROUPS <- c(`Colon and rectum` = "Colon and rectum", NETs = "NET", Appendix = "Appendix")
# Ochre and teal next to the post's blue
GROUP_COL <- set_names(c(COL[["Incidence"]], "#C98A1B", "#1B7F79"), names(GROUPS))

groups <- imap(GROUPS, \(src, g) imap(GROUP_BANDS, \(a, b) asr(inc[[src]], a) |>
                                        mutate(band = b)) |>
                 list_rbind() |> mutate(group = g)) |>
  list_rbind() |>
  filter(sex == "Persons") |>
  mutate(group = factor(group, names(GROUPS)))
group_fit <- imap(WINDOWS, \(n, w) eapc(groups, n, keys = c("group", "band", "sex")) |>
                    mutate(window = w)) |>
  list_rbind()

# A panel per age band and a line per tumour group, on one log axis: points
# joined by a pale line, the trend over the window dashed in a darker shade,
# and the group and its EAPC at the line's end, as in the trends figure. ggrepel
# keeps labels that end at the same height apart, to the right of the data.
fig_groups <- function(window) {
  d   <- groups |> filter(rate > 0)
  fit <- group_fit |> filter(window == !!window)
  seg <- fit |>
    mutate(year = map2(year_from, year_to, \(a, z) c(a, z))) |>
    unnest(year) |>
    mutate(rate = exp(intercept + slope * year))
  lab <- fit |>
    mutate(year = year_to, rate = exp(intercept + slope * year_to),
           label = str_c(str_replace(group, " and ", " and\n"), "\n",
                         sprintf("%+.1f%%/yr", eapc)))
  ggplot(d, aes(year, rate, colour = group)) +
    geom_line(linewidth = 0.55, alpha = 0.75) +
    geom_point(data = \(x) filter(x, year == max(year)), size = 1.4) +
    geom_line(data = seg, aes(colour = str_c(group, " trend")), linewidth = 0.7,
              linetype = "62") +
    ggrepel::geom_text_repel(data = lab, aes(label = label), hjust = 0, nudge_x = 0.8,
                             direction = "y", xlim = c(max(d$year) + 0.5, Inf),
                             size = TEXT, fontface = "bold", family = FIG_FONT,
                             lineheight = 0.9, segment.colour = NA,
                             min.segment.length = Inf, box.padding = 0.15, seed = 1) +
    facet_wrap(~band, nrow = 1, labeller = as_labeller(\(b) str_c("Ages ", b))) +
    scale_colour_manual(values = c(GROUP_COL,
                                   set_names(alpha(map_chr(GROUP_COL, shade, by = 0.35), 0.9),
                                             str_c(names(GROUP_COL), " trend"))),
                        guide = "none") +
    scale_y_log10(breaks = c(0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200),
                  labels = scales::label_number(drop0trailing = TRUE)) +
    scale_x_continuous(breaks = trend_years(window, gap = 5),
                       expand = expansion(add = c(1, 14))) +
    coord_cartesian(clip = "off") +
    labs(x = NULL, y = "Rate per 100,000 (age-standardised, log scale)") +
    theme_ft("xy") +
    theme(axis.text.x         = element_text(
            size = rel(if_else(trend_years(window, gap = 5) %in% window_years(window), 0.75, 1)),
            vjust = 1),
          axis.ticks.x        = element_line(colour = COL[["axis"]], linewidth = 0.3),
          axis.ticks.length.x = unit(1.2, "mm"),
          panel.spacing.x     = unit(14, "mm"),
          plot.margin         = margin(5, 30, 5, 5))
}

# Everything a registry that counts by site alone would call colorectal cancer
# (carcinomas with the appendix, plus NETs), and the NETs' share of it in the
# first and last five years
all_morph <- by_sex(bind_rows(inc_codes, net_codes))
net_share <- function(ages, years) {
  n <- \(d) sum(d$cases[d$sex == "Persons" & d$agegrp %in% ages & d$year %in% years])
  100 * n(inc$NET) / n(all_morph)
}

# Colon and rectum with the NETs added back, and with the appendix added back
with_net <- by_sex(bind_rows(inc_codes |> filter(code != APPENDIX), net_codes))
with_app <- by_sex(inc_codes)

group_eapc <- \(group, band, window = "Past 30 years") group_fit |>
  filter(group == !!group, band == !!band, window == !!window)

At 20-34, NETs are about as common as colorectal carcinomas: in a registry that counts every tumour of the colon and rectum, whatever its morphology, about 46% of the cases at these ages in 2020-24 would be NETs.4 At 35-49 their share went from 7% in 1990-94 to 14% in 2020-24. The appendix carcinomas rose fastest of all, +5.1% a year at 20-49 over the past 30 years, as in the US (Bleyer et al. 2025), but they are rare: 0.40 per 100,000 at 20-49 in 2020-24, against 8.7 for colon and rectum. When we add them back the 30-year EAPC under 50 goes from +1.2% a year to +1.5% with every NET, +1.3% with the appendix, and +1.6% with both. Both also rose at 50-74 (+4.3% and +5.7% a year), where the colorectal carcinomas did not (-0.1%) meaning their rise is not one of the young.

Is it screening?

Screening finds cancers earlier, and in its first rounds it finds cancers that would otherwise have been diagnosed years later. This is clearly illustrated by the jump in 2014-2016 in the trends figure. The Dutch programme started in 2014 and invites everyone aged 55 to 75 to do a faecal immunochemical test (FIT) at home every two years (Toes-Zoutendijk et al. 2017); it was phased in by year of birth, and 2019 was the first year in which the whole age range could be invited. No one under 55 has ever been invited. Moreover, the rise under 50 began well before 2014, and 50-54, the one five-year age group over 50 that is never invited, rose too, if only slightly (+0.3% a year over the past 30 years).

However, screening could still reach the under-55s indirectly, through more colonoscopies in the relatives of people with a positive test (though initially I would be skeptical of the idea that Dutch doctors would eagerly follow-up on this). It does line up with the EO age of their children and the colon EO rising slightly faster in the past 10 years (i.e. since the start of LO screening). But anything that makes doctors look more often in people without symptoms would show up as more early-stage tumours. Do we see such an increase?

Is it more diagnosis?

If more young people were looked at – more colonoscopies for vague symptoms, more awareness among doctors – the extra tumours would be small and early: stage I, some stage II. A rise in advanced tumours is much harder to explain that way.5

Code: fill in the hidden stage counts, stage trends and their figure
# ---- stage, with the rows the NKR hides allocated ------------------------------
# As in my report. The NKR hides a whole year x sex x age row of its stage
# table when the row holds about ten cases or fewer: always at 20-24, mostly
# at 25-29, now and then at 30-34, never from 35 up. A hidden row's total is
# known from the incidence counts, and is split across the stages in the
# proportions of the shown rows at 20-49 of the same site, year and sex. That
# is an assumption, not data; for colon and rectum together it covers about 3%
# of the cases at 20-49, for either site alone more, as the NKR hides rows by
# their size.
STAGE_LEVELS   <- c("I", "II", "III", "IV", "Unknown")
STAGES         <- STAGE_LEVELS[1:4]
STAGE_MIX_AGES <- 5:10

stage_raw <- nl_stage$stage
stopifnot(
  "NKR has colorectal cases in stage 0 or NVT; the post assumes none" =
    all(stage_raw$cases[stage_raw$stage %in% c("0", "NVT")] %in% c(0, NA)))
stage_raw <- stage_raw |> filter(stage %in% STAGE_LEVELS)

# The incidence codes of each site, which its stages must add up to
STAGE_SITE_CODES <- list(`Colon and rectum` = CRC, Colon = "C18 excl. C181",
                         Rectum = "C19-C20")
stage_totals <- imap(STAGE_SITE_CODES, \(codes, site) inc_codes |>
  filter(code %in% codes) |>
  summarise(total = sum(cases), .by = c(year, sex, agegrp)) |>
  mutate(site = site)) |>
  list_rbind()

stage_hidden <- stage_raw |>
  summarise(hidden = all(is.na(cases)), .by = c(site, year, sex, agegrp)) |>
  filter(hidden) |>
  inner_join(stage_totals, by = c("site", "year", "sex", "agegrp"))

stage_mix <- stage_raw |>
  filter(!is.na(cases), agegrp %in% STAGE_MIX_AGES) |>
  summarise(n = sum(cases), .by = c(site, year, sex, stage)) |>
  mutate(p = n / sum(n), .by = c(site, year, sex))

stage_counts <- bind_rows(
  stage_raw |> filter(!is.na(cases)) |> mutate(imputed = FALSE),
  stage_hidden |>
    inner_join(stage_mix |> select(site, year, sex, stage, p),
               by = c("site", "year", "sex"), relationship = "many-to-many") |>
    transmute(site, year, sex, agegrp, stage, cases = total * p, imputed = TRUE))

# The parts must add up to the whole, shown rows and allocated ones alike
stopifnot("the stages do not add up to the incidence counts" =
            stage_counts |>
              summarise(n = sum(cases), .by = c(site, year, sex, agegrp)) |>
              inner_join(stage_totals, by = c("site", "year", "sex", "agegrp")) |>
              with(isTRUE(all.equal(n, total))))

# Per site, stage and sex, and both sexes added up, over the population of the
# incidence counts
stage_sexes <- stage_counts |>
  summarise(imputed = sum(cases[imputed]), cases = sum(cases),
            .by = c(site, sex, year, agegrp, stage)) |>
  (\(x) bind_rows(x, x |>
                    summarise(imputed = sum(imputed), cases = sum(cases),
                              .by = c(site, year, agegrp, stage)) |>
                    mutate(sex = "Persons")))() |>
  left_join(inc$`Colon and rectum` |> mutate(sex = as.character(sex)) |>
              select(sex, year, agegrp, pop),
            by = c("sex", "year", "agegrp")) |>
  mutate(sex = factor(sex, SEXES))

STAGE_BANDS <- list(`Early onset, 20-49` = 5:10, `Later onset, 50-74` = 11:15)
stage_asr <- expand_grid(site = SITES, stage = STAGES) |>
  pmap(\(site, stage) imap(STAGE_BANDS, \(a, b)
    asr(filter(stage_sexes, site == !!site, stage == !!stage), a) |>
      mutate(site = site, stage = stage, band = b)) |>
      list_rbind()) |>
  list_rbind()
stage_fit <- imap(WINDOWS, \(n, w)
  eapc(stage_asr, n, keys = c("site", "band", "stage", "sex")) |> mutate(window = w)) |>
  list_rbind()
stage_eapc <- \(band, stage, window, site = "Colon and rectum", sex = "Persons") stage_fit |>
  filter(band == !!band, stage == !!stage, window == !!window, site == !!site,
         sex == !!sex)

imputed_share <- \(ages, site = "Colon and rectum")
  with(filter(stage_sexes, agegrp %in% ages, site == !!site, sex == "Persons"),
       100 * sum(imputed) / sum(cases))

# The share of each band's cases in each stage, per year, and the share of
# stage III and IV over a run of years
stage_share <- imap(STAGE_BANDS, \(a, b)
  stage_sexes |>
    filter(agegrp %in% a) |>
    summarise(cases = sum(cases), .by = c(site, sex, year, stage)) |>
    mutate(share = 100 * cases / sum(cases), .by = c(site, sex, year)) |>
    mutate(band = b)) |>
  list_rbind()
advanced_share <- \(band, from, to, site = "Colon and rectum", sex = "Persons") with(
  filter(stage_share, band == !!band, site == !!site, sex == !!sex, between(year, from, to)),
  100 * sum(cases[stage %in% c("III", "IV")]) / sum(cases))

# The first year of a window, as eapc() counts it
window_from <- \(window) max(stage_asr$year) - WINDOWS[[window]] + 1L

# Stage I and II a shade darker than a fill would need, so their points and
# lines show on white too
STAGE_COL <- c(I = "#9CBDE2", II = "#6A99D0", III = "#3D72B8", IV = "#0F3F7A",
               Unknown = "#D9D4CF")

# The rate of each stage, a panel per band with its own y axis, the same in
# both windows: it runs to the highest rate or trend of the band. Points joined
# by a pale line, the trend over the window dashed in a darker shade, and its
# EAPC at the line's end, which also names the stage -- as in the trends figure. The
# dotted lines mark the 7th TNM edition, labelled at the foot of the panel,
# and at 50-74 the start of screening, labelled at the top as in the trends figure.
fig_stage <- function(window, sex, site) {
  panels <- imap(names(STAGE_BANDS), \(b, i) {
    # the axis runs to the highest rate or trend of the band in any site or sex
    fit_all <- stage_fit |> filter(band == b)
    top <- max(filter(stage_asr, band == b)$rate,
               exp(fit_all$intercept + fit_all$slope * fit_all$year_from),
               exp(fit_all$intercept + fit_all$slope * fit_all$year_to), na.rm = TRUE)
    d   <- stage_asr |>
      filter(band == b, site == !!site, sex == !!sex) |>
      mutate(stage = factor(stage, STAGES))
    fit <- fit_all |> filter(window == !!window, site == !!site, sex == !!sex)
    seg <- fit |>
      mutate(year = map2(year_from, year_to, \(a, z) c(a, z))) |>
      unnest(year) |>
      mutate(rate = exp(intercept + slope * year))
    lab <- fit |>
      mutate(year = year_to, rate = exp(intercept + slope * year_to),
             label = str_c("Stage ", stage, " ", sprintf("%+.1f%%/yr", eapc)))
    marker <- \(x, label, y, vjust) list(
      geom_vline(xintercept = x, colour = COL[["context"]], linetype = "dotted",
                 linewidth = 0.45),
      annotate("text", x = x, y = y, label = label, hjust = 1.05, vjust = vjust,
               size = TEXT * 0.9, colour = COL[["context"]], family = FIG_FONT,
               lineheight = 0.95))
    screen <- if (b == "Later onset, 50-74")
      marker(2013.5, "Screening starts\n(FIT, 55-75)", top, 1)
    ggplot(d, aes(year, rate, colour = stage)) +
      marker(2009.5, "7th TNM\nedition", 0, -0.3) +
      screen +
      geom_line(linewidth = 0.4, alpha = 0.35) +
      geom_point(size = 0.9) +
      geom_line(data = seg, aes(colour = str_c(stage, " trend"), group = stage),
                linewidth = 0.8, linetype = "62") +
      ggrepel::geom_text_repel(data = lab, aes(label = label), hjust = 0, nudge_x = 0.8,
                               direction = "y", xlim = c(max(d$year) + 0.5, Inf), size = TEXT, fontface = "bold",
                               family = FIG_FONT, segment.colour = NA,
                               min.segment.length = Inf, box.padding = 0.15, seed = 1) +
      scale_colour_manual(values = c(STAGE_COL[STAGES],
                                     set_names(map_chr(STAGE_COL[STAGES], shade, by = 0.3),
                                               str_c(STAGES, " trend"))),
                          guide = "none") +
      scale_y_continuous(expand = expansion(mult = c(0, 0.05)),
                         name = if (i == 1) "Rate per 100,000 (age-standardised)" else NULL) +
      scale_x_continuous(breaks = seq(1990, 2020, 10), expand = expansion(add = c(1, 15))) +
      coord_cartesian(ylim = c(0, top), clip = "off") +
      labs(title = b, x = NULL) +
      theme_ft("xy")
  })
  wrap_plots(panels, nrow = 1)
}

# The stage mix of each band, stacked to 100%: stage IV on the baseline and III
# on it, so the advanced share reads off the axis from zero; unknown against
# the top. The years before the window are faded, and the III-IV share of its
# first and last five years is written over the middle of those years. Each
# stage is named in its layer at the start of the panel, in the faded years;
# unknown, too thin for its name, above the panel's right end, clear of its title.
fig_stage_share <- function(window, sex, site) {
  from <- window_from(window)
  to   <- max(stage_share$year)
  d <- stage_share |>
    filter(site == !!site, sex == !!sex) |>
    # geom_area stacks the first level on top
    mutate(stage = factor(stage, c("Unknown", STAGES)),
           band  = factor(band, names(STAGE_BANDS)))
  adv <- expand_grid(band = factor(names(STAGE_BANDS), names(STAGE_BANDS)),
                     end = c("first", "last")) |>
    mutate(y1    = if_else(end == "first", from, to - 4L),
           share = map2_dbl(as.character(band), y1,
                            \(b, y) advanced_share(b, y, y + 4L, site, sex)),
           year  = y1 + 2,
           label = str_c(y1, "-", str_sub(y1 + 4L, 3, 4), "\n", fmt(share, 0), "%"))
  area <- geom_area(colour = "white", linewidth = 0.2)
  at   <- min(d$year) + 1
  # where geom_area put each layer, read back from the plot
  names_at <- layer_data(ggplot(d, aes(year, share, fill = stage)) + area +
                           facet_wrap(~band, nrow = 1)) |>
    filter(abs(x - at) < 1e-9) |>
    transmute(band  = factor(levels(d$band)[PANEL], levels(d$band)),
              stage = levels(d$stage)[group],
              year  = if_else(stage == "Unknown", max(d$year), min(d$year) + 0.6),
              share = if_else(stage == "Unknown", 100, (ymin + ymax) / 2),
              label = if_else(stage == "Unknown", "Unknown", str_c("Stage ", stage)))
  ggplot(d, aes(year, share, fill = stage)) +
    area +
    annotate("rect", xmin = -Inf, xmax = from - 0.5, ymin = -Inf, ymax = Inf,
             fill = "white", alpha = 0.5) +
    geom_text(data = names_at, aes(year, share, label = label,
                                   hjust = if_else(stage == "Unknown", 1, 0),
                                   vjust = if_else(stage == "Unknown", -0.4, 0.5),
                                   colour = stage == "Unknown"),
              inherit.aes = FALSE, size = TEXT * 0.85, family = FIG_FONT, fontface = "bold") +
    scale_colour_manual(values = c(`FALSE` = COL[["ink"]], `TRUE` = COL[["context"]]),
                        guide = "none") +
    geom_vline(xintercept = 2009.5, colour = "white", linetype = "dotted", linewidth = 0.5) +
    geom_label(data = adv, aes(year, share, label = label),
               inherit.aes = FALSE, vjust = -0.15, size = TEXT * 0.85, family = FIG_FONT,
               lineheight = 0.9,
               fill = alpha("white", 0.85), linewidth = 0, label.padding = unit(0.3, "mm")) +
    facet_wrap(~band, nrow = 1) +
    scale_fill_manual(values = STAGE_COL, guide = "none") +
    scale_x_continuous(breaks = seq(1990, 2020, 10), expand = c(0, 0)) +
    scale_y_continuous(breaks = seq(0, 100, 25), expand = c(0, 0)) +
    coord_cartesian(clip = "off") +
    labs(x = NULL, y = "% of cases") +
    theme_ft("xy") +
    theme(panel.spacing.x = unit(8, "mm"))
}

Under 50, stage I barely changed over the past 30 years (+0.3% a year), while stage IV – tumours that have already spread to distant organs – rose by +2.3% a year (95% CI +2.0 to +2.7). Stage II fell (-1.0%) and stage III rose (+2.6%), but that may largely be tumours moving from one to the other because of the classification changes introduced in the 7th TNM edition. Moreover, pathologists could perhaps examine more lymph nodes now resulting in the detecting of more tumours that have reached the nodes (stage III rather than II). However, we can compare stage I and IV as they should be unaffected. In the colon over the past 10 years, the window that overlaps with screening, stage I did rise (+1.0% a year), but stage III and IV rose far faster (+3.9% and +2.3%) – the opposite of what more looking would do.

Stage III and IV made up 52% of the cases under 50 in 1995-99, and 70% in 2020-24. Jooste et al. saw the same in France: under 50, metastatic adenocarcinoma rose while adenocarcinoma overall did not (Jooste et al. 2025).

At 50-74 the effect of FIT screening (55-75y) is evident again: stage I jumped after its start, and over the past 10 years all stages fell, as this window starts at the peak of the first screening round.

Code: five-year relative survival, and its figure
# Five-year relative survival from NKR-cijfers, per 5-year period of diagnosis.
# 2020-24 is left out: most of its patients have not yet been followed for
# five years, so its five-year figure rests on the few diagnosed in 2020.
SURV_FROM <- 1990L
SURV_TO   <- 2015L
surv <- nl_surv$survival |>
  filter(years == 5, between(period, SURV_FROM, SURV_TO)) |>
  mutate(period_mid = period + 2.5)
# The NKR hides the survival of some small groups of one site and sex; the
# figure leaves a gap there, but the figures quoted in the text, of colon and
# rectum and both sexes, must all be shown
stopifnot("the NKR hides a survival figure of colon and rectum, both sexes" =
            !anyNA(filter(surv, site == "Colon and rectum", sex == "Persons")$survival))
surv_at <- \(age, stage, period, site = "Colon and rectum", sex = "Persons") surv |>
  filter(age == !!age, stage == !!stage, period == !!period, site == !!site,
         sex == !!sex) |>
  pull(survival)
# The share dying of the disease within five years, last period over first
excess_ratio <- (100 - surv_at("18-44", "All", SURV_TO)) /
  (100 - surv_at("18-44", "All", SURV_FROM))

# The stages in the blues of the stage figure, all stages together in ink, heavier;
# the youngest age group in the incidence colour, 45-54 -- which straddles 50 --
# in a paler shade of it, and the older groups in grey, darkening with age
SURV_STAGE_COL <- c(All = COL[["ink"]], STAGE_COL[STAGES])
SURV_STAGE_LAB <- c(All = "All stages", set_names(str_c("Stage ", STAGES), STAGES))
SURV_AGE_COL   <- set_names(c(COL[["Incidence"]], "#7EA6D6",
                              colorRampPalette(c("#B5AEA7", COL[["context"]]))(3)),
                            unname(NKR_SURV_AGES))
SURV_PERIODS   <- seq(SURV_FROM, SURV_TO, 5L) + 2.5
# The tabs of the figure: a panel per stage, or per age group
SURV_VIEWS     <- c(stage = "By stage", age = "By age group")

# Two views of the same numbers: `by = "age"` draws a panel per age group and a
# line per stage, `by = "stage"` the reverse, with the line that stands out --
# all stages, or 18-44 -- drawn heavier. One y axis throughout; the dotted line
# marks the 7th TNM edition, as in the stage figure. Every period is named, at a
# slant and in smaller type so that they fit.
fig_survival <- function(by = c("age", "stage"), sex, site) {
  by <- match.arg(by)
  d <- surv |>
    filter(site == !!site, sex == !!sex) |>
    mutate(stage = factor(SURV_STAGE_LAB[stage], SURV_STAGE_LAB),
           age   = factor(age, unname(NKR_SURV_AGES)))
  line <- if (by == "age") "stage" else "age"
  cols <- if (by == "age") set_names(SURV_STAGE_COL, SURV_STAGE_LAB) else SURV_AGE_COL
  ggplot(d, aes(period_mid, survival, colour = .data[[line]], group = .data[[line]])) +
    geom_vline(xintercept = 2010, colour = COL[["context"]], linetype = "dotted",
               linewidth = 0.45) +
    geom_line(aes(linewidth = .data[[line]] %in% c("All stages", "18-44"))) +
    geom_point(size = 1.1) +
    facet_wrap(vars(.data[[by]]), nrow = 1) +
    scale_colour_manual(values = cols) +
    scale_linewidth_manual(values = c(`FALSE` = 0.6, `TRUE` = 1.1), guide = "none") +
    scale_y_continuous(limits = c(0, 100), breaks = seq(0, 100, 25),
                       expand = expansion(mult = c(0, 0.02))) +
    scale_x_continuous(breaks = SURV_PERIODS,
                       labels = \(x) str_c(x - 2.5, "-", str_sub(x + 1.5, 3, 4)),
                       expand = expansion(add = 2)) +
    labs(x = "Period of diagnosis", y = "5-year relative survival (%)") +
    theme_ft("xy") +
    theme(axis.text.x        = element_text(angle = 45, hjust = 1, vjust = 1,
                                            size = rel(0.75)),
          axis.ticks.x       = element_line(colour = COL[["axis"]], linewidth = 0.3),
          axis.ticks.length.x = unit(1, "mm"))
}

Another way to check the diagnosis argument is deaths. If the extra tumours were harmless, deaths would not follow. In the Dutch under 50y data, deaths have not risen (trends figure): -0.3% a year since 1996, though rectal cancer deaths did (+1.0% a year (95% CI +0.3 to +1.6)).Over the past 10 years they fell clearly in women (-3.6% a year (95% CI -5.9 to -1.3)), but not in men (+0.3% a year (95% CI -2.6 to +3.4)). Survival, meanwhile, improved a lot (figure below). Five-year relative survival of colon and rectum cancer at 18-44 years, the NKR’s youngest age group for survival, was 55% for patients diagnosed in 1990-94 and 66% for those diagnosed in 2015-19, and the gain was largest in the advanced stages: at stage III from 49% to 81%, and at stage IV from 7% to 21%. More tumours, each less often fatal, can leave the death rate largely unchanged while the incidence rises. A sceptic could object that finding harmless tumours raises survival too. But stage I did not rise, and the gains were largest in stage III and IV. Most of that gain came before 2005-09, however: since then (67%) survival has not improved, which leaves the past 10 years harder to explain (see below).

NKR hides the figures for small groups, resulting in a gap in the line: 27 of the 1350 figures. The NKR publishes survival only in these age groups, which differ from those in the rest of the post. A stage’s survival can rise when the TNM edition moves tumours between stages (dotted line: the 7th edition, from 2010).

We can do some napkin math here too: if incidence rose by 1.2% a year, and the share of patients who die of the disease within five years fell from about 45% to 34% over the 25 years between those two survival periods, about 1.2% a year if spread evenly, deaths would change by about 0.0% a year – close to the -0.3% a year we see.

The same sum does not work as well for the past 10 years. Incidence rose by 1.0% a year and survival stood still, so deaths should have risen by about as much; instead they fell (-1.6% a year). Part of this may be chance, as – fortunately – few people under 50 die of colorectal cancer each year. Another option is the C26.0 code. These are deaths certified as “intestinal tract, part unspecified” which in practice are mostly bowel cancers whose site was not recorded. A growing share of Dutch bowel cancer deaths ends up there: at all ages it went from 8% in 1996 to 17% in 2024. However, under 50 it grew less, and stopped growing after the mid-2000s: 7% in 1996-2004, 10% in 2005-14 and 9% in 2015-24. Counting them all as colorectal cancer only gives -0.1% a year since 1996. So it seems that the past 10 years may in part be some (awareness) artefact.

Is it the bookkeeping?

  • Coding. I mentioned some coding changes already (appendix, TNM), but there’s one more: CBS switched to coding causes of death automatically in 2013 (CBS), which moved some deaths between codes; however the death rate under 50 was flat before and after it.
  • COVID-19. Screening paused for several months in 2020, and both windows include that year. Under 50, the rate in 2020 does not stand out (trends figure).

So, is early-onset colorectal cancer really rising in the Netherlands?

I would say so yes. Under 50, colon and rectum cancer has become more common by 1.2% a year over the past 30 years and by 1.0% a year over the past 10 – mostly in the colon, mostly at an advanced stage, and not because of neuroendocrine tumours, the appendix, screening, or more diagnosis of small tumours. This means that at the same age, people born since approximately 1960 have faced progressively higher colorectal cancer risk. Over 30 years deaths have not followed, which improved survival can explain; their fall over the past 10 years, while survival stood still, is a bit of a loose end.

After separating artefact from reality, the next step is to figure out what exposure is causing this. A rise in early-onset that is carried by birth cohorts, combined with cancer’s long induction period, points to something in how the generations born since the 1960s grew up. Did some harmful exposure suddenly become much more prevalent? In the ECCE project, we are following up on promising evidence from the mutational signatures found in the colon of these people. Stay tuned!

References

Bleyer, Archie et al. 2025. “Colon, Colorectal, and All Cancer Incidence Increase in the Young Due to Appendix Reclassification.” JNCI: Journal of the National Cancer Institute 117 (7): 1340–49. doi:10.1093/jnci/djaf038.
Downham, Laura et al. 2025. “Increase of Early-Onset Colorectal Cancer: A Cohort Effect.” JNCI: Journal of the National Cancer Institute, djaf238. doi:10.1093/jnci/djaf238.
Jooste, Valérie et al. 2025. “Epidemiological Classification Changes and Incidence of Early-Onset Colorectal Cancer.” JAMA Network Open 8 (11): e2541732. doi:10.1001/jamanetworkopen.2025.41732.
Kooyker, Arthur I. et al. 2020. “Change in Incidence, Characteristics and Management of Colorectal Neuroendocrine Tumours in the Netherlands in the Last Decade.” United European Gastroenterology Journal 8 (1): 59–67. doi:10.1177/2050640619865113.
Siegel, Rebecca L. et al. 2017. “Colorectal Cancer Incidence Patterns in the United States, 1974–2013.” JNCI: Journal of the National Cancer Institute 109 (8): djw322. doi:10.1093/jnci/djw322.
Siegel, Rebecca L. et al. 2020. “Colorectal Cancer Statistics, 2020.” CA: A Cancer Journal for Clinicians 70 (3): 145–64. doi:10.3322/caac.21601.
Sung, Hyuna et al. 2025. “Colorectal Cancer Incidence Trends in Younger Versus Older Adults: An Analysis of Population-Based Cancer Registry Data.” The Lancet Oncology 26 (1): 51–63. doi:10.1016/S1470-2045(24)00600-4.
Toes-Zoutendijk, Esther et al. 2017. “Real-Time Monitoring of Results During First Year of Dutch Colorectal Cancer Screening Program and Optimization by Altering Fecal Immunochemical Test Cut-Off Levels.” Gastroenterology 152 (4): 767–775.e2. doi:10.1053/j.gastro.2016.11.022.

Footnotes

  1. You may be surprised to learn how many countries do not have a national cancer registry: France, the USA, Australia, Canada to name a few!↩︎

  2. CBS StatLine offers an “average population” by age (gemiddelde bevolking), which seems the obvious choice. But in that table CBS counts people by the age they reach during the year, which is half a year older than the age at diagnosis or death that the NKR and WHO count. Against those counts it shifts people into older age groups, and makes an age-standardised rate about 2% too low. I use the mean of the population on 1 January of the year and on 1 January of the next year, at the same age, instead.↩︎

  3. Leaving out the youngest and oldest ages is common in descriptive cancer epidemiology as under 20, colorectal cancer is very rare and from 75 on, the data tend to be less reliable (in some registries): older people with other illnesses are less often investigated down to a confirmed diagnosis, and their causes of death are harder to pin down. 50-74 also matches up with the ages that the Dutch screening programme invites (55-75).↩︎

  4. An upper bound: the NKR does not split its NET group by site, so this includes the NETs of the stomach and small bowel too.↩︎

  5. Better imaging can move tumours from stage III to stage IV, by finding small metastases that used to go unnoticed. This may explain part of the rise in stage IV. However, this would explain the relative, not their absolute rise.↩︎

Citation

BibTeX citation:
@online{oosterwegel2026,
  author = {Oosterwegel, Max J.},
  title = {Is Early-Onset Colorectal Cancer Rising in the
    {Netherlands?}},
  date = {2026-10-24},
  url = {https://maxoosterwegel.com/blog/crc-netherlands/},
  doi = {placeholder},
  langid = {en}
}
For attribution, please cite this work as:
Oosterwegel, Max J. 2026. “Is Early-Onset Colorectal Cancer Rising in the Netherlands?” October 24. doi:placeholder.