District Heating Luleå

Heat Generation Mix, Industrial Contracts & Transition Projections

Author

Amna Maqsood — George Washington University / LTU

Published

May 6, 2026

Overview

Luleå’s district heating system is one of Sweden’s most distinctive — and most exposed. For over 50 years, roughly 90% of the city’s heat has come from a single industrial partner: SSAB’s steel plant on the waterfront. That partnership is now entering its most consequential transition since it began.

Note

2025 heat generation mix — Luleå Energi (official)
86% Recycled · 9% Renewable · 5% Fossil
Source: Luleå Energi environmental declaration 2025

This report visualizes the historical heat generation data, the industrial contract landscape, and scenario projections for the period leading to and following SSAB’s blast furnace shutdown (~2030).


Heat generation mix

Historical mix 2019–2025

Show code
heat_mix <- data.frame(
  year = c(2019, 2020, 2021, 2022, 2023, 2024, 2025),
  ssab_gas_lulekraft = c(87, 86, 85, 84, 82, 80, 78),
  biofuel_pellets    = c(8,  9,  9,  9,  10, 10, 9),
  electric_boilers   = c(3,  3,  4,  4,  5,  6,  5),
  other              = c(2,  2,  2,  3,  3,  4,  4)
)

heat_long <- heat_mix |>
  pivot_longer(-year, names_to = "source", values_to = "pct") |>
  mutate(source = factor(source,
    levels = c("ssab_gas_lulekraft","biofuel_pellets","electric_boilers","other"),
    labels = c("SSAB gas (Lulekraft)","Biofuel / pellets","Electric boilers","Other")
  ))

p_mix <- ggplot(heat_long, aes(x = factor(year), y = pct, fill = source,
                               text = paste0(source, ": ", pct, "%"))) +
  geom_bar(stat = "identity", width = 0.7) +
  scale_fill_manual(values = c(
    "SSAB gas (Lulekraft)" = "#A32D2D",
    "Biofuel / pellets"    = "#1D9E75",
    "Electric boilers"     = "#185FA5",
    "Other"                = "#BA7517"
  )) +
  labs(x = "Year", y = "Share (%)", fill = NULL) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom",
        panel.grid.major.x = element_blank())

ggplotly(p_mix, tooltip = "text") |>
  layout(legend = list(orientation = "h", y = -0.15))

2025 breakdown — official declaration

The graphic below reflects Luleå Energi’s published 2025 environmental declaration:

Show code
mix_2025 <- data.frame(
  category = c("Recycled", "Renewable", "Fossil"),
  pct      = c(86, 9, 5),
  color    = c("#1D9E75", "#BA7517", "#5F5E5A")
)

p_2025 <- plot_ly(mix_2025,
  labels = ~category, values = ~pct,
  type = "pie", hole = 0.55,
  marker = list(colors = mix_2025$color,
                line = list(color = "#FFFFFF", width = 2)),
  textinfo = "label+percent",
  textfont = list(size = 14)
) |>
  layout(
    title = list(text = "Luleå DH energy mix 2025", font = list(size = 16)),
    showlegend = TRUE,
    legend = list(orientation = "h")
  )
p_2025

Emission factors (2025, Luleå Energi):

Factor Value
Combustion emissions 36.91 g CO₂eq/kWh
Transport emissions 3.60 g CO₂eq/kWh
Bio-oil origin Hungary, Spain, France, Portugal
Pellet raw material Sweden

Residential prices

Show code
prices <- data.frame(
  year            = c(2019,  2020,  2021,  2022,  2023,  2024),
  lulea           = c(9245,  9506,  9754,  9451, 10457, 10919),
  north_avg       = c(12884, 13201, 13550, 13890, 14820, 15671),
  south_avg       = c(15087, 15420, 15890, 16540, 18120, 19654),
  national_median = c(14200, 14600, 15100, 15800, 17900, 19466)
)

price_long <- prices |>
  pivot_longer(-year, names_to = "series", values_to = "sek") |>
  mutate(series = recode(series,
    "lulea"           = "Luleå",
    "north_avg"       = "North Sweden avg",
    "south_avg"       = "South Sweden avg",
    "national_median" = "National median"
  ))

p_price <- ggplot(price_long,
    aes(x = year, y = sek, color = series, linetype = series,
        text = paste0(series, " (", year, "): ", format(sek, big.mark=","), " SEK"))) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  scale_color_manual(values = c(
    "Luleå"            = "#A32D2D",
    "North Sweden avg" = "#185FA5",
    "South Sweden avg" = "#BA7517",
    "National median"  = "#888780"
  )) +
  scale_linetype_manual(values = c(
    "Luleå"            = "solid",
    "North Sweden avg" = "dashed",
    "South Sweden avg" = "dashed",
    "National median"  = "dotted"
  )) +
  scale_y_continuous(labels = label_comma()) +
  labs(x = "Year", y = "SEK per year (15 000 kWh)", color = NULL, linetype = NULL) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom")

ggplotly(p_price, tooltip = "text") |>
  layout(legend = list(orientation = "h", y = -0.15))
Tip

Luleå has Sweden’s lowest district heating prices, driven by cheap industrial waste gas from SSAB. In 2024 Luleå residents paid 10,919 SEK/year vs a national median of 19,466 SEK — a 44% discount. This advantage is directly tied to the SSAB contract and is at risk as the transition unfolds.


Industrial contracts

Show code
contracts <- data.frame(
  Partner         = c("SSAB / LuleKraft (BF gas)",
                      "SSAB EAF + biochar (MOU)",
                      "Uniper (hydrogen waste heat)",
                      "LKAB Svartön (sulfuric acid)",
                      "Own reserve boilers",
                      "Thermal storage (Aronstorp)"),
  Status          = c("In transition","In transition","Planned",
                      "Planned","Confirmed","Confirmed"),
  Heat_source     = c("Blast furnace process gas",
                      "EAF waste heat + biochar gas",
                      "Electrolysis waste heat",
                      "Sulfuric acid process heat",
                      "Pellets / bio-oil / electricity",
                      "Buffer / flexibility asset"),
  Contract_start  = c(1970, 2030, 2024, 2026, 1980, 2022),
  Contract_end    = c(2030, 2040, 2035, 2036, 2040, 2042),
  Years_remaining = c(5, 15, 10, 11, 15, 17),
  Confirmed       = c("Yes","MOU only","Yes","Under assessment","Yes","Yes")
)

contracts |>
  kable(col.names = c("Partner","Status","Heat source","Start","End",
                      "Years left","Confirmed"),
        align = c("l","l","l","c","c","c","c")) |>
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE, font_size = 13) |>
  row_spec(which(contracts$Status == "In transition"),
           background = "#FFF3CD") |>
  row_spec(which(contracts$Status == "Planned"),
           background = "#D1ECF1") |>
  row_spec(which(contracts$Status == "Confirmed"),
           background = "#D4EDDA")
Partner Status Heat source Start End Years left Confirmed
SSAB / LuleKraft (BF gas) In transition Blast furnace process gas 1970 2030 5 Yes
SSAB EAF + biochar (MOU) In transition EAF waste heat + biochar gas 2030 2040 15 MOU only
Uniper (hydrogen waste heat) Planned Electrolysis waste heat 2024 2035 10 Yes
LKAB Svartön (sulfuric acid) Planned Sulfuric acid process heat 2026 2036 11 Under assessment
Own reserve boilers Confirmed Pellets / bio-oil / electricity 1980 2040 15 Yes
Thermal storage (Aronstorp) Confirmed Buffer / flexibility asset 2022 2042 17 Yes

Contract timeline

Show code
contracts_plot <- contracts |>
  mutate(Partner = factor(Partner, levels = rev(Partner)),
         color = case_when(
           Status == "Confirmed"      ~ "#1D9E75",
           Status == "In transition"  ~ "#BA7517",
           Status == "Planned"        ~ "#185FA5"
         ))

p_timeline <- ggplot(contracts_plot,
    aes(y = Partner, xmin = Contract_start, xmax = Contract_end,
        color = Status,
        text = paste0(Partner, "\n", Contract_start, "–", Contract_end,
                      "\nStatus: ", Status))) +
  geom_linerange(linewidth = 6, alpha = 0.85) +
  geom_vline(xintercept = 2025, linetype = "dashed",
             color = "#444441", linewidth = 0.8) +
  annotate("text", x = 2025.5, y = 0.4, label = "Now",
           hjust = 0, size = 3.5, color = "#444441") +
  scale_color_manual(values = c(
    "In transition" = "#BA7517",
    "Planned"       = "#185FA5",
    "Confirmed"     = "#1D9E75"
  )) +
  scale_x_continuous(breaks = seq(1970, 2045, 5)) +
  labs(x = "Year", y = NULL, color = "Status") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom",
        panel.grid.major.y = element_blank(),
        axis.text.x = element_text(angle = 30, hjust = 1))

ggplotly(p_timeline, tooltip = "text") |>
  layout(legend = list(orientation = "h", y = -0.2))

Transition gap projection

Warning

These are illustrative estimates, not official projections. They are based on Luleå Energi public statements, the SSAB/Luleå Energi 2023 MOU, and NSD investigative reporting (September 2024). SSAB has not confirmed exact EAF waste heat volumes.

Show code
gap <- data.frame(
  year               = 2024:2035,
  ssab_bf_gas        = c(80,78,75,70,60,40, 0, 0, 0, 0, 0, 0),
  ssab_eaf_waste     = c( 0, 0, 0, 0, 0, 0,20,25,30,30,30,30),
  new_industrial     = c( 5, 6,10,12,15,18,22,28,32,35,38,40),
  own_production     = c(10,10,10,10,10,12,15,18,18,18,18,18),
  gap_unconfirmed    = c( 0, 0, 0, 3,10,25,38,24,15,12, 9, 7)
)

gap_long <- gap |>
  pivot_longer(-year, names_to = "source", values_to = "pct") |>
  mutate(source = factor(source,
    levels = c("ssab_bf_gas","ssab_eaf_waste","new_industrial",
               "own_production","gap_unconfirmed"),
    labels = c("SSAB BF gas (phasing out)",
               "SSAB EAF waste heat (est.)",
               "New industrial sources",
               "Own production (backup)",
               "Gap — unconfirmed supply")
  ))

p_gap <- ggplot(gap_long,
    aes(x = factor(year), y = pct, fill = source,
        text = paste0(source, " (", year, "): ", pct, "%"))) +
  geom_bar(stat = "identity", width = 0.75) +
  geom_vline(xintercept = 6.5, linetype = "dashed",
             color = "#A32D2D", linewidth = 0.9) +
  annotate("text", x = 6.6, y = 95,
           label = "SSAB EAF\nonline ~2030",
           hjust = 0, size = 3, color = "#A32D2D") +
  scale_fill_manual(values = c(
    "SSAB BF gas (phasing out)"   = "#A32D2D",
    "SSAB EAF waste heat (est.)"  = "#BA7517",
    "New industrial sources"      = "#1D9E75",
    "Own production (backup)"     = "#185FA5",
    "Gap — unconfirmed supply"    = "#B4B2A9"
  )) +
  labs(x = "Year", y = "Share of total heat demand (%)", fill = NULL) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom",
        panel.grid.major.x = element_blank(),
        axis.text.x = element_text(angle = 30, hjust = 1))

ggplotly(p_gap, tooltip = "text") |>
  layout(legend = list(orientation = "h", y = -0.25))

The grey bars represent the unconfirmed supply gap — the period where current confirmed contracts do not cover projected heat demand. This gap is largest around 2029–2031, the critical window between SSAB’s blast furnace shutdown and the ramp-up of EAF and new industrial partnerships.


North vs South comparison

Heat generation mix by region for each year 2019–2024. Shares are calculated from the Energimarknadsinspektionen production registry, aggregating all systems classified as northern or southern by city name.

Note

Reading the tabs: Industrial excess heat is structurally higher in northern systems throughout all years, while waste incineration dominates in southern systems. Northern systems have seen a modest but consistent increase in biofuel share since 2019.


Key events timeline

Show code
events <- data.frame(
  year  = c(1970, 2019, 2022, 2023, 2024, 2025, 2026, 2030, 2030),
  label = c(
    "SSAB/Luleå Energi collaboration begins",
    "Contract extended to 2030",
    "Thermal storage commissioned (Aronstorp)",
    "SSAB EAF waste heat MOU signed",
    "Uniper hydrogen heat agreement",
    "SSAB EAF groundbreaking (Sep 2025)",
    "LKAB Svartön industrial park (est.)",
    "SSAB blast furnace shutdown (target)",
    "SSAB EAF operations begin (target)"
  ),
  type  = c("Contract","Contract","Infrastructure","MOU",
            "Contract","Milestone","Planned","Critical","Transition"),
  y_pos = c(1,2,1,2,1,2,1,1,2)
)

color_map <- c(
  "Contract"       = "#185FA5",
  "Infrastructure" = "#1D9E75",
  "MOU"            = "#BA7517",
  "Milestone"      = "#534AB7",
  "Planned"        = "#888780",
  "Critical"       = "#A32D2D",
  "Transition"     = "#BA7517"
)

p_tl <- ggplot(events,
    aes(x = year, y = y_pos, color = type,
        text = paste0(year, ": ", label))) +
  geom_point(size = 5) +
  geom_segment(aes(xend = year, yend = 0), linewidth = 0.5, alpha = 0.4) +
  geom_hline(yintercept = 0, linewidth = 0.5, color = "#B4B2A9") +
  scale_color_manual(values = color_map) +
  scale_x_continuous(breaks = seq(1970, 2035, 5)) +
  scale_y_continuous(limits = c(-0.5, 3)) +
  labs(x = "Year", y = NULL, color = "Event type") +
  theme_minimal(base_size = 12) +
  theme(axis.text.y = element_blank(),
        panel.grid.major.y = element_blank(),
        panel.grid.minor.y = element_blank(),
        legend.position = "bottom",
        axis.text.x = element_text(angle = 30, hjust = 1))

ggplotly(p_tl, tooltip = "text") |>
  layout(legend = list(orientation = "h", y = -0.25))

Data sources

Dataset Source URL
Production per price area (2019–2024) Energimarknadsinspektionen ei.se technical data
Price per price area (2019–2024) Energimarknadsinspektionen ei.se economic data
2025 energy mix declaration Luleå Energi luleaenergi.se
SSAB/Luleå Energi 2023 MOU SSAB press release ssab.com
Contract extension 2019 ENERGInyheter energinyheter.se
Heat gap investigation NSD (Sep 2024) nsd.se

Report prepared as part of research on district heating governance and SSAB’s industrial transition in Luleå, Sweden. George Washington University / Luleå University of Technology, 2025.