{"record":{"id":"7f3afb181311bb8f","repo":"TechnitiumSoftware/DnsServer","slug":"cannot-update-record-ttl-cannot-be-greater-than-s-7f3afb","errorCode":null,"errorMessage":"Cannot update record: TTL cannot be greater than SOA EXPIRE.","messagePattern":"Cannot update record: TTL cannot be greater than SOA EXPIRE\\.","errorType":"exception","errorClass":"DnsServerException","httpStatus":null,"severity":"error","filePath":"DnsServerCore/Dns/Zones/ForwarderZone.cs","lineNumber":250,"sourceCode":"                    }\n\n                    return false;\n            }\n        }\n\n        public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)\n        {\n            switch (oldRecord.Type)\n            {\n                case DnsResourceRecordType.SOA:\n                    throw new InvalidOperationException(\"Cannot update record: use SetRecords() for \" + oldRecord.Type.ToString() + \" record\");\n\n                default:\n                    if (oldRecord.Type != newRecord.Type)\n                        throw new InvalidOperationException(\"Old and new record types do not match.\");\n\n                    if (newRecord.OriginalTtlValue > GetZoneSoaExpire())\n                        throw new DnsServerException(\"Cannot update record: TTL cannot be greater than SOA EXPIRE.\");\n\n                    if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord))\n                        throw new DnsServerException(\"Cannot update record: the record does not exists to be updated.\");\n\n                    AddRecord(newRecord, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords);\n\n                    List<DnsResourceRecord> allDeletedRecords = new List<DnsResourceRecord>(deletedRecords.Count + 1);\n                    allDeletedRecords.Add(deletedRecord);\n                    allDeletedRecords.AddRange(deletedRecords);\n\n                    CommitAndIncrementSerial(allDeletedRecords, addedRecords);\n\n                    TriggerNotify();\n                    break;\n            }\n        }\n\n        public override IReadOnlyList<DnsResourceRecord> QueryRecords(DnsResourceRecordType type, bool dnssecOk)","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/TechnitiumSoftware/DnsServer/blob/d0484b6c1e7439cdc53d67d81e9c876cda2ad756/DnsServerCore/Dns/Zones/ForwarderZone.cs#L232-L268","documentation":"Thrown by ForwarderZone.UpdateRecord() when newRecord.OriginalTtlValue exceeds GetZoneSoaExpire(). The check enforces RFC 1035 SOA semantics: a record's TTL must not outlive the zone's SOA EXPIRE, because slaves would be permitted to serve stale data past the point the zone is considered stale. The default forwarder-zone dummy SOA sets Expire to 604800 (7 days). This is a DnsServerException (a domain/operational error, surfaced as an HTTP failure to API clients), so it is catchable.","triggerScenarios":"zone.UpdateRecord(oldRecord, newRecord) where newRecord.OriginalTtlValue (the un-capped TTL before zone-minimum clamping) > the zone's SOA EXPIRE value. Triggered by importing records with very high TTLs, or by raising the record TTL without first raising SOA EXPIRE.","commonSituations":"Bulk import of external zone data with TTLs like 1209600 (14d); copying records from a primary zone whose SOA EXPIRE is larger than the forwarder's 604800; misreading OriginalTtlValue vs the already-clamped display TTL.","solutions":["Lower newRecord.OriginalTtlValue to <= GetZoneSoaExpire() (default 604800 seconds for a forwarder zone).","If a longer TTL is required, raise the forwarder zone's SOA EXPIRE via SetRecords(SOA, ...) first.","Clamp incoming TTLs at your import boundary: ttl = Math.Min(ttl, (int)zone.GetZoneSoaExpire())."],"exampleFix":"// before\nnewRecord = new DnsResourceRecord(name, type, cls, importedTtl, rdata);\nzone.UpdateRecord(oldRecord, newRecord);\n\n// after\nuint maxTtl = zone.GetZoneSoaExpire();\nuint safeTtl = Math.Min(importedTtl, maxTtl);\nnewRecord = new DnsResourceRecord(name, type, cls, safeTtl, rdata);\nzone.UpdateRecord(oldRecord, newRecord);","handlingStrategy":"validation","validationCode":"uint maxTtl = zone.GetZoneSoaExpire();\nif (newRecord.OriginalTtlValue > maxTtl)\n    newRecord = new DnsResourceRecord(newRecord.Name, newRecord.Type, newRecord.Class, maxTtl, newRecord.RDATA);\nzone.UpdateRecord(oldRecord, newRecord);","typeGuard":"static bool IsTtlWithinSoaExpire(DnsResourceRecord rec, uint soaExpire) => rec.OriginalTtlValue <= soaExpire;","tryCatchPattern":"try { zone.UpdateRecord(oldRecord, newRecord); }\ncatch (DnsServerException ex) when (ex.Message.Contains(\"SOA EXPIRE\")) { /* clamp TTL and retry */ }","preventionTips":["Always clamp record TTLs against GetZoneSoaExpire() at the import/API boundary.","When raising a record TTL, verify the zone's SOA EXPIRE first and raise it if needed.","Remember the default forwarder-zone SOA EXPIRE is 604800s (7 days)."],"tags":["dns","forwarder-zone","ttl","soa-expire","dnsserverexception"],"backgroundTag":null,"analyzedSha":"d0484b6c1e7439cdc53d67d81e9c876cda2ad756","analyzedAt":"2026-08-13T22:57:35.508Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}