Case studies / Open source

Eliminating 762 repeated queries from OpenProject’s Work Packages API

An open source Rails performance fix that removed repeated type-variant lookups from OpenProject’s Work Packages API and cut median server duration by 55.7%.

OpenProject Work Packages API results at a glance: 56% lower median server duration, 38% less SQL time, 84% fewer SQL events, 90% fewer duplicate query fingerprints, and both repeated type-variant query patterns reduced from 381 to zero.

At a glance

ProblemRepeated project-type and type-variant queries while serializing work packages
ScopeGET /api/v3/work_packages in OpenProject’s Rails application
Result55.7% lower median server duration and 83.9% fewer SQL events for a page of 20 work packages
DeliveryRegression-tested open source change merged upstream

The problem

OpenProject’s API v3 serializer calls Project#type_variant while building work package resources, schemas, and links. Although a response can contain many work packages with the same project and type, each call could query the project_types join and then load its associated variant.

For the profiled page of 20 work packages, both query patterns occurred 381 times:

  • finding a project_type by project and type; and
  • loading that project type’s variant by ID.

The application was repeatedly retrieving the same data while constructing one response. In total, the request generated 922 SQL events, including 827 served by Rails’ query cache.

What I changed

I connected the existing work package eager-loading path to the model lookup that needed the data:

  • the collection loader now preloads each project’s project_types and every project type’s variant;
  • Project#type_variant checks whether the project_types association is loaded;
  • when it is loaded, the method finds the matching project type in memory and uses its preloaded variant; and
  • when it is not loaded, the original database lookup remains as a fallback for other callers.

The existing type.default_variant fallback is also preserved. This keeps the model method safe outside the API collection path while allowing serialization to benefit from the eager-loaded association graph.

Measured result

I measured the endpoint with Rack Mini Profiler using an admin API key and a page size of 20 work packages.

MetricBefore medianAfter medianChange
Server duration2,277 ms1,009 ms−55.7%
SQL duration149.81 ms92.79 ms−38.1%
SQL events922148−83.9%
Cached SQL events82764−92.3%
Duplicate query fingerprints86591−89.5%
Project/type lookups3810−100%
Variant-by-ID lookups3810−100%

The server-duration figures came from a local Docker development environment, so they are subject to runtime variance. The stable query counts show the direct result: both targeted repeated-query patterns were eliminated. Response contents and size were unchanged.

Validation

The model spec preloads project types and variants, resolves a type variant, and asserts that the operation executes zero queries. The API integration spec verifies that the eager loader loads all project types—including one not used by the work package—preloads each associated variant, and allows type_variant to run without another query.

These checks cover both halves of the optimization: the collection loader supplies the complete association graph, and the model consumes it without returning to the database. The change was approved and merged into OpenProject upstream.

View the merged OpenProject pull request →