immigration and migration inflows to Europe over the past 10 years trend percentage change
Over the past decade, total immigration inflows to the European Union (EU27) have risen sharply, climbing from around 3.19 million in 2014 to 5.68 million in 2024—an overall increase of 78.3%1. The trajectory was uneven, driven by policy shifts, the COVID-19 pandemic, and the humanitarian response to Russia’s invasion of Ukraine.
Annual trend and percentage change
Year-over-year growth rates fluctuated considerably1:
- 2015: +28.5% (spike to 4.10 million)
- 2016: –8.8%
- 2017–2019: modest, steady growth of +0.8% to +6.9%
- 2020: –22.0% (drop to 3.28 million during the pandemic)
- 2021: +26.0% (rebound to 4.13 million)
- 2022: +69.3% (surge to 6.99 million, the highest level in the series, driven largely by temporary protection for Ukrainians)
- 2023: –14.1% (decline to 6.01 million)
- 2024: –5.4% (further easing to 5.68 million)
The net absolute gain between 2014 and 2024 was approximately 2.50 million additional immigrants per year1.
Composition of inflows (intra-EU vs. extra-EU)
A breakdown by origin is available from 2020 onward21. Extra-EU migration consistently accounted for the larger share:
- 2020: 1.98 million extra-EU vs. 1.26 million intra-EU
- 2021: 2.62 million extra-EU vs. 1.47 million intra-EU
- 2022: 5.36 million extra-EU vs. 1.57 million intra-EU
- 2023: 4.40 million extra-EU vs. 1.54 million intra-EU
- 2024: 4.15 million extra-EU vs. 1.47 million intra-EU
Intra-EU mobility remained relatively stable (roughly 1.3–1.6 million annually), whereas extra-EU inflows were the primary driver of volatility, especially the 2022 spike and subsequent partial retreat1.
Visual summary
The trend line and the 2020–2024 origin breakdown are illustrated in the charts generated during the analysis1.
- Code Run 1Dataset: migr_imm12prv
Images
eu_immigration_trend.svgeu_immigration_breakdown.svgDataset filters for
migr_imm12prvFilter Value(s) freq A geo EU27_2020 unit NR sinceTimePeriod 2014 Program output
Total Immigration to EU27 (annual) time 2014 3187841 2015 4097244 2016 3735536 2017 3766413 2018 3936055 2019 4206293 2020 3279673 2021 4132213 2022 6997284 2023 6011360 2024 5684984 Year-over-year % change time 2015 28.5 2016 -8.8 2017 0.8 2018 4.5 2019 6.9 2020 -22.0 2021 26.0 2022 69.3 2023 -14.1 2024 -5.4 Overall change 2014 to 2024: 78.3% Absolute change: 2,497,143 Charts saved: eu_immigration_trend.svg and eu_immigration_breakdown.svgCode
import pandas as pd import matplotlib.pyplot as plt # Read data df = pd.read_csv(DATASET_FILE) # Pivot to get years as rows total = df[df['partner'] == 'TOTAL'][['time', 'value']].sort_values('time').set_index('time')['value'] intra = df[df['partner'] == 'EU27_2020_FOR'][['time', 'value']].sort_values('time').set_index('time')['value'] extra = df[df['partner'] == 'NEU27_2020_FOR'][['time', 'value']].sort_values('time').set_index('time')['value'] # Calculate YoY % change for total pct_change = total.pct_change() * 100 # Calculate overall change 2014 vs 2024 overall_change = (total.loc[2024] - total.loc[2014]) / total.loc[2014] * 100 print("Total Immigration to EU27 (annual)") print(total.to_string()) print("\nYear-over-year % change") print(pct_change.dropna().round(1).to_string()) print(f"\nOverall change 2014 to 2024: {overall_change:.1f}%") print(f"Absolute change: {total.loc[2024] - total.loc[2014]:,.0f}") # Create chart fig, ax1 = plt.subplots(figsize=(10, 6)) # Plot total immigration line years_total = total.index.astype(int) ax1.plot(years_total, total.values/1e6, marker='o', color='#1f77b4', linewidth=2, label='Total immigration') ax1.set_xlabel('Year') ax1.set_ylabel('Number of immigrants (millions)', color='#1f77b4') ax1.tick_params(axis='y', labelcolor='#1f77b4') ax1.set_ylim(0, 8) # Add annotation for 2022 spike ax1.annotate('2022 spike:\n+69.3% YoY', xy=(2022, total.loc[2022]/1e6), xytext=(2020, 7.5), arrowprops=dict(arrowstyle='->', color='red'), fontsize=10, color='red') # Add secondary axis for percentage change? Or just a bar chart for the components # Create a second plot for the breakdown fig2, ax2 = plt.subplots(figsize=(10, 5)) breakdown_years = intra.index.astype(int) width = 0.35 ax2.bar(breakdown_years - width/2, intra.values/1e6, width, label='Intra-EU (from other EU)', color='#2ca02c') ax2.bar(breakdown_years + width/2, extra.values/1e6, width, label='Extra-EU (from outside EU)', color='#d62728') ax2.set_xlabel('Year') ax2.set_ylabel('Number of immigrants (millions)') ax2.set_title('Immigration to EU27 by Origin (2020-2024)') ax2.legend() ax2.set_ylim(0, 6) # Add values on top of bars for i, (x, v) in enumerate(zip(breakdown_years, intra.values/1e6)): ax2.text(x - width/2, v + 0.1, f'{v:.1f}M', ha='center', va='bottom', fontsize=8) for i, (x, v) in enumerate(zip(breakdown_years, extra.values/1e6)): ax2.text(x + width/2, v + 0.1, f'{v:.1f}M', ha='center', va='bottom', fontsize=8) fig.savefig('eu_immigration_trend.svg', format='svg') fig2.savefig('eu_immigration_breakdown.svg', format='svg') print("\nCharts saved: eu_immigration_trend.svg and eu_immigration_breakdown.svg") - Eurostat dataset: Immigration by broad group of country of previous residence