Files
triggerdotdev--trigger.dev/.server-changes
Eric Allam db67a856fe
📦 Preview packages (pkg.pr.new) / Build and publish previews (push) Has been cancelled
📚 Publish docs / publish (push) Has been cancelled
perf(webapp,database): index the newest-task-version lookup (#4518)
Implementing PlanetScale Insights improvement.

## Summary

Validating a schedule (creating or updating one through the API or the
dashboard, and deploying a project that declares schedules) looks up the
newest version of a task by slug. That lookup reads *every* version of
the task and sorts them to return one. A project gains a row per task on
every deploy, so the work grows with the project's age: the oldest
projects pay the most, and dev-mode redeploys make it worse. This was
picked because it was the largest single consumer of database time on
the schedules path, and the fix is a sort key with no index behind it.

## Fix

`BackgroundWorkerTask` is indexed on `(projectId, slug)`, which serves
the equality but not the `ORDER BY createdAt DESC`. Postgres seeks the
index, then bitmap-scans and top-N sorts the whole group to produce a
single row. Adding `createdAt` to the index lets it scan backward and
stop at the first row.

The same call site also selected all 21 columns, including five JSON
blobs, to read one field (`triggerSource`), so it now selects that field
alone.

## Benchmark

Local Postgres 17, 997,000 seeded rows / 748 MB, group sizes chosen to
match the distribution seen in production.

| Group size | Before | After |
| --- | --- | --- |
| 15,000 versions of one task | 11.118 ms, 1,510 buffers, 15,000 rows
scanned | 0.027 ms, 4 buffers, 1 row |
| 2,000 versions of one task | 2.081 ms, 1,455 buffers, 2,000 rows
scanned | 0.022 ms, 4 buffers, 1 row |

```
before:  Limit -> Sort (top-N heapsort) -> Bitmap Heap Scan
after:   Limit -> Index Scan Backward using BackgroundWorkerTask_projectId_slug_createdAt_idx
```

An ascending index scanned backward is enough here, so no descending
index is needed.

## Impact and risk

Real-world gain lands between the two rows above and scales with how
many deploys a project has accumulated. Projects with few deploys will
see little change, since there is barely anything to sort.

The new index costs noticeably more than the existing two-column one: 43
MB against 7.3 MB on the benchmark rig. Adding `createdAt` makes every
key unique, which defeats btree deduplication, so this is a real disk
and write cost rather than a rounding error. Writes to this table happen
at deploy time, not on the run path, so the write amplification is
acceptable. The existing `(projectId, slug)` index is now a redundant
prefix and could be dropped, but this PR keeps it so index usage can be
observed before removing it.

Behavior is unchanged: same predicate, same ordering, same row returned.
The narrowed select is the only code change, and the field it keeps is
the only one the caller read.

Deploy note: the migration is
`20260806100000_add_background_worker_task_project_id_slug_created_at_index`
and uses `CREATE INDEX CONCURRENTLY IF NOT EXISTS`, so it can be
pre-applied by hand before the deploy.
2026-08-07 11:17:10 +01:00
..

Server Changes

This directory tracks changes to server-only components (webapp, supervisor, etc.) that are not captured by changesets. Changesets only track published npm packages — server changes would otherwise go undocumented.

When to add a file

Server-only PRs: If your PR only changes apps/webapp/, apps/supervisor/, or other server components (and does NOT change anything in packages/), add a .server-changes/ file.

Mixed PRs (both packages and server): Just add a changeset as usual. No .server-changes/ file needed — the changeset covers it.

Package-only PRs: Just add a changeset as usual.

File format

Create a markdown file with a descriptive name:

.server-changes/fix-batch-queue-stalls.md

With this format:

---
area: webapp
type: fix
---

Speed up batch queue processing by removing stalls and fixing retry race

Fields

  • area (required): webapp | supervisor
  • type (required): feature | fix | improvement | breaking

Description

The body text (below the frontmatter) is a one-line description of the change. Keep it concise — it will appear in release notes.

Writing guidance

These entries are public-facing - they ship verbatim in user-visible release notes. A few rules to keep them clean:

  • Write for the user, not the reviewer. Lead with what the user notices or has to do. If a reader who doesn't know the codebase can't tell what changed for them, rewrite it.
  • One sentence is usually enough. The body is the bullet in the changelog. If you need a paragraph, you're probably describing the implementation rather than the change.
  • Describe behavior, not implementation. Skip internal scopes, middleware names, library specifics, framework internals. Users care about what's different for them, not how it's wired.
  • Never name internal tools or infra. Observability stacks, internal services, infra components, monitoring backends, CI surfaces, AWS specifics - none of these belong in user-facing notes.

Before / after:

  • "The image verification step now parses the manifest's layer media types and returns a new result the finalizer rejects." (describes the wiring; a user can't act on it)
  • "Deploying with an outdated CLI could produce an image that fails to start on every run. These deploys are now stopped before going live, with a message asking you to upgrade the CLI and re-deploy." (what the user sees and does)

Lifecycle

  1. Engineer adds a .server-changes/ file in their PR
  2. Files accumulate on main as PRs merge
  3. The changeset release PR includes these in its summary
  4. After the release merges, CI cleans up the consumed files

Examples

New feature:

---
area: webapp
type: feature
---

TRQL query language and the Query page

Bug fix:

---
area: webapp
type: fix
---

Fix schedule limit counting for orgs with custom limits

Improvement:

---
area: webapp
type: improvement
---

Use the replica for API auth queries to reduce primary load