Why air_quality AQI Values Differ by Country — and How to Build a Display Layer That Handles All of Them

The AQI field isn’t one thing

Pull the air_quality object from a WeatherAPI current or forecast response and you’ll see several index values alongside each other: us-epa-index, gb-defra-index, and the raw pollutant concentrations — pm2_5, pm10, o3, no2, so2, co. Most developers grab one index value, display it, and move on. The problem is that these indices use different breakpoints, different pollutant weightings, and different category labels. Showing the US EPA index to a UK user — or the DEFRA index to a US user — gives them a number that’s technically correct by one standard and meaningfully misleading by the one they’re familiar with.

This isn’t an API bug. It reflects a real disagreement between regulatory agencies about how to communicate air quality risk to the public. The EPA’s AQI runs 0–500, with “Good” covering 0–50. DEFRA’s index runs 1–10 with a completely different verbal descriptor system. Same atmosphere, different rulers.

What the breakpoints actually mean

The US EPA AQI is a piecewise linear function. Each pollutant gets its own sub-index computed from concentration, and the reported AQI is whichever sub-index is highest. An AQI of 101 means at least one pollutant — usually PM2.5 or ozone — has crossed into “Unhealthy for Sensitive Groups”, even if everything else is clean. The EPA publishes the breakpoint table directly; it’s a well-specified standard, and the NWS uses the same methodology when issuing air quality alerts.

DEFRA’s Daily Air Quality Index works differently. It’s also pollutant-driven — the worst pollutant determines the overall band — but the scale runs 1–10 grouped into four bands: Low (1–3), Moderate (4–6), High (7–9), and Very High (10). DEFRA’s PM2.5 breakpoints are not the same as EPA’s. A PM2.5 concentration of 35 µg/m³ lands at roughly EPA AQI 100 (the Moderate boundary) and also at DEFRA band 5 (Moderate) — close enough in category that you might not notice, but the numbers themselves are incomparable. You cannot display both on the same scale.

The raw concentration fields (pm2_5, pm10, etc.) are in µg/m³ throughout, which makes them the most portable thing in the response. If you need to support multiple regional standards, computing the sub-index yourself from raw concentrations is more reliable than trying to translate one national index into another.

A locale-aware display layer in practice

The minimal approach: detect the user’s country from the requested location and serve the appropriate index. WeatherAPI’s location object gives you country as a string — not an ISO code, which is slightly annoying, but parseable enough for a lookup table:

function getDisplayIndex(airQuality, country) {
  if (country === "United Kingdom") {
    return {
      index: airQuality["gb-defra-index"],
      scale: "DEFRA",
      label: defraLabel(airQuality["gb-defra-index"])
    };
  }
  // Default to US EPA for US and most other countries
  return {
    index: airQuality["us-epa-index"],
    scale: "US AQI",
    label: epaLabel(airQuality["us-epa-index"])
  };
}

The label functions are just lookup tables — EPA has six categories, DEFRA has four bands. Hard-code them. Don’t interpolate them. These are regulatory definitions, not continuous gradients.

Always show the dominant pollutant alongside the index. You derive it by computing each pollutant’s fraction of its respective threshold and finding the highest. PM2.5 wins most often in urban areas; ozone tends to dominate on summer afternoons at lower latitudes. Users understand “elevated PM2.5” better than they understand “AQI 142” on its own — and for any health or advisory use case, that pollutant attribution matters legally in some jurisdictions.

Where the raw concentrations are more trustworthy than the index

Both national indices hide something: they take the max sub-index across pollutants, so you lose information about whether a reading is driven by one very elevated pollutant or several moderately elevated ones. An AQI of 100 from ozone alone is a different situation from AQI 100 split across PM2.5, NO2, and SO2 — the health implications differ, and so do the behavioral recommendations (indoors vs. outdoors, whether a mask helps, whether to cancel outdoor exercise).

If you’re building something that needs to give users genuinely actionable guidance rather than a color-coded dot, store and display the raw µg/m³ values too, and let your thresholds be pollutant-specific. The WHO publishes guideline values for PM2.5 (5 µg/m³ annual mean, 15 µg/m³ 24-hour mean), PM10, NO2, and ozone that are stricter than either EPA or DEFRA thresholds — some teams building health-adjacent apps have started using WHO values as a separate alert tier sitting above the national index.

One thing worth being direct about: the air quality data in our API, like most commercial weather APIs, comes from model estimates blended with monitoring station data — not a dense local sensor network. In areas with sparse monitoring coverage the concentrations reflect model output more than direct measurement. Urban micro-environments (near a motorway, downwind of an industrial site) can diverge substantially from what a regional model predicts. The numbers are not hyperlocal.

Handling the fields that go missing

Not every location returns a full air quality object. When the aqi parameter is set to yes but the response comes back with null concentrations, it usually means the location is outside the model’s coverage grid for that pollutant — more common in remote areas and parts of the world where monitoring data used to constrain the model is thin. A null us-epa-index is not the same as an index of 1, and rendering it as “Good” when the value is simply absent is the kind of silent error that compounds over time.

Check for null explicitly before rendering any AQI badge. If the concentration fields are present but the composite index is null, you can compute a rough EPA AQI from the PM2.5 breakpoints — it’s a few lines of math and covers the most common case. If the concentrations themselves are missing, render nothing rather than a stale fallback.

A note on the DEFRA index in forecast responses

The gb-defra-index field in forecast hours isn’t always populated even when the current conditions object has it. DEFRA index values for future hours depend on forecast pollutant transport modelling, which some data providers don’t run or don’t expose — ours included in some cases. If you need forward-looking UK air quality, the raw pm2_5 and o3 forecast values are more consistently present and you can compute the DEFRA band from those directly. Slightly more work, but more robust than relying on the pre-computed field being there.

If you’re already displaying a single index number from the air_quality object, the first concrete thing worth doing is pulling the raw concentration fields alongside it and checking which pollutant is actually driving the reading. Knowing whether you’re looking at a PM2.5 problem or an ozone problem is the difference between a number and advice.

Scroll to Top