Load a WCS Coverage in PyQGIS
A Web Map Service returns pictures: rendered, coloured images meant for looking at. A Web Coverage Service returns data: the actual elevation values, temperatures or classification codes, in their native data type, ready for analysis. National mapping agencies publish elevation models through WCS, meteorological services publish gridded forecasts, and environmental agencies publish land cover and soil grids. When a script needs the numbers rather than the picture, WCS is the protocol to use.
This recipe belongs to Web Services and Remote Data in PyQGIS. It discovers the coverages a server offers, loads one as a raster layer, chooses CRS, format and time, downloads a subset into a local GeoTIFF for analysis, and explains when WMS, WCS or a cloud-optimised file is the better source.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series.
- The base URL of a WCS endpoint, for example
https://geo.example.org/wcs. Opening…?SERVICE=WCS&REQUEST=GetCapabilitiesin a browser shows the offered coverages. - Credentials, if the service is protected, stored as an authentication configuration as in loading a WFS layer.
Discover coverages
The capabilities document lists each coverage with an identifier and title. QGIS's network classes fetch it with QGIS's proxy and authentication settings.
import xml.etree.ElementTree as ET
from qgis.PyQt.QtCore import QUrl
from qgis.PyQt.QtNetwork import QNetworkRequest
from qgis.core import QgsBlockingNetworkRequest
BASE = "https://geo.example.org/wcs"
req = QNetworkRequest(QUrl(f"{BASE}?SERVICE=WCS&VERSION=2.0.1&REQUEST=GetCapabilities"))
blocking = QgsBlockingNetworkRequest()
if blocking.get(req) != QgsBlockingNetworkRequest.NoError:
raise RuntimeError(blocking.errorMessage())
root = ET.fromstring(bytes(blocking.reply().content()))
ns = {"wcs": "http://www.opengis.net/wcs/2.0", "ows": "http://www.opengis.net/ows/2.0"}
for cs in root.iter("{http://www.opengis.net/wcs/2.0}CoverageSummary"):
cid = cs.find("wcs:CoverageId", ns).text
title = cs.find("ows:Title", ns)
print(f"{cid:<36} {title.text if title is not None else ''}")
Breakdown: WCS 2.0 lists coverages as CoverageSummary elements with a CoverageId, the name the provider needs. Older servers speak WCS 1.0 or 1.1 with different element names; the QGIS provider handles all three, but a hand-written parser must match the version requested. QgsBlockingNetworkRequest is fine in scripts; inside plugins, use the asynchronous request classes to keep the interface responsive, as described in making HTTP requests with QgsNetworkAccessManager.
Load a coverage as a layer
The wcs provider takes a URI listing the server URL and coverage identifier, plus optional CRS, format and caching options.
from qgis.core import QgsDataSourceUri, QgsRasterLayer, QgsProject
uri = QgsDataSourceUri()
uri.setParam("url", BASE)
uri.setParam("identifier", "dem_1m")
uri.setParam("crs", "EPSG:25832")
uri.setParam("format", "image/tiff")
uri.setParam("cache", "PreferNetwork")
dem = QgsRasterLayer(str(uri.encodedUri(), "utf-8"), "DEM (WCS)", "wcs")
if not dem.isValid():
raise RuntimeError(dem.error().summary())
prov = dem.dataProvider()
print(dem.bandCount(), "band(s);", prov.dataType(1), "; NoData:",
prov.sourceNoDataValue(1) if prov.sourceHasNoDataValue(1) else None)
QgsProject.instance().addMapLayer(dem)
Breakdown: The WCS provider expects an encoded URI, which QgsDataSourceUri.encodedUri() produces; it returns bytes, hence the decode. identifier is the coverage id from the capabilities; crs asks for a CRS the server supports; format selects the transfer encoding — GeoTIFF preserves data type and NoData. Checking the band data type is a quick confirmation that real values arrived: a Float32 elevation band is data, a Byte RGB triplet would mean the server returned a rendered image. The layer requests data for the current view as you pan and zoom.
Choose CRS, format and time
Coverage servers can return data in several CRSs and formats, and many coverages have a time dimension — daily forecasts, monthly composites.
temporal = QgsDataSourceUri()
temporal.setParam("url", "https://met.example.org/wcs")
temporal.setParam("identifier", "temperature_2m")
temporal.setParam("crs", "EPSG:4326")
temporal.setParam("format", "image/tiff")
temporal.setParam("time", "2026-10-02T12:00:00Z")
t2m = QgsRasterLayer(str(temporal.encodedUri(), "utf-8"), "T2m 12:00", "wcs")
print(t2m.isValid(), t2m.dataProvider().dataType(1))
Breakdown: Requesting the coverage's native CRS avoids server-side resampling, which can blur values or change NoData handling. GeoTIFF is the format that reliably carries floats and NoData; PNG and JPEG are image formats and quantise values. For temporal coverages, the time parameter selects one slice in ISO 8601 form; loading several times means several layers, which can then be stepped through with the temporal controller. The capabilities and DescribeCoverage responses list supported CRSs, formats and time positions.
Download a subset for analysis
A WCS layer re-requests data whenever the view changes, which is wasteful for analysis and unreliable for long jobs. Downloading the area of interest once into a local GeoTIFF gives a stable input.
import processing
from qgis.core import QgsRectangle
area = QgsRectangle(565000, 5928000, 575000, 5938000) # in EPSG:25832
local = processing.run("gdal:cliprasterbyextent", {
"INPUT": dem,
"PROJWIN": f"{area.xMinimum()},{area.xMaximum()},{area.yMinimum()},{area.yMaximum()} [EPSG:25832]",
"NODATA": None,
"OPTIONS": "COMPRESS=DEFLATE|PREDICTOR=3|TILED=YES",
"DATA_TYPE": 0,
"OUTPUT": "/data/cache/dem_1m_subset.tif",
})["OUTPUT"]
check = QgsRasterLayer(local, "DEM subset")
print(check.width(), "x", check.height(), "pixels at", round(check.rasterUnitsPerPixelX(), 2), "m")
Breakdown: Clipping the WCS layer by extent makes GDAL request exactly that window and write it to disk, keeping the native data type with DATA_TYPE 0. The pixel size printed afterwards should match the coverage's native resolution; if it is coarser, the request was resampled and should be repeated with an explicit resolution. Large areas may exceed a server's maximum response size — download in tiles and merge, as in merging raster tiles into a mosaic.
Download large areas in tiles
Servers cap the size of a single response — often a few thousand pixels on a side. For a large area at native resolution, request a grid of tiles, each within the limit, and assemble them locally.
import math
from pathlib import Path
TILE_PX = 2000
px = dem.rasterUnitsPerPixelX()
step = TILE_PX * px
tiles = []
for i in range(math.ceil(area.width() / step)):
for j in range(math.ceil(area.height() / step)):
x0 = area.xMinimum() + i * step
y0 = area.yMinimum() + j * step
x1, y1 = min(x0 + step, area.xMaximum()), min(y0 + step, area.yMaximum())
out = f"/data/cache/dem_tiles/t_{i:03d}_{j:03d}.tif"
if not Path(out).exists():
processing.run("gdal:cliprasterbyextent", {
"INPUT": dem, "PROJWIN": f"{x0},{x1},{y0},{y1} [EPSG:25832]",
"DATA_TYPE": 0, "OPTIONS": "COMPRESS=DEFLATE|TILED=YES", "OUTPUT": out})
tiles.append(out)
vrt = processing.run("gdal:buildvirtualraster", {
"INPUT": tiles, "SEPARATE": False, "OUTPUT": "/data/cache/dem_area.vrt"})["OUTPUT"]
Breakdown: Tiles of 2,000 pixels at native resolution stay under typical limits; check the server's documentation or its MaxWidth/MaxHeight hints. Skipping tiles that already exist makes the loop restartable after a network failure — rerun it and only missing tiles are fetched. Starting tiles at the area's origin and stepping by whole pixels keeps them aligned with each other. A VRT assembles the result without copying, as in building a virtual raster; translate it to one GeoTIFF if a single file is needed.
Record where the data came from
Downloaded coverages lose their link to the server. Writing the source, coverage id and request time into the file's metadata keeps the provenance.
from osgeo import gdal
from datetime import datetime, timezone
ds = gdal.Open(local, gdal.GA_Update)
ds.SetMetadata({"SOURCE_SERVICE": BASE, "COVERAGE_ID": "dem_1m",
"RETRIEVED_UTC": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")})
ds = None
print(gdal.Info(local).splitlines()[0])
Breakdown: GeoTIFF metadata items travel with the file and appear in QGIS's layer properties and in gdalinfo. Recording the retrieval time matters most for coverages that change — forecasts, near-real-time land cover — where "the DEM from the WCS" is ambiguous without a date. The same habit is shown for vector services in loading an OGC API Features layer.
WCS, WMS or a COG?
Use WMS when you only need to see the data. Use WCS when you need values and the provider publishes through it. Increasingly, providers publish raster data as Cloud Optimized GeoTIFFs on object storage instead — simpler for them, faster for you, and readable with the same GDAL machinery; see reading a Cloud Optimized GeoTIFF. Where both WCS and COG are offered, COG is usually the more reliable choice for scripted downloads.
QGIS version compatibility
The wcs provider supports WCS 1.0, 1.1 and 2.0 servers on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. URI parameters such as identifier, crs, format, time and cache are stable across these releases. On QGIS 4, QgsBlockingNetworkRequest.NoError is QgsBlockingNetworkRequest.ErrorCode.NoError.
Troubleshooting
- The layer is invalid. The identifier is the title rather than the CoverageId, or the server only speaks an older version; try
version=1.0.0in the URI. - Values look like colours (0–255). The server returned a rendered image; request
image/tiffand check the coverage is data, not imagery. - Downloads fail for large areas. The server limits response size; download in tiles.
- NoData appears as a real value. The format dropped NoData; use GeoTIFF and set NoData explicitly if needed.
Conclusion
Discover coverage ids from GetCapabilities, load them with the wcs provider in their native CRS and GeoTIFF format, select a time slice for temporal coverages, download the area of interest once into a compressed local GeoTIFF for analysis, record provenance in its metadata, and prefer COG sources where providers offer them.
Frequently Asked Questions
Can I compute slope directly on a WCS layer? Yes, but download first: analysis on a live service is slower and can fail mid-run.
Does QGIS cache WCS responses?
Yes, through its network cache, controlled by the cache URI parameter and QGIS's cache settings.
What is DescribeCoverage? A request returning one coverage's details — grid, CRSs, range type — useful for choosing resolution and format.
Is WCS being replaced? OGC API – Coverages is its successor; adoption is still early, and WCS remains widely deployed.