The UK public sector IT contract expiry cliff Dataset: uk-public-sector-it-contract-expiry-2026.csv WHAT THIS IS One row per UK public sector IT contract whose STATED contract end date falls in the 12 months from the snapshot date: 25 September 2026 to 24 September 2027 inclusive. n = 9,416 contracts, held by 1,430 distinct buyer names, counted by normalising the 1,613 spellings the two services publish. This is a population of published notices, not a sample, and not a register of contracts. It is built from the two services on which UK contracting authorities publish contract notices: Contracts Finder and the Find a Tender Service. Both publish notices as they happen. Neither publishes a forward view of what is about to expire. That view is what this dataset is. Commercial services do publish forward expiry views over these same feeds - PSIP sells one across five UK portals, Stotles publishes a free browsable list, Tussell has published forward IT expiry totals for central government - and they are cited in the study. What is open here is the method: a pre-registered IT definition with its sensitivities, a stated deduplication rule with its counts, five ways of totalling the same pounds, and this file. SNAPSHOT DATE: 2026-09-25 Every figure is as at that date. Contracts Finder was harvested in full from its first published day, 2014-12-17, to 2026-09-23. Find a Tender was harvested in full from 2021-01-01 to 2026-09-24. WHAT A ROW IS NOT A stated contract end date is not the date a contract actually ends. Extensions, options and early termination are invisible in published notices. This dataset measures what buyers published, and nothing else. There is no supplier column, no supplier ranking and no supplier-level total anywhere in this dataset. Supplier names are present in the raw published notices and were deliberately not carried through: the study is about buyers and timing, and a supplier can be a sole trader. They are not eliminated, though - buyers write supplier names into their own notice titles, and 44 of the 9,416 titles here contain a company suffix, counted as a case-SENSITIVE match of the regular expression \b(?:Ltd|Limited|PLC|LLP|Inc|GmbH)\b against the title. The same search ignoring case returns more. Titles are reproduced exactly as published, under OGL v3.0. suppliers_named_on_notice is a COUNT, never a name. No contact point, no individual's name and no email address is read into any output. THE MONEY IS A FLOOR, AN OVER-COUNT AND NOT A MEASURE OF IT SPEND, ALL AT ONCE Summed exactly as published, the 8,928 rows that state a positive GBP amount total GBP 130.13 bn. It is a FLOOR because: - rows stating zero, a negative amount or a non-GBP currency are excluded, never converted; - Find a Tender states a contract end date only on notices PUBLISHED from 24 February 2025. A contract signed earlier still reaches this dataset if a later Procurement Act notice restates it - 19 rows here carry a signature date before that day, the earliest 2012 - but a contract whose only notice predates the regime is invisible to that service. Contracts Finder carries the older history. It is an OVER-COUNT because: - a framework agreement states a CEILING shared across every appointed supplier, and the call-off contracts placed under it are published separately and counted too; - under the pre-registered any-CPV rule, a large non-IT contract carrying a single IT sub-code is counted at its full value. And it is NOT A MEASURE OF IT SPEND. Under that same any-CPV rule, 52.1% of the sterling total sits on 270 contracts whose PRIMARY CPV code is outside CPV divisions 30, 32, 48 and 72. The single largest notice in the whole study is a Crown Commercial Service electricity and gas supply framework whose primary code is in CPV division 09; it enters the IT basket on incidental sub-codes and is 39% of the sterling total on its own. The primary-CPV variant below drops it. Aggregate value_amount_gbp by the first two digits of primary_cpv_code to see the whole split. The headline and four alternative totals are all published, so a reader can choose, and the CSV carries the flags needed to rebuild any of them: as published GBP 130.13 bn (8,928 rows) in-notice repeats collapsed GBP 124.02 bn (8,835 rows) repeats collapsed AND aggregated awards excluded GBP 97.12 bn (8,716 rows) rebuild: value_used_in_gbp_total = 1 AND in_notice_repeat = 0 AND aggregated_award = 0. This cut is cumulative on the one above it; aggregated_award = 0 alone gives a larger figure. largest 1% of rows excluded GBP 8.41 bn (8,839 rows) primary-CPV field rule GBP 62.09 bn (8,628 rows, 9,085 contracts) Holding the CPV basket still and changing only the field rule moves the contract count a little and the pounds a lot. That is a claim about this pair of cuts, not about every cut: widening or narrowing the basket changes what is being counted and moves the count a great deal - on the narrow basket the count roughly halves while the pounds barely move. COLUMNS row_id: Sequential row number in this file. Not an identifier of anything outside it. source_service: contracts-finder or find-a-tender. Which service published the notice. source_route: How this row reached the study. daily-bulk-file: the service's own daily bulk file on data.gov.uk, which is a snapshot of the notice on its publication day. ocds-api-gapfill: the Contracts Finder OCDS API, used only for the 20 publication days the bulk route serves broken, and which returns the CURRENT state of a notice rather than its publication-day state. The difference is small in this dataset but real, so it is carried rather than hidden. ocid: Open Contracting identifier as published. Contracts Finder issues identifiers beginning ocds-b5fd17- and Find a Tender ocds-h6vhtk-; because the two ranges cannot intersect, joining on OCID across the services returns zero matches whatever the real overlap is, which is why a secondary duplicate rule exists. notice_id: The notice: OCDS release id on Contracts Finder, notice document id on Find a Tender. record_id: Which award or contract block inside that notice this row is. A notice with several lots carries several. notice_published_date: Date the notice carrying this contract was published. buyer_name: Buying organisation as the notice states it. An organisation, never a person. buyer_name_key: buyer_name normalised (lower case, punctuation stripped, '&' spelled out). The only key that works across both services and the register. buyer_identifier_scheme: Identifier scheme as published: GB-CFS, GB-SRS, GB-GOR, GB-LAE on Contracts Finder; GB-PPON, GB-COH, GB-NHS on Find a Tender. Often absent. buyer_identifier: The identifier itself, where the notice gives one. Often absent on Contracts Finder. buyer_record_identifier: The buyer record the service itself holds: the published identifier where there is one, otherwise the service's own internal buyer id (Contracts Finder issues GB-CFS records to buyers who never registered centrally). Counting these roughly doubles the buyer figure against counting distinct names, because one organisation can hold several records. Both counts are published. buyer_on_cabinet_office_register: 1 if buyer_name_key matches a name on the Cabinet Office buyer register, else 0. Context only: the register is not a superset of notice publishers and a 0 does not mean the body is unregistered. title: Contract or notice title as published, newlines removed. primary_cpv_code: The notice's primary CPV code (Contracts Finder: tender.classification; Find a Tender: the first code recorded on the notice). primary_cpv_description: Official CPV 2008 English description of that code, from Annex I to Regulation (EC) No 2195/2002 as it stands in assimilated UK law on legislation.gov.uk. This is the ONE column in this file copied verbatim from a source, and it is not under the OGL: legislation.gov.uk re-uses EUR-Lex-derived content under Commission Decision 2011/833/EU. Reuse it with that acknowledgement (c) European Union, https://eur-lex.europa.eu. primary_cpv_in_core_basket: 1 if the primary code is itself in the study's core IT basket. all_cpv_codes: Every CPV code recorded anywhere on the notice, pipe-separated, check digits stripped. core_basket_codes_on_this_notice: The subset of those codes that put this contract in the core IT basket. This is the audit trail for the IT filter: it says exactly why the row is here. value_amount_gbp: The amount used in this study's GBP totals. Empty where the row is excluded. value_currency_as_published: Currency as published. Non-GBP amounts are never converted. value_amount_as_published: The amount exactly as published, including zeros and the one negative figure. value_amount_gross_incl_vat: Find a Tender only: the VAT-inclusive figure. Contracts Finder publishes no gross field, so this is empty there and the two must not be mixed. value_used_in_gbp_total: 1 if this row's value enters the study's GBP totals, else 0. value_exclusion_reason: Why not, where 0: no value stated, value stated as zero, value stated as a negative amount, or value stated in another currency. contract_start_date: Stated contract start date. contract_end_date: Stated contract end date. This is what the study measures. It is not the date the contract actually ends. contract_max_extent_date: Find a Tender only: the end date if every option and extension is taken. Empty on Contracts Finder, which publishes no equivalent. expiry_month: contract_end_date truncated to the month. award_or_signature_date: Award date (Contracts Finder) or contract signature date (Find a Tender). procurement_route: Procurement procedure as published (Contracts Finder) or main procurement category (Find a Tender). The two are not the same field and are not comparable. is_call_off: Contracts Finder only: 1 where the published procedure is a call-off from a framework agreement or a dynamic purchasing system. Empty on Find a Tender. publisher_framework_flag: Find a Tender only: the publisher's own flag that this notice is a framework agreement. Empty on Contracts Finder, which publishes no such flag at all. suppliers_named_on_notice: Find a Tender only: how many suppliers the notice names. Empty on Contracts Finder. aggregated_award: 1 where the row looks like a framework agreement, dynamic purchasing system or multi-supplier lot award, whose stated value is a ceiling shared across suppliers rather than one buyer's committed spend. aggregated_award_basis: Exactly why: the publisher's own flag, the number of suppliers named, or framework language in the title. Empty where the row is not flagged. in_notice_repeat: 1 where another row in the SAME notice states an identical value, an identical contract start date and an identical contract end date - one award or contracts block per appointed supplier, each restating the same ceiling. The first in publication order is 0, the rest are 1. Flagged, never deleted: sum value_amount_gbp where value_used_in_gbp_total = 1 and in_notice_repeat = 0 to rebuild the collapsed-repeats total. cross_service_duplicate_test: strict, loose or empty: whether this row also appears on the other service under the published secondary rule. Rows matching either strict key are removed before publication, so this column reads 'loose' or empty in the published file. 'loose' rows are KEPT: that key ignores the stated value and cannot tell a duplicate from a renewal at a different price. in_narrow: 1 if the notice falls in the narrow CPV basket (CPV division 72 only, 252 codes). in_core: 1 if it falls in the core basket, the pre-registered headline (702 codes). Every row in this file is 1, because this file IS the core cut. in_core_plus_adjacent: 1 if it falls in the core-plus-adjacent basket (799 codes). Every row in this file is 1, because the core basket is a subset of it. Both wider baskets contain every row of this file, so the core_plus_adjacent and wide cuts the page reports CANNOT be rebuilt from this file alone - the extra rows are not in it. Rerun analyse.py to reproduce them. Note that core_plus_adjacent and wide are not nested in each other: wide takes CPV divisions 30, 32, 48 and 72 whole, while core_plus_adjacent adds 97 codes that sit OUTSIDE those four divisions (IT maintenance 503, installation 5161, telecommunications services 642 and a handful of others) and which wide therefore does not contain. That is why the wide cut can hold more contracts than core_plus_adjacent and less money. in_wide: 1 if it falls in the wide basket (all of CPV divisions 30, 32, 48 and 72, 1,077 codes). Every row in this file is 1, for the same reason as in_core_plus_adjacent. in_core_primary_cpv_rule: 1 if the notice would still count as IT under the pre-registered primary-CPV sensitivity, which looks only at the primary code. snapshot_date: The date the underlying data was retrieved. Every figure in this study is as at this date. METHOD 1. Both services were harvested in full from their bulk files on data.gov.uk, published by Crown Commercial Service. The rate-limited OCDS APIs were used only to repair 20 publication days whose bulk file the publisher serves broken, and for spot checks. 2. Notices were normalised to one row per award or signed-contract block. Notices with no award block - planning, tender and pipeline notices - never enter. Pipeline notices are planned FUTURE procurement and are the opposite of an expiring contract; 10,413 were found on Find a Tender and every one was kept out. 3. Deduplicated within each service, in two passes. First by OCID: Contracts Finder keeps the latest release, because a buyer amending a contract republishes it under the same OCID; Find a Tender by OCID and contract block. Then by a secondary rule, because OCID cannot see the same contract republished under a NEW OCID - normalised buyer name + normalised title + stated value in whole units + contract end date, across different OCIDs, keeping the earliest-published. That removed 542 further rows. A buyer-independent version of that key would remove 23 more, and is NOT applied: inside one service it also matches genuinely different buyers - two NHS trusts appointing the same platform for the same money on the same date, two district councils sharing a contract. Its size is disclosed rather than acted on. 4. Filtered to IT by a CPV rule written and hashed BEFORE any expiring value or count was computed (2026-09-25T09:21:52Z, sha256 7da4db343cfe2aa3f02a6fa150a2f43ab412007d1e403ff3daaa56722adf74d7). The headline uses the core basket (702 of the 9,454 CPV 2008 codes) and counts a notice as IT if ANY code recorded anywhere on it is in the basket. Three other baskets and a primary-code-only field rule are published as sensitivities. 5. Filtered to contracts whose stated end date falls inside the 12-month window. 6. Deduplicated ACROSS the two services. OCID cannot do this: Contracts Finder issues ocds-b5fd17-... and Find a Tender ocds-h6vhtk-..., and because the two ranges cannot intersect, joining on OCID across the services returns zero matches whatever the real overlap is. A published secondary rule was used instead: strict key A: normalised buyer name + normalised title + value + start + end date strict key B: normalised title + end date + value, buyer name ignored loose : normalised buyer name + normalised title + end date Key B exists because the buyer name is the field that differs between the services: 'Lancashire Constabulary' on one is 'Police and Crime Commissioner for Lancashire' on the other. A row matching EITHER strict key is removed. A row matching only the loose key is flagged and KEPT, because that key ignores the value and cannot tell a duplicate from a renewal at a different price. On the headline cut the strict rule matched 9 rows and a further 7 matched ONLY the loose key and were flagged and kept. The test is run once, over all 16,777 Find a Tender and 28,688 Contracts Finder rows ending in the window on ALL CPV divisions, and every basket cut reuses those flags; the counts above are the matches that fall inside the headline IT cut. Strict matches were removed, keeping the Contracts Finder row. 7. Aggregated by month, by buyer and by CPV division. WHAT THE ANSWER IS contracts expiring in the window 9,416 distinct buyers 1,430 stated value, as published GBP 130.13 bn (a FLOOR and an OVER-COUNT at once, and not a measure of IT spend: see THE MONEY above. 488 contracts are excluded rather than estimated, framework ceilings sit alongside the call-offs placed under them, and the largest single notice is a GBP 51.00 bn Crown Commercial Service ENERGY framework, 39.19% of this total. Lead on the contract count, not on this figure.) biggest month 2027-03 (2,082 contracts) biggest single date 2027-03-31 (1,405 contracts) median contract value GBP 108,068 (quantiles here are ORDER STATISTICS, not interpolated: the value at index floor(n * p) of the sorted list. With an even n the reported median is the UPPER of the two middle observations, not their mid-point, so numpy or statistics.median on the same rows returns a slightly different figure.) BUYERS: WHY THERE IS NO 'M OF 7,753' The buyer count of 1,430 is a count of distinct NORMALISED names: 1,613 spellings as published collapse to that many keys. The Cabinet Office buyer register holds 7,753 REGISTRATIONS, of which 7,736 are currently registered. They resolve to 7,337 distinct names as published and 7,132 after the same normalisation used to match buyers; 491 names hold more than one registration. Matching is by name because most notices publish no buyer identifier at all. Where the register's OWN identifier is published it does join, and is not a separate namespace: 2,340 of the 9,416 rows (24.9%) carry a GB-SRS identifier in one of the register's own two namespaces (supplierregistration.cabinetoffice.gov.uk/ and the predecessor sid4gov.cabinetoffice.gov.uk/), carrying 381 distinct identifiers across 386 buyer names, and all 381 of those identifiers resolve in the register file. It is still not a denominator, but NOT because it is published too rarely: on 185 of those rows, covering 40 bodies, the registered name differs from the name on the notice - the identifier on the largest expiring notice resolves to 'GOVERNMENT COMMERCIAL AGENCY' while the notice names Crown Commercial Service - and the register is not a superset of notice publishers, and counts registrations rather than organisations. The buyer count in this study is therefore a count of distinct buyers INSIDE the expiring set, and the register is context. LICENCE AND ATTRIBUTION Source data: Open Government Licence v3.0 https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ Contains public sector information licensed under the Open Government Licence v3.0. Contracts Finder notices are published by the Cabinet Office and Crown Commercial Service under OGL v3.0, declared in the license and publicationPolicy columns inside the CSVs themselves. The Find a Tender bulk XML declares no licence element at all and the data.gov.uk dataset pages for both services read 'Licence: Not set', so on that side OGL v3.0 rests on the data.gov.uk site-wide statement, the Crown Commercial Service publisher and the same notices declaring license = OGL v3.0 when served through the Find a Tender OCDS API. The Cabinet Office buyer register is published under OGL v3.0 via the Open Contracting publication policy. CPV code descriptions are NOT under the OGL. They come from Annex I to Regulation (EC) No 2195/2002 as it stands in assimilated UK law at legislation.gov.uk, which is EUR-Lex-derived content: legislation.gov.uk states that all content is available under OGL v3.0 'except where otherwise stated' and that the site 'additionally contains content derived from EUR-Lex, reused under the terms of the Commission Decision 2011/833/EU on the reuse of documents from the EU institutions'. That is the basis for the primary_cpv_description column, the one field in this file copied verbatim from a source. Commission Decision 2011/833/EU (EUR-Lex re-use), via legislation.gov.uk https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32011D0833 © European Union, https://eur-lex.europa.eu, 1998-2026. This compilation and the derived columns: CC BY 4.0. The underlying notice data remains under OGL v3.0 and must keep its attribution. Published by Servnet (servnetuk.com). Cite as: Servnet, 'The UK public sector IT contract expiry cliff', snapshot 2026-09-25. REPRODUCING THIS FILE python3 scripts/research/uk-public-sector-it-contract-expiry-2026/analyse.py It makes no network request. Every input is a file in data/research/uk-public-sector-it-contract-expiry-2026/