---
title: Fix nflverse contracts import heap OOM
type: task
status: completed
description: >-
  The 2026-08-10 nflverse contracts parquet renamed its yearly-detail list cols -> season_history
  and began emitting a seasons x contracts cross product, which OOMed the importer at any heap size.
  Reworked the importer to read only the columns it consumes and to read the nested list one leaf
  column at a time.
base_uri: user:task/league/fix-nflverse-contracts-import-heap-oom.md
created_at: '2026-08-10T21:48:37.438Z'
entity_id: 134b42a6-e580-4de1-9d60-9ab9ec52078b
finished_at: '2026-08-11T23:20:25.811Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[user:repository/active/league]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-08-11T23:20:42.891Z'
visibility_analyzed_at: '2026-11-05T23:49:26.101Z'
---

## Context

`service:league-import-player-contracts-nflverse` (cron `0 3 * * *` daily 03:00 ET on league, source `league/server/crontab-main/league-imports.cron` line 104) crashed its 2026-08-10 03:00 ET run with a V8 heap OOM (`FATAL ERROR: Reached heap limit`, ~4GB) during player-cache preload / parquet processing. This surfaced as signal 125117 (pipeline_failure, failure_mode stale), censused 2026-08-10 as a GENUINE stall — cause present and will recur on every run until fixed.

Root cause:

- The 2026-08-10 nflverse contracts parquet (`/tmp/nflverse_contracts_2026-08-10.parquet`) is 20.8MB versus the ~6.7MB historical daily norm — a 3x jump from the nflverse data source.
- The importer (`scripts/import-player-contracts-nflverse.mjs` in the production checkout) preloads the full player set into an in-memory cache (`preload_active_players` with `all_players` + 3 indexes) and reads the whole parquet into a buffer (`asyncBufferFromFile`) before processing.
- Node's default heap ceiling on this host is ~4GB (`heap_size_limit` 4144MB); the larger parquet pushed it over. The host has 24GB RAM (~17.5GB available), so headroom exists.

## Corrected root cause (2026-08-11)

The file-size reading above is a symptom, and the heap-raising plan below it was invalid — a manual run at `--max-old-space-size=8192` still aborted, and a bare `parquetRead` of the file exceeds 16GB against a host with ~17.6GB available. The real change is to the parquet SCHEMA, verified by reading both the 08-09 and 08-11 files on the host:

- The yearly-detail list column was renamed `cols` -> `season_history`, so the importer's `row.cols` read resolved to nothing. Its 13 sub-fields are otherwise unchanged and map 1:1 to `format_yearly_contract_detail`.
- A parallel `contract_history` list (11 sub-fields) was added, which the importer never reads.
- Both lists now carry the CROSS PRODUCT of each player's seasons and contracts, padded with nulls where elements are unpaired: 3,231,117 elements each against ~311K under the old schema, an identical count for both lists.
- Materializing `season_history` through `parquetRead` peaks at 9.75GB of heap on its own. Row-range and offset-index reads do not bound this — the column has 3 coarse pages of ~18K rows.

## Plan

- [x] Rework the importer to read `season_history` instead of `cols`, select only the columns it consumes, and stay within a bounded heap. Shipped in league `cb3049c4d`: the scalar columns are read normally, and `season_history` is read one leaf column at a time through hyparquet's published `./src/*.js` internals (its `columns` option filters by top-level name only, so a single leaf cannot be selected through the public API), with the cross product collapsed to one element per (row, season year) before records are assembled.
- [x] Verify a run completes and reports success. Validated on the host against deployed `cb3049c4d`: 51,045 `player_contracts` rows against the 50,000 floor, 11,337 player summaries, peak RSS 2.6-3.6GB, 27s, exit 0. Reported to the runs ledger; signal 125117 auto-resolved 23:15:52 and escalation 125181 self-closed 23:17:14.
- [x] Confirm the nflverse change is permanent and add a regression note. Both the 08-10 and 08-11 releases carry the new schema at ~20MB, so it is treated as permanent; the regression is documented in a header comment on the `SEASON_HISTORY_COLUMN` constant in the importer.

## Open decision

The cron's `--max-old-space-size=8192` (commit `6361cf7b6`) is retained rather than reverted. It is no longer load-bearing — peak RSS is ~2.6GB, and the transient cost is one assembled leaf column — but the upstream element count grew 10x in a single day, and that transient scales with it. The flag is cheap headroom against another such jump; revert it if the schema stabilizes.
