How to check sitemap lastmod
A lastmod value must be W3C Datetime, and the published schema narrows that to xsd:date or xsd:dateTime. Check one value with python lastmod-check.py 2026-09-11, then check the spread across the file: a single date repeated on every URL is valid and tells a crawler nothing.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
Run this after any change to the sitemap generator and on every release that moves the build to a new runtime. Two defects hide here. A value in a format no crawler accepts, which drops the field silently because the element is optional. And a value that is the build timestamp, which marks all 84 pages as changed today when two of them changed.
The second one costs more. A crawler that learns lastmod is wrong on a site stops using it there, and the field is the only cheap signal in a sitemap for which pages are worth refetching. Both defects pass every parser, so the check is on the values, not on the file.
Prerequisites
- Python 3.8 or later, and
pip install lxmlfor the schema check in steps 3 and 5. - Node 18 or later for step 4. No packages.
- W3C Datetime for the six granularities the protocol points at, and sitemap.xsd for the narrower type the schema actually declares.
- Save this as
lastmod-audit.py. It reads the values out of a sitemap or an index and describes their spread.
# python lastmod-audit.py <sitemap.xml|.xml.gz>
import sys, gzip, collections
import xml.etree.ElementTree as ET
NS = "{http://www.sitemaps.org/schemas/sitemap/0.9}"
path = sys.argv[1]
raw = gzip.open(path, "rb").read() if path.endswith(".gz") else open(path, "rb").read()
root = ET.fromstring(raw)
entries = root.findall(NS + "url") or root.findall(NS + "sitemap")
vals = [(e.findtext(NS + "lastmod") or "").strip() for e in entries]
have = [v for v in vals if v]
counts = collections.Counter(have)
print("entries: %d, with lastmod: %d, without: %d" % (len(vals), len(have), len(vals) - len(have)))
print("distinct lastmod values: %d" % len(counts))
if have:
print("oldest: %s newest: %s" % (min(have), max(have)))
top, n = counts.most_common(1)[0]
print("most common: %s on %d of %d entries (%.0f%%)" % (top, n, len(have), 100.0 * n / len(have)))
if n == len(have) and len(have) > 1:
print("WARN every entry carries the same lastmod, so the field carries no signal")
- Save this as
lastmod-check.py. It validates values against both definitions and prints them side by side.
# python lastmod-check.py <value> [<value> ...]
import sys, re
from lxml import etree
# tLastmod copied from https://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd
XSD = b"""<?xml version="1.0"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="lastmod" type="tLastmod"/>
<xsd:simpleType name="tLastmod">
<xsd:union>
<xsd:simpleType><xsd:restriction base="xsd:date"/></xsd:simpleType>
<xsd:simpleType><xsd:restriction base="xsd:dateTime"/></xsd:simpleType>
</xsd:union>
</xsd:simpleType>
</xsd:schema>"""
schema = etree.XMLSchema(etree.fromstring(XSD))
# The six granularities of https://www.w3.org/TR/NOTE-datetime
W3C = re.compile(r"^\d{4}(-\d{2}(-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2}))?)?)?$")
print("%-32s %-6s %-6s" % ("value", "W3C", "XSD"))
for v in sys.argv[1:]:
doc = etree.fromstring(("<lastmod>%s</lastmod>" % v).encode())
xsd_ok = schema.validate(doc)
print("%-32s %-6s %-6s" % (v, "yes" if W3C.match(v) else "NO", "yes" if xsd_ok else "NO"))
Steps
- Step 1.
Describe the spread of values in a real sitemap that keeps them per page.
python lastmod-audit.py mdn-en-us.xml.gzentries: 14759, with lastmod: 14622, without: 137 distinct lastmod values: 753 oldest: 2023-02-18 newest: 2026-09-11 most common: 2026-08-21 on 688 of 14622 entries (5%)753 distinct values across three and a half years is what a per page date looks like. 137 entries carry none, which is allowed. No single value holds more than 5% of the file, so the field separates pages instead of grouping them.
- Step 2.
Run the same audit on a sitemap written by a static build.
python lastmod-audit.py out/sitemap-check.xmlentries: 84, with lastmod: 84, without: 0 distinct lastmod values: 1 oldest: 2026-09-11 newest: 2026-09-11 most common: 2026-09-11 on 84 of 84 entries (100%) WARN every entry carries the same lastmod, so the field carries no signalThis is the sitemap of the site you are reading. Every value is valid and the field is the build date, not a page date. Compare what matters: 753 distinct values out of 14,622 against 1 out of 84.
- Step 3.
Validate candidate values against both definitions at once.
python lastmod-check.py 2026-09-11 2026-09-11T17:33:30+02:00 2026-09 2026 2026-09-11T17:33Z 2026-09-11T17:33:30 2026-9-1 09/11/2026 2026-02-30value W3C XSD 2026-09-11 yes yes 2026-09-11T17:33:30+02:00 yes yes 2026-09 yes NO 2026 yes NO 2026-09-11T17:33Z yes NO 2026-09-11T17:33:30 NO yes 2026-9-1 NO NO 09/11/2026 NO NO 2026-02-30 yes NOThe two columns disagree four times.
2026-09and2026are granularities 1 and 2 of W3C Datetime, and the schema type is a union ofxsd:dateandxsd:dateTime, neither of which accepts them.2026-09-11T17:33Zis granularity 4, without seconds, whichxsd:dateTimerequires.2026-09-11T17:33:30has no zone designator, so W3C refuses it and the schema does not.2026-02-30is a well-formed date that does not exist, and only the schema checks that. Ship values that pass both columns. - Step 4.
Put the same values through the date parser a generator is most likely to use.
node -e "for (const v of ['2026-09-11','2026-09','2026','2026-9-1','09/11/2026','2026-02-30','2026-09-11T17:33Z']) { const d = new Date(v); console.log(v.padEnd(20), isNaN(d) ? 'Invalid Date' : d.toISOString()); }"2026-09-11 2026-09-11T00:00:00.000Z 2026-09 2026-09-01T00:00:00.000Z 2026 2026-01-01T00:00:00.000Z 2026-9-1 2026-08-31T21:00:00.000Z 09/11/2026 2026-09-10T21:00:00.000Z 2026-02-30 2026-03-02T00:00:00.000Z 2026-09-11T17:33Z 2026-09-11T17:33:00.000ZEvery one of them parses. Nothing here is an
Invalid Date, so a generator that guards withisNaN(new Date(v))lets all seven through.2026-9-1is read in local time and lands on 31 August, because this machine runs at Europe/Kiev, UTC+3.2026-02-30rolls over into March. A parser that accepts a value is not a validator for it. - Step 5.
Validate the values of a file built with the mistakes this produces.
python lastmod-check.py 2026-09-11 2026-9-1 2026-09-11T17:33Z "Fri, 11 Sep 2026 01:34:47 GMT" "2026-09-11 17:33:30"value W3C XSD 2026-09-11 yes yes 2026-9-1 NO NO 2026-09-11T17:33Z yes NO Fri, 11 Sep 2026 01:34:47 GMT NO NO 2026-09-11 17:33:30 NO NOOne of five is usable. The HTTP date came from a
last-modifiedresponse header pasted straight through, and the last value has a space where theTbelongs, which is what most database drivers return for a timestamp column.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| yes yes in both columns | The value is accepted by the note and by the schema | Ship it |
| yes for W3C and NO for XSD | A year, a month, a time without seconds, or a date that does not exist | Write the full YYYY-MM-DD, or add seconds |
| NO for W3C and yes for XSD | A timestamp with no zone designator | Append Z or the offset |
| NO NO | A locale date, an HTTP date, or a space instead of T | Format from a date object, never from a string |
| distinct lastmod values: 1 | The build stamped one date on the whole file | Take the date from the content, not from the run |
| without: N | N entries omit the element | Allowed. A missing value beats a wrong one |
Common mistakes
What to check next
- How to validate sitemap xml: the rules that apply to the rest of the file, none of which look at dates.
- How to check sitemap index file: an index carries
lastmodon each child, with the same format rules and the same build timestamp trap. - How to check last-modified header: the response header this value is often confused with, and the format it really uses.
- How to check sitemap and robots txt: the declaration that decides which file these dates are read from.
- How to check robots.txt: the first file a crawler reads on the host.
FAQ
What date format does Google want in a sitemap?
W3C Datetime. 2026-09-11 and 2026-09-11T17:33:30+02:00 are both accepted and both validate against the schema type. Google uses the value when it is consistently accurate against the page, and stops using it on a site where it is not.
Is lastmod required?
No. It is optional on a url entry and on a sitemap entry in an index. Step 1 shows 137 of 14,759 URLs on a live site with no value. Leaving it out is better than publishing the run date.
Should lastmod include a time?
Only when the time is real. A date alone is valid and cannot be wrong by hours. Adding a time means adding a zone designator, because the schema accepts a bare timestamp and the W3C note does not.
Why does my valid date fail a validator?
The two definitions the protocol points at do not overlap exactly. A year, a month, or a time without seconds passes the W3C note and fails the schema, as step 3 shows. Use the full date, with seconds and a zone when a time is present.
Verified
Verified by Maks Vernypython 3.13.1lxml 6.0.2node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
- Checker: robots-sitemap robots.txt parse, sitemap reachability and validity
- All crawlability and indexing checks
intermediate10 minpublished updated Maks Verny