A Practical Guide to Web Coverage Service for Weather Data

Written by Steve Gifford

September 18, 2025

The Web Coverage Service (WCS) is an OGC standard for retrieving time-series gridded data sets. It has a strong weather and climate focus, and that’s mostly who uses it. I say that as if it’s a well-known standard… it’s really not. Most weather companies roll their own solutions.

We had a request for WCS before we got around to rolling our own service, so we made it work — and now? Well, we kind of like it. That’s right, Web Coverage Service for weather data has grown on us.

WCS Metadata

There’s a huge spec for Web Coverage Service, and most of it isn’t terribly necessary for simple use cases. It is there, though, and we support a lot of it. What is useful is a few examples you can use to build your own queries and then forget about the details.

Metadata takes up a lot (a lot!) of the effort. This is similar to WMS and WMTS if you’ve dealt with those. You ask for their contents, and you receive a dizzyingly long XML file in return. WCS course corrects for this… a bit. This is kind of a problem with OGC standards.

But here’s the trick with these standards. Most users don’t hit the “GetCapabilities” endpoint very often. Usually just once to hardwire the URLs they want. Let’s examine the GetCapabilities return. Well, er, let’s look at a snippet of it, because it’s around 10MB of XML. Thankfully, it compresses.

We start with ye olde XML boilerplate. Tell me about the service! And all the specs it uses, because XML is self-documenting!

<wcs:Capabilities xmlns="http://www.opengis.net/ows/2.0" xmlns:ows="http://www.opengis.net/ows/2.0" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:wcs="http://www.opengis.net/wcs/2.0" xmlns:metocean="http://def.wmo.int/metce/2013/metocean" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:crs="http://www.opengis.net/wcs/service-extension/crs/1.0" xmlns:wdw="http://wetdogweather.com" version="2.0.0" xsi:schemaLocation="http://schemas.opengis.net/wcs/2.0 http://schemas.opengis.net/wcs/2.0/wcsAll.xsd">
    <ServiceIdentification>
        <Title>Example Web Coverage Service</Title>
        <Abstract>This WCS provide geospatial weather data</Abstract>
        <ServiceType>OGC WCS</ServiceType>
        <ServiceTypeVersion>2.0.0</ServiceTypeVersion>
    </ServiceIdentification>

You can click on one of those definitions, and it’ll… oh, the websites went down years ago? Huh. I can’t imagine how JSON outcompeted XML. Let’s move on to the parts people care about. Model data!

Service Metadata in WCS

Let’s look at the High Resolution Rapid Refresh (HRRR). The CONUS region is the biggest one, and we use it for lots of different things. If you want to query it, you can see what’s available in two forms. The second, which we’re showing first, is a pretty reasonable list of all the time slices we have available. I’ve cut that down from what you’d typically see, because it’s enormous.

The first is a reasonable list of time slices:

<metocean:CoverageCollectionSummary>
  <metocean:CoverageCollectionId>hrrr-conus-sfcf</metocean:CoverageCollectionId>
    <gml:Name>hrrr-conus-sfcf</gml:Name>
    <gml:boundedBy>
      <gml:Envelope gml:srsName="PROJ4:+proj=lcc +lat_0=38.5 +lon_0=262.5 +lat_1=38.5 +lat_2=38.5 +x_0=0 +y_0=0 +R=6371229 +units=m +no_defs +type=crs" axisLabels="x y" uomLabels="m m">
        <gml:lowerCorner>-2699020.142521929 -1588806.152556665</gml:lowerCorner>
        <gml:upperCorner>2697979.857478071 1588193.847443335</gml:upperCorner>
      </gml:Envelope>
    </gml:boundedBy>
    <metocean:ReferenceTimeList>
     <metocean:ReferenceTime>
      <gml:ReferenceTime>2025-09-17T16:00:00Z</gml:ReferenceTime>
      <gml:ReferenceTime>2025-09-17T18:00:00Z</gml:ReferenceTime>
      <gml:ReferenceTime>2025-09-17T19:00:00Z</gml:ReferenceTime>
      <gml:ReferenceTime>2025-09-17T01:00:00Z</gml:ReferenceTime>
      <gml:ReferenceTime>2025-09-17T22:00:00Z</gml:ReferenceTime>
     </metocean:ReferenceTime>
    </metocean:ReferenceTimeList>
  <metocean:referenceTimeList/>
</metocean:CoverageCollectionSummary>

The first variant in the return, which I’m showing second, is more wordy, but someone needed it, so:

         <wcs:CoverageSummary>
            <wcs:CoverageId>hrrr-conus-sfcf_2025-09-17T01:00:00Z</wcs:CoverageId>
            <wcs:CoverageSubType>hrrr-conus-sfcf</wcs:CoverageSubType>
            <wdw:Complete>True</wdw:Complete>
        </wcs:CoverageSummary>
        <wcs:CoverageSummary>
            <wcs:CoverageId>hrrr-conus-sfcf_2025-09-17T22:00:00Z</wcs:CoverageId>
            <wcs:CoverageSubType>hrrr-conus-sfcf</wcs:CoverageSubType>
            <wdw:Complete>True</wdw:Complete>
        </wcs:CoverageSummary>
        <wcs:CoverageSummary>
            <wcs:CoverageId>hrrr-conus-sfcf_latest</wcs:CoverageId>
            <wcs:CoverageSubType>hrrr-conus-sfcf</wcs:CoverageSubType>
            <wdw:Complete>True</wdw:Complete>

It does have a really interesting entry here: hrrr-conus-sfcf_latest. Keep that one in mind, because it means you’ll never have to look at the GetCapabilities return again.

Weather Model Metadata

If you’re digging through the entrails of GetCapabilities, there’s no information about what variables you can query. You can determine this with DescribeCoverage.

Let’s hit the endpoint for one of those HRRR conus slices:

DescribeCoverage?CoverageId=hrrr-conus-sfcf_2025-09-17T01:00:00Z

All we’re doing here is saying “what you got for the hrrr conus sfcf 1AM run?” This is a whole model run, by the way. It’s just how forecasters think about forecasting.

What you’ll get back is… daunting… but very complete. Let’s say we’re interested in wind_direction and we search for that.

<metocean:dataMaskReference fieldName="wind_direction" xlink:href="#maskId_hrrr-conus-sfcf_wind_direction">maskId_hrrr-conus-sfcf_wind_direction</metocean:dataMaskReference>

Yes, XML can reference itself. Brilliant. That’s just like HTML! That was a great design choice! So awesome! <distant screaming>

What this does tell us is that wind_direction is a variable in this HRRR data set, and if we poke around further, we can see more.

The data mask is…. I’m not sure how to put it into words. It’s best to take it at face value.

<metocean:dataMask maskName="wind_direction" gml:id="maskId_hrrr-conus-sfcf_wind_direction">
   <gml:domainSet>
    <gml:axisLabels>z t</gml:axisLabels>
    <gml:posList>
10 0 80 0 
10 3600 80 3600 
10 7200 80 7200 
10 10800 80 10800 
10 14400 80 14400 
    </gml:posList>
  </gml:domainSet>

That describes the elevation (z) and forecast offset (t) values you can find for the wind_direction variable in hrrr-conus-sfcf. I cut out most of the noise. You’re welcome.

Armed with this information, we can actually get to the good part!

WCS Data Queries: GetCoverage

We can query any of the data sets anywhere they’re defined. So let’s look at the most recent wind_speed at a certain point at forecast hour 1.

GetCoverage?
SUBSET=lat(40)&SUBSET=long(-98)&FORMAT=JSON&CRS=WGS:84&RANGESUBSET=wind_direction&SUBSET=z(80)&CoverageId=hrrr-conus-sfcf_latest&SUBSET=f(PT1H)

That makes more sense in Postman’s grid form.

What we get back is something like this:

{
    "coverageData": [
        {
            "parameterName": "wind_direction",
            "units": "m/s",
            "modelName": "hrrr-conus-natf",
            "modelRun": "2025-09-17T22:00:00Z",
            "modelForecast": "PT1H",
            "validity": "2025-09-17T23:00:00Z",
            "level": "80m AGL",
            "interval": "N_NONE",
            "values": [
                [
                    254.12814331054688
                ]
            ],
            "coordinates": [
                [
                    {
                        "lat": 40.0,
                        "lon": -98.0
                    }
                ]
            ]
        }
    ]
}

It’s JSON! So much easier to parse, particularly in JavaScript, which is probably where you’re using it.

The returns are self-explanatory, with units and everything.

Multi-Queries in WCS

Sometimes, you need to make multiple queries at once. For example, you’d like to hit every forecast hour in a given forecast to show a graph of wind_direction over time to your users.

Simple, you actually specify less:

GetCoverage?SUBSET=lat(40)&SUBSET=long(-98)&FORMAT=JSON&CRS=WGS:84&RANGESUBSET=wind_direction&SUBSET=z(80)&CoverageId=hrrr-conus-sfcf_latest

All we did there was turn off the forecast offset, and you get back all the forecast hours available for that model run.

You can do the same with elevation… but it’ll be slow. We’ve built special infrastructure to enable fast execution of full forecast queries. If we need to do it for elevation, we can, but at a certain point, we should simply provide you with access to the data files, which we can do using Labrador.

One detail to mention. Our old friend “latest came back. We’re querying for hrrr-conus-sfcf_latest, and so we don’t have to figure out what model runs are in. We can just let the service figure that out and parse the returns. That cuts out a lot of complexity.

This is one of the places where Web Coverage Service for weather data really shines — you can query entire runs or vertical levels with one call, instead of making dozens of individual calls.

Why We Use Web Coverage Service for Weather Data

We’ve barely scratched the surface here. You can query rectangles and get NetCDF files back, for instance. You can query several variables at once. You can specify variable ranges and let the server figure it out. It’s very flexible.

Use the GetCapabilities and DescribeCoverage calls to figure out the structure of your GetCoverage call… and then never look at them again. Variable names between runs are consistent, as are levels. You can then proceed directly to a GetCoverage call once you’ve finalized the details.

This isn’t really a forecast server in the sense of one of the many free or paid options available. It doesn’t tell you the temperature at a given point; instead, it tells you what’s in the model you specify. In that sense, it’s like the Costco or Sam’s Club version of a forecast endpoint. If you know exactly what you want and how to go about it, it’s pretty good. If you’re looking for the server to decide what data is best, not so much.

But this works well for our customers, and we’re excited to see what they do with it. And that’s why we’ve come to appreciate Web Coverage Service for weather data.