Skip to content

feat(spend_logs): native Postgres partitioning for SpendLogs retention - #29454

Closed
yassin-berriai wants to merge 1 commit into
litellm_internal_stagingfrom
claude/magical-gauss-r8Hen
Closed

feat(spend_logs): native Postgres partitioning for SpendLogs retention#29454
yassin-berriai wants to merge 1 commit into
litellm_internal_stagingfrom
claude/magical-gauss-r8Hen

Conversation

@yassin-berriai

Copy link
Copy Markdown
Contributor

Relevant issues

High-volume deployments see LiteLLM_SpendLogs grow to hundreds of GB within a month even with request/response payloads excluded. The root cause is that retention via DELETE does not return disk to the OS; it leaves dead tuples that autovacuum cannot reclaim fast enough when writes run at millions of rows per day, so the table bloats indefinitely.

Linear ticket

LIT-3502

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Type

🆕 New Feature

Changes

This adds opt-in time-based range partitioning for LiteLLM_SpendLogs. With a partitioned table, retention drops whole partitions with DROP TABLE, an instant metadata operation that frees disk immediately, with no tombstones and no vacuum.

The design is deliberately non-invasive for existing installs. There is no forced migration and no schema change shipped in a Prisma migration; the default table stays non-partitioned. The cleanup job auto-detects whether the table is partitioned by querying the Postgres catalog. When it is, the job reclaims disk by dropping expired partitions and pre-creates upcoming partitions on each run; when it is not (every existing deployment), it keeps the current batched DELETE behavior unchanged. Catalog-detection failures fall back to DELETE, so the job is never left in a broken state.

Converting an existing table is a manual, documented operation in db_scripts/partition_spend_logs.sql, since Postgres cannot partition a populated table in place. The script renames the existing table aside, creates the partitioned parent, the composite primary key, the startTime index, and a DEFAULT partition as a safety net. The partition key must be part of the primary key, so the key becomes the composite (request_id, startTime). The live spend-log write path uses create_many(skip_duplicates=True) which compiles to INSERT ... ON CONFLICT DO NOTHING and is compatible with the composite key.

New code is litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py, wired into SpendLogCleanup by dependency injection. Partition naming, range math, retention selection, and bound parsing are pure functions with direct unit tests; the manager's DB methods and both cleanup branches (partitioned drop vs. non-partitioned delete) are covered with mocked Prisma clients. Granularity and pre-create lookahead are tunable via SPEND_LOG_PARTITION_INTERVAL (default day) and SPEND_LOG_PARTITION_PRECREATE_AHEAD (default 7).

Docs are in a companion PR on BerriAI/litellm-docs.

Follow-ups (not in this PR, to keep scope isolated)

The legacy insert_data(table_name="spend") upsert path uses ON CONFLICT (request_id), which would not work against the composite key. It has no active spend-log writer today (the live path is create_many), so it is out of scope here and noted for a follow-up if it is ever reintroduced.

Screenshots / Proof of Fix

Proof of fix against a live partitioned table to follow before this leaves draft. Manual verification plan: convert a dev DB with db_scripts/partition_spend_logs.sql, set maximum_spend_logs_retention_period, send real traffic so multiple day-partitions are created, then confirm via \d+ "LiteLLM_SpendLogs" that expired partitions are dropped on the cleanup run while recent ones remain.


Generated by Claude Code

High-volume deployments see LiteLLM_SpendLogs grow to hundreds of GB in a
month because retention via DELETE leaves dead tuples that autovacuum cannot
reclaim fast enough, so disk is never returned to the OS.

This adds opt-in time-based range partitioning on startTime. When the table is
partitioned, the existing spend-log cleanup job reclaims disk by dropping whole
partitions (instant, frees disk immediately) instead of batched deletes, and
pre-creates upcoming partitions on each run. Detection is automatic via the
Postgres catalog, so non-partitioned installs are completely unaffected and keep
the existing DELETE behavior.

Converting an existing table is a manual, documented operation
(db_scripts/partition_spend_logs.sql) since Postgres cannot partition a
populated table in place. The partition key must be part of the primary key, so
the PK becomes composite (request_id, startTime); the live write path uses
INSERT ... ON CONFLICT DO NOTHING, which is compatible.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.42553% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
..._transaction_queue/spend_logs_partition_manager.py 89.15% 9 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor Author

Superseded by #29466 (same work on the renamed branch litellm_fix/spendlogs-partition). Closing this one.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants