Summary
GET /api/v1/image_map/points returns every accessible point. MAX_CLUSTERED_POINTS = 50_000 (projection.py) only disables clustering — it is not a cap on the response. There is no pagination, no limit, and no rate limit.
Measured on this PR's code:
| points |
body |
full request |
worst single event-loop stall |
Σ stalls > 5 ms |
| 20,000 |
1.8 MB |
0.32 s |
71 ms |
0.13 s |
| 50,000 |
4.6 MB |
0.70 s |
183 ms |
0.36 s |
| 120,000 |
11.0 MB |
1.11 s |
399 ms |
1.07 s |
| 500,000 (serialize only) |
50 MB |
— |
— |
1.57 s |
Where the time goes
Clustering is correctly off the event loop via asyncio.to_thread. Two things are not:
- Response serialization —
jsonable_encoder/json.dumps over the point list runs on the loop.
- Pre-thread DB reads —
list_accessible_embedded_images + get_projection + scope_hash measure 13 ms @20k, 32 ms @50k, 82 ms @120k on the loop. GET /status does the same.
So the PR's "never blocks" claim holds for the UMAP fit and for clustering, but not for assembling and writing the response.
Suggested direction
Paginate, or add a limit with a documented default, or stream. Moving the pre-thread DB reads into the worker thread is a smaller independent win.
Context
Found during adversarial review of PR #37. Not a correctness or security bug — the access filtering is applied before clustering and verified correct — but it scales badly with gallery size and is trivially reachable by any authenticated user.
Summary
GET /api/v1/image_map/pointsreturns every accessible point.MAX_CLUSTERED_POINTS = 50_000(projection.py) only disables clustering — it is not a cap on the response. There is no pagination, nolimit, and no rate limit.Measured on this PR's code:
Where the time goes
Clustering is correctly off the event loop via
asyncio.to_thread. Two things are not:jsonable_encoder/json.dumpsover the point list runs on the loop.list_accessible_embedded_images+get_projection+scope_hashmeasure 13 ms @20k, 32 ms @50k, 82 ms @120k on the loop.GET /statusdoes the same.So the PR's "never blocks" claim holds for the UMAP fit and for clustering, but not for assembling and writing the response.
Suggested direction
Paginate, or add a
limitwith a documented default, or stream. Moving the pre-thread DB reads into the worker thread is a smaller independent win.Context
Found during adversarial review of PR #37. Not a correctness or security bug — the access filtering is applied before clustering and verified correct — but it scales badly with gallery size and is trivially reachable by any authenticated user.