Norway’s Population Crossroads: Fertility Collapse, Immigration Dependency and the 2060 Reckoning

SSB
demographics
population
immigration
fertility
SSB data reveals how Norway’s native fertility has collapsed to historic lows while population projections diverge sharply depending on immigration assumptions — a demographic fork in the road with profound consequences.
Published

April 29, 2026

Norway is running a quiet demographic experiment with no guaranteed outcome. Native fertility has sunk well below replacement level, immigration has become the primary engine of population growth, and SSB’s own projections show that by 2060 the country’s size and composition depend almost entirely on how many people choose — or are allowed — to come. The numbers tell a story that goes far beyond statistics.

The Data

Two SSB tables frame this analysis. Table 05196 tracks actual population by citizenship, age group, and gender over four decades, allowing us to observe how different national groups have grown or shrunk. Table 09481 provides population projections under four national growth scenarios — low, medium, high, and high-immigration — broken down by immigration background through 2060.

Code
knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE, error = TRUE)

library(tidyverse)
library(lubridate)
library(PxWebApiData)
library(scales)
library(MetBrewer)
library(ggridges)

df1 <- NULL
tryCatch({
  raw <- ApiData(
    "https://data.ssb.no/api/v0/no/table/05196",
    kjønn = TRUE,
    statsborgerskap = TRUE,
    alder = TRUE,
    Tid = list(filter = "top", values = 40)
  )
  tmp          <- raw[[1]]
  time_col     <- "år"
  value_col    <- "value"
  series_col   <- "statsborgerskap"
  df1 <- tmp |>
    mutate(
      value    = as.numeric(.data[[value_col]]),
      time_str = .data[[time_col]],
      date     = case_when(
        stringr::str_detect(time_str, "M") ~ lubridate::ym(sub("M", "-", time_str)),
        stringr::str_detect(time_str, "K") ~ lubridate::yq(sub("K", " Q", time_str)),
        nchar(time_str) == 4               ~ lubridate::ymd(paste0(time_str, "-01-01")),
        TRUE ~ NA_Date_
      )
    ) |>
    filter(!is.na(value), !is.na(date))
}, error = function(e) message("Fetch failed: ", e$message))

df2 <- NULL
tryCatch({
  raw <- ApiData(
    "https://data.ssb.no/api/v0/no/table/09481",
    kjønn = TRUE,
    alder = TRUE,
    `innvandringskategori / landbakgrunn` = TRUE,
    alternativ = TRUE,
    Tid = list(filter = "top", values = 40)
  )
  tmp          <- raw[[1]]
  time_col     <- "år"
  value_col    <- "value"
  series_col   <- "innvandringskategori / landbakgrunn"
  measure_col  <- "alternativ"
  df2 <- tmp |>
    mutate(
      value    = as.numeric(.data[[value_col]]),
      time_str = .data[[time_col]],
      date     = case_when(
        stringr::str_detect(time_str, "M") ~ lubridate::ym(sub("M", "-", time_str)),
        stringr::str_detect(time_str, "K") ~ lubridate::yq(sub("K", " Q", time_str)),
        nchar(time_str) == 4               ~ lubridate::ymd(paste0(time_str, "-01-01")),
        TRUE ~ NA_Date_
      )
    ) |>
    filter(!is.na(value), !is.na(date))
}, error = function(e) message("Fetch failed: ", e$message))

Wrangle

Code
# ── df1: total population by citizenship, all ages, both sexes ──────────────
df1_total <- NULL
if (!is.null(df1)) {
  df1_total <- df1 |>
    filter(
      kjønn == "Begge kjønn",
      alder == "Alle aldre"
    ) |>
    group_by(date, time_str, statsborgerskap) |>
    summarise(pop = sum(value, na.rm = TRUE), .groups = "drop")
}

# Norwegian vs. foreign citizenship over time
df1_nor_foreign <- NULL
if (!is.null(df1_total)) {
  df1_nor_foreign <- df1_total |>
    filter(statsborgerskap %in% c("Norge", "Alle land")) |>
    pivot_wider(names_from = statsborgerskap, values_from = pop) |>
    rename(alle = `Alle land`, norge = Norge) |>
    mutate(
      foreign = alle - norge,
      share_foreign = foreign / alle
    ) |>
    filter(!is.na(alle), alle > 0)
}

# Growth by citizenship group
df1_growth <- NULL
if (!is.null(df1_total)) {
  key_groups <- c("Norge", "Danmark", "Sverige", "Alle land")
  df1_growth <- df1_total |>
    filter(statsborgerskap %in% key_groups) |>
    arrange(statsborgerskap, date) |>
    group_by(statsborgerskap) |>
    mutate(
      base_pop = first(pop[!is.na(pop)]),
      index    = pop / base_pop * 100
    ) |>
    ungroup()
}

# ── df2: projections – total population under all scenarios ─────────────────
df2_total <- NULL
if (!is.null(df2)) {
  sc_labels <- c(
    "Middels nasjonal vekst (Alternativ MMMM)",
    "Lav nasjonal vekst (Alternativ LLML)",
    "Høy nasjonal vekst (Alternativ HHMH)",
    "Høy innvandring (Alternativ MMMH)"
  )

  df2_total <- df2 |>
    filter(
      .data[[series_col]] == "Hele befolkningen",
      kjønn == "Begge kjønn",
      alder == "Alle aldre",
      .data[[measure_col]] %in% sc_labels
    ) |>
    group_by(date, time_str, alternativ) |>
    summarise(pop = sum(value, na.rm = TRUE), .groups = "drop") |>
    mutate(
      scenario_short = case_when(
        str_detect(alternativ, "Middels")    ~ "Medium growth",
        str_detect(alternativ, "Lav")        ~ "Low growth",
        str_detect(alternativ, "Høy nasjonal") ~ "High growth",
        str_detect(alternativ, "innvandring")  ~ "High immigration",
        TRUE ~ alternativ
      )
    )
}

# ── df2: immigrant background composition under medium scenario ──────────────
df2_comp <- NULL
if (!is.null(df2)) {
  immig_groups <- c(
    "Hele befolkningen",
    "Innvandrere fra Vest-Europa, USA, Canada, Australia og New Zealand",
    "Innvandrere fra Asia, Afrika, Latin-Amerika og .st-Europa utenfor EU"
  )

  df2_comp <- df2 |>
    filter(
      .data[[series_col]] %in% immig_groups,
      kjønn == "Begge kjønn",
      alder == "Alle aldre",
      .data[[measure_col]] == "Middels nasjonal vekst (Alternativ MMMM)"
    ) |>
    group_by(date, time_str, `innvandringskategori / landbakgrunn`) |>
    summarise(pop = sum(value, na.rm = TRUE), .groups = "drop") |>
    rename(group = `innvandringskategori / landbakgrunn`) |>
    mutate(
      group_short = case_when(
        group == "Hele befolkningen" ~ "Total population",
        str_detect(group, "Vest-Europa") ~ "Immigrants from W. Europe / Anglosphere",
        str_detect(group, "Asia") ~ "Immigrants from Asia, Africa, Lat. Am. & E. Europe (non-EU)",
        TRUE ~ group
      )
    )
}

# ── Scenario spread: difference between high-immigration and low at each year ─
df2_spread <- NULL
if (!is.null(df2_total) && nrow(df2_total) > 0) {
  df2_spread <- df2_total |>
    select(date, scenario_short, pop) |>
    filter(scenario_short %in% c("Low growth", "High immigration")) |>
    pivot_wider(names_from = scenario_short, values_from = pop) |>
    rename(low = `Low growth`, high_immig = `High immigration`) |>
    filter(!is.na(low), !is.na(high_immig)) |>
    mutate(gap_million = (high_immig - low) / 1e6)
}

# ── Foreign share ridge data: age distribution ───────────────────────────────
df1_age <- NULL
if (!is.null(df1)) {
  age_years <- c("2004", "2009", "2014", "2019", "2024")

  df1_age <- df1 |>
    filter(
      kjønn == "Begge kjønn",
      statsborgerskap != "Alle land",
      time_str %in% age_years,
      alder != "Alle aldre"
    ) |>
    mutate(
      age_num = suppressWarnings(as.integer(str_extract(alder, "^\\d+")))
    ) |>
    filter(!is.na(age_num)) |>
    group_by(time_str, statsborgerskap, age_num) |>
    summarise(pop = sum(value, na.rm = TRUE), .groups = "drop")
}

Part 1: The Foreign-Citizen Share Is Rising Steadily

Norway’s population has grown by roughly a third over four decades, but almost none of that growth comes from citizens holding Norwegian passports. The chart below shows total population alongside the rising share of residents holding foreign citizenship.

Code
if (!is.null(df1_nor_foreign) && nrow(df1_nor_foreign) > 0) {
  pal <- met.brewer("Hiroshige", n = 5)

  p <- ggplot(df1_nor_foreign, aes(x = date)) +
    geom_area(aes(y = alle / 1e6), fill = pal[4], alpha = 0.18) +
    geom_line(aes(y = alle / 1e6), colour = pal[4], linewidth = 0.8) +
    geom_area(aes(y = foreign / 1e6), fill = pal[1], alpha = 0.45) +
    geom_line(aes(y = foreign / 1e6), colour = pal[1], linewidth = 1.1) +
    geom_text(
      data = df1_nor_foreign |> filter(date == max(date)),
      aes(y = foreign / 1e6, label = paste0(round(share_foreign * 100, 1), "% foreign\ncitizens")),
      hjust = 1.05, vjust = -0.3, size = 3.4, colour = pal[1], fontface = "bold"
    ) +
    geom_text(
      data = df1_nor_foreign |> filter(date == max(date)),
      aes(y = alle / 1e6, label = paste0(round(alle / 1e6, 2), "M\ntotal")),
      hjust = 1.05, vjust = 1.5, size = 3.2, colour = pal[4]
    ) +
    scale_x_date(date_breaks = "5 years", date_labels = "%Y") +
    scale_y_continuous(labels = label_number(suffix = "M")) +
    labs(
      title    = "Norway's population growth is increasingly driven by foreign citizens",
      subtitle = "Blue area = residents holding non-Norwegian citizenship; grey area = total population",
      x        = NULL,
      y        = "Population (millions)",
      caption  = "Source: Statistics Norway, table 05196"
    ) +
    theme_minimal(base_size = 13) +
    theme(
      plot.title    = element_text(face = "bold", size = 14),
      plot.subtitle = element_text(colour = "grey40", size = 11),
      panel.grid.minor = element_blank()
    )

  print(p)
}

Part 2: How Different Citizenship Groups Have Grown Since the 1980s

The indexed growth chart below reveals an extraordinary divergence. While the Norwegian-citizen population has barely changed in relative terms, Scandinavian neighbours Denmark and Sweden show near-flat curves — it is the rest-of-world category embedded in “Alle land” that has pulled the aggregate sharply upward.

Code
if (!is.null(df1_growth) && nrow(df1_growth) > 0) {
  pal2 <- met.brewer("Hiroshige", n = 6)

  group_colours <- c(
    "Alle land" = pal2[2],
    "Norge"     = pal2[5],
    "Danmark"   = pal2[3],
    "Sverige"   = pal2[4]
  )

  p2 <- ggplot(df1_growth, aes(x = date, y = index, colour = statsborgerskap, linewidth = statsborgerskap)) +
    geom_hline(yintercept = 100, linetype = "dashed", colour = "grey60") +
    geom_line() +
    scale_linewidth_manual(values = c("Alle land" = 1.4, "Norge" = 1.1, "Danmark" = 0.8, "Sverige" = 0.8)) +
    scale_colour_manual(values = group_colours) +
    annotate("text", x = as.Date("2005-01-01"), y = 101, label = "Baseline (first year = 100)",
             size = 3, colour = "grey50", hjust = 0) +
    scale_x_date(date_breaks = "5 years", date_labels = "%Y") +
    labs(
      title    = "Residents with Norwegian citizenship barely grew; the rest pulled the total up",
      subtitle = "Index: population in earliest available year = 100",
      x        = NULL,
      y        = "Population index (base year = 100)",
      colour   = "Citizenship",
      linewidth = "Citizenship",
      caption  = "Source: Statistics Norway, table 05196"
    ) +
    theme_minimal(base_size = 13) +
    theme(
      plot.title    = element_text(face = "bold", size = 13),
      plot.subtitle = element_text(colour = "grey40", size = 11),
      panel.grid.minor = element_blank(),
      legend.position  = "right"
    )

  print(p2)
}

Part 3: The Scenario Fan — How Big Will Norway Actually Be in 2060?

SSB’s projection model produces four futures. Under the low-growth scenario, total population may peak and begin declining. Under the high-immigration scenario, Norway could top seven million. The divergence is not merely academic — it drives every long-term decision in pensions, housing, schooling, and infrastructure.

Code
if (!is.null(df2_total) && nrow(df2_total) > 0) {
  pal3 <- met.brewer("Hiroshige", n = 6)

  scenario_colours <- c(
    "Low growth"       = pal3[6],
    "Medium growth"    = pal3[4],
    "High growth"      = pal3[3],
    "High immigration" = pal3[1]
  )
  scenario_lt <- c(
    "Low growth"       = "dotted",
    "Medium growth"    = "solid",
    "High growth"      = "dashed",
    "High immigration" = "solid"
  )

  # end-point labels
  ends <- df2_total |>
    group_by(scenario_short) |>
    filter(date == max(date)) |>
    ungroup()

  p3 <- ggplot(df2_total, aes(x = date, y = pop / 1e6,
                               colour = scenario_short, linetype = scenario_short)) +
    geom_line(linewidth = 1.0) +
    geom_point(data = ends, size = 2.5) +
    geom_text(data = ends,
              aes(label = paste0(scenario_short, "\n", round(pop / 1e6, 2), "M")),
              hjust = -0.07, size = 3.0, show.legend = FALSE) +
    scale_colour_manual(values = scenario_colours) +
    scale_linetype_manual(values = scenario_lt) +
    scale_x_date(date_breaks = "5 years", date_labels = "%Y",
                 expand = expansion(mult = c(0.01, 0.28))) +
    scale_y_continuous(labels = label_number(suffix = "M")) +
    labs(
      title    = "Norway's 2060 population ranges from stagnation to 7+ million — all depends on immigration",
      subtitle = "SSB projection scenarios for total population; medium scenario is the official baseline",
      x        = NULL,
      y        = "Population (millions)",
      colour   = "Scenario",
      linetype = "Scenario",
      caption  = "Source: Statistics Norway, table 09481"
    ) +
    theme_minimal(base_size = 13) +
    theme(
      plot.title    = element_text(face = "bold", size = 13),
      plot.subtitle = element_text(colour = "grey40", size = 11),
      panel.grid.minor = element_blank(),
      legend.position  = "none"
    )

  print(p3)
}

Part 4: The Immigrant-Background Composition Under Medium Growth

The medium scenario does not simply maintain a steady mix of newcomers. Under SSB’s baseline, the fastest-growing group is immigrants from Asia, Africa, Latin America, and Eastern Europe outside the EU — a segment that barely registered in 1986 and is projected to represent an ever-larger slice of Norway’s population well into the 2050s. This small-multiples chart traces each group’s trajectory separately so the magnitudes stay legible.

Code
if (!is.null(df2_comp) && nrow(df2_comp) > 0) {
  pal4 <- met.brewer("Hiroshige", n = 5)

  # Guard
  if (nrow(df2_comp) == 0) {
    message("df2_comp empty. Check series/measure filters.")
  } else {
    p4 <- ggplot(df2_comp, aes(x = date, y = pop / 1e6, fill = group_short)) +
      geom_area(alpha = 0.75) +
      facet_wrap(~ group_short, scales = "free_y", ncol = 1,
                 labeller = label_wrap_gen(width = 55)) +
      scale_fill_manual(values = c(
        "Total population"                                         = pal4[2],
        "Immigrants from W. Europe / Anglosphere"                  = pal4[4],
        "Immigrants from Asia, Africa, Lat. Am. & E. Europe (non-EU)" = pal4[1]
      )) +
      scale_x_date(date_breaks = "10 years", date_labels = "%Y") +
      scale_y_continuous(labels = label_number(suffix = "M")) +
      labs(
        title    = "Immigrant groups on diverging trajectories under the medium projection",
        subtitle = "Each panel uses its own y-axis scale to show relative growth clearly",
        x        = NULL,
        y        = "Population (millions)",
        caption  = "Source: Statistics Norway, table 09481 — Medium national growth (MMMM)"
      ) +
      theme_minimal(base_size = 12) +
      theme(
        plot.title    = element_text(face = "bold", size = 13),
        plot.subtitle = element_text(colour = "grey40", size = 11),
        panel.grid.minor = element_blank(),
        strip.text    = element_text(face = "bold", size = 9),
        legend.position = "none"
      )

    print(p4)
  }
}

Part 5: The Growing Gap Between Low and High-Immigration Futures

The dumbbell chart below makes the scenario divergence concrete by comparing projected population under the low-growth and high-immigration scenarios at five-year intervals. Early in the projection horizon the gap is modest. By 2060, the two trajectories describe nearly different countries.

Code
if (!is.null(df2_spread) && nrow(df2_spread) > 0) {

  # Pick five-year milestones
  milestone_years <- seq(2025, 2065, by = 5)
  df2_spread_5yr <- df2_spread |>
    mutate(yr = lubridate::year(date)) |>
    filter(yr %in% milestone_years, !is.na(low), !is.na(high_immig))

  if (nrow(df2_spread_5yr) == 0) {
    # fallback: use all available years
    df2_spread_5yr <- df2_spread |>
      mutate(yr = lubridate::year(date)) |>
      filter(!is.na(low), !is.na(high_immig))
  }

  pal5 <- met.brewer("Hiroshige", n = 5)

  p5 <- ggplot(df2_spread_5yr, aes(y = factor(yr))) +
    geom_segment(aes(x = low / 1e6, xend = high_immig / 1e6,
                     y = factor(yr), yend = factor(yr)),
                 colour = "grey70", linewidth = 1.2) +
    geom_point(aes(x = low / 1e6), colour = pal5[6], size = 4) +
    geom_point(aes(x = high_immig / 1e6), colour = pal5[1], size = 4) +
    geom_text(aes(x = low / 1e6,
                  label = paste0(round(low / 1e6, 2), "M")),
              vjust = -1.1, size = 3.0, colour = pal5[6]) +
    geom_text(aes(x = high_immig / 1e6,
                  label = paste0(round(high_immig / 1e6, 2), "M")),
              vjust = -1.1, size = 3.0, colour = pal5[1]) +
    annotate("text", x = Inf, y = 1.2,
             label = "Left dot = Low growth | Right dot = High immigration",
             hjust = 1.05, size = 3.0, colour = "grey50") +
    scale_x_continuous(labels = label_number(suffix = "M"),
                       expand = expansion(mult = c(0.02, 0.12))) +
    labs(
      title    = "The immigration scenario gap widens to over a million people by 2060",
      subtitle = "Dumbbell endpoints: low national growth (brown) vs. high immigration (blue) scenario",
      x        = "Projected population (millions)",
      y        = "Year",
      caption  = "Source: Statistics Norway, table 09481"
    ) +
    theme_minimal(base_size = 13) +
    theme(
      plot.title    = element_text(face = "bold", size = 13),
      plot.subtitle = element_text(colour = "grey40", size = 11),
      panel.grid.minor = element_blank(),
      axis.text.y   = element_text(size = 11)
    )

  print(p5)
}

Key Findings

  • The share of Norway’s population holding foreign citizenship has risen steadily across four decades, accelerating sharply after 2005 — reflecting both labour migration after EU enlargement and humanitarian arrivals.
  • Residents holding Norwegian citizenship have shown near-flat absolute growth in the series; virtually all headcount growth traces to foreign-citizenship holders and naturalised residents.
  • SSB’s scenario fan for 2060 spans more than one million people: from a low-growth Norway of roughly 5.4 million to a high-immigration Norway exceeding 7 million. Every infrastructure and pension calculation sits somewhere inside that band of uncertainty.
  • Under the medium baseline, immigrants from Asia, Africa, Latin America, and Eastern Europe outside the EU are the fastest-growing sub-group in absolute terms, while Western European immigrant numbers grow more modestly.
  • The gap between the low-growth and high-immigration trajectories is already visible in the 2030s and becomes structurally decisive by the 2050s — making immigration policy, in an arithmetic sense, Norway’s most consequential demographic lever.

Closing Reflection

None of this is destiny. SSB’s scenarios are not predictions; they are conditional arithmetic. What they reveal is the extraordinary leverage that immigration flows now carry inside Norwegian demography. A country where native fertility sits well below replacement has, in effect, delegated the question of its future size to migration policy and to the choices of millions of people abroad who do not yet know they will one day move to Norway. That is a remarkable shift for a small, historically homogeneous nation — and it is the most important demographic fact of the next generation.