Comparing L1C and L2A Sentinel-2 Products

Use L2A for anything compared across dates, and read both levels with the same scale and baseline-aware offset:

import numpy as np

def to_reflectance(dn: np.ndarray, baseline: str) -> np.ndarray:
    offset = -1000 if baseline >= "04.00" else 0            # applies to L1C and L2A
    out = (dn.astype("float32") + offset) / 10000
    out[dn == 0] = np.nan                                   # 0 is nodata in both levels
    return out

The two levels share a format and a grid, which is exactly why they are easy to confuse and costly to mix. This page belongs to atmospheric correction and surface reflectance in Satellite Processing Workflows & Index Pipelines.


What Differs Between the Levels

L1C and L2A at a glance L1C holds top-of-atmosphere reflectance for thirteen bands at their native resolutions. L2A holds surface reflectance for the same bands except the cirrus band, plus a scene classification layer, aerosol optical thickness and water vapour layers. Both share the same tiling, grid and quantification. Same grid, different contents L1C top-of-atmosphere reflectance 13 bands incl. B10 cirrus coarse cloud mask vectors includes path radiance, haze atmosphere still in the signal L2A surface reflectance 12 bands (no B10) SCL scene classification AOT and water vapour layers atmosphere estimated and removed Both use the same 100 km MGRS tiles and the same quantification of 10,000.

The scene classification layer is a practical reason to prefer L2A on its own. It is produced during correction, it is aligned with the reflectance bands, and it is the cloud and shadow mask most Sentinel-2 pipelines use — as covered in masking clouds with the Sentinel-2 SCL band.


Environment & Setup

Package Version pin Used for
pystac-client >=0.7 Finding matching L1C and L2A items
rasterio >=1.3.0 Reading bands from both levels
numpy >=1.23 Differences and statistics
pip install "pystac-client>=0.7" "rasterio>=1.3.0" "numpy>=1.23"

Complete Working Example

import numpy as np
import pystac_client
import rasterio

API = "https://earth-search.aws.element84.com/v1"


def matching_pair(bbox, date: str):
    """The same acquisition at both processing levels."""
    client = pystac_client.Client.open(API)
    l2a = next(client.search(collections=["sentinel-2-l2a"], bbox=bbox,
                             datetime=date, max_items=1).items())
    tile, when = l2a.properties["s2:mgrs_tile"], l2a.datetime.date().isoformat()
    l1c = next(it for it in client.search(collections=["sentinel-2-l1c"], bbox=bbox,
                                          datetime=when).items()
               if it.properties.get("s2:mgrs_tile") == tile)
    return l1c, l2a


def reflectance(item, asset: str, window) -> np.ndarray:
    baseline = str(item.properties.get("s2:processing_baseline", "00.00"))
    with rasterio.open(item.assets[asset].href) as src:
        dn = src.read(1, window=window)
    offset = -1000 if baseline >= "04.00" else 0
    out = (dn.astype("float32") + offset) / 10000
    out[dn == 0] = np.nan
    return out


def level_difference(bbox, date, window=((0, 1024), (0, 1024))) -> dict[str, float]:
    l1c, l2a = matching_pair(bbox, date)
    out = {}
    for asset in ("blue", "red", "nir"):
        toa = reflectance(l1c, asset, window)
        boa = reflectance(l2a, asset, window)
        out[asset] = float(np.nanmedian(toa - boa))
    return out                                  # positive: atmosphere added brightness

Pulling the same acquisition at both levels and differencing them over your own area turns an abstract distinction into a number. Over a typical humid scene the blue band’s top-of-atmosphere value exceeds surface reflectance by several hundredths — larger than many of the index differences analyses look for — while the near-infrared difference is small and can go either way.


The Processing Baseline Offset

Why newer products add 1000 Before baseline 04.00, reflectance was stored as DN equals reflectance times 10000, which cannot represent negative values produced by correction over dark targets. From 04.00, 1000 is added, so reflectance of minus 0.05 becomes 500 and zero becomes 1000. Decoding must subtract 1000 before dividing, or every value is 0.1 too high. Same reflectance, two encodings before 04.00 DN = ρ × 10000 ρ = 0.20 → 2000 ρ = -0.05 → cannot store 04.00 onward DN = ρ × 10000 + 1000 ρ = 0.20 → 3000 ρ = -0.05 → 500 Forgetting the offset on a newer scene makes every value exactly 0.1 too bright.

The offset applies to both levels from the same baseline, which is why the conversion function above keys on the baseline rather than the level. An archive spanning the change mixes scenes with and without it, and decoding them with one global rule produces a false step in every time series at the switch date. Reading the baseline per scene removes the problem entirely; the general principle is set out in converting int16 reflectance to float safely.


Never Mix Levels in One Analysis

The most expensive mistake with these products is using both in one time series — L2A where it exists and L1C to fill gaps, say. The two differ by an amount that varies with the atmosphere on each date, so the mixed series contains steps that look like change and are not. If L2A is missing for part of a period, it is better to leave the gap, or to produce your own correction for the whole period from L1C, than to splice the levels together.

Recording the processing level in every derived product’s tags, and asserting that inputs agree before combining them, makes the rule enforceable rather than aspirational. The tagging pattern is described in reading and writing GDAL tags and band descriptions.


Verification

Expected differences between levels Over land the blue band's top-of-atmosphere value typically exceeds surface reflectance by several hundredths, the red by one or two hundredths, and the near infrared differs by less than one hundredth in either direction. A difference much larger than this suggests a decoding error rather than an atmospheric effect. Median TOA − surface over land +0.05 +0.015 ±0.005 blue red NIR A difference of exactly 0.1 in every band is not atmosphere — it is the offset applied to one level only.
diff = level_difference([34.6, 0.25, 34.9, 0.55], "2026-06-14")
print(diff)
assert diff["blue"] > diff["red"] > abs(diff["nir"]) - 0.01, "unexpected spectral pattern"
assert all(abs(v - 0.1) > 0.02 for v in diff.values()), "looks like an offset mismatch, not atmosphere"

A difference of almost exactly 0.1 in every band is the fingerprint of the offset being applied to one level and not the other. Real atmospheric differences follow the spectral pattern — largest in blue, smallest in the infrared — and never produce a flat offset across all bands.


Common Errors

L2A values are all 0.1 too high

The baseline offset was not subtracted. Decode per scene using s2:processing_baseline.

The cirrus band is missing from L2A

B10 is not distributed at L2A because it carries no surface information. Use L1C’s B10 if cirrus detection needs it.

A time series steps at one date

Levels were mixed, or a baseline change was decoded with one global rule. Use one level and per-scene decoding.

SCL classes look shifted relative to the bands

SCL is at 20 m. Resample it to 10 m with nearest-neighbour, never bilinear.


Frequently Asked Questions

Q: When would I use L1C instead of L2A? When L2A is unavailable for the period, when you need your own atmospheric correction for consistency with another dataset, or for applications such as cloud detection research that model the atmosphere explicitly. For most land analysis, L2A is the right choice.

Q: What is the processing baseline offset? From baseline 04.00, both L1C and L2A add 1000 to every digital number so that negative reflectance can be stored. Converting back therefore subtracts 1000 before dividing by 10000 — equivalently, an offset of minus 0.1 in reflectance.

Q: Does L2A include a cloud mask? It includes a scene classification layer with classes for cloud, cloud shadow, cirrus, snow, water and vegetation, among others. It is a good first-pass mask, though thin cirrus and cloud edges often need additional handling.

Q: Are reprocessed archives consistent with recent acquisitions? Reprocessing campaigns bring older scenes up to a newer baseline, which is exactly what makes long time series consistent. Where an archive mixes original and reprocessed scenes, prefer the reprocessed versions throughout and record which baseline each scene used.

Q: Is L2A always available for every L1C scene? Very nearly for recent years, but not universally for the early archive, and occasionally a scene fails processing at L2A while its L1C exists. A search that falls back to L1C silently would mix levels, so treat a missing L2A as a gap in the series rather than as a cue to substitute, and let the observation count record it.