Skip to content

⚡️ Skip included routers whose prefix cannot match the request path - #16159

Open
Kludex wants to merge 1 commit into
fastapi:masterfrom
Kludex:skip-included-routers-by-prefix
Open

⚡️ Skip included routers whose prefix cannot match the request path#16159
Kludex wants to merge 1 commit into
fastapi:masterfrom
Kludex:skip-included-routers-by-prefix

Conversation

@Kludex

@Kludex Kludex commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

_IncludedRouter._match scans every candidate in the included router, and recurses into nested ones, without ever checking whether the request path could be under that router's prefix. On an app built the idiomatic way - many include_router(..., prefix=...) calls - a request walks the whole route tree before it can 404, and a late route is only found after every earlier subtree has been fully scanned.

Every route in an included router is registered under its prefix, so a path that does not start with that prefix cannot match any of them, and the subtree can be skipped:

literal_prefix = self.include_context.prefix.split("{", 1)[0]
if literal_prefix and not get_route_path(scope).startswith(literal_prefix):
    return Match.NONE, {}, None, None

A prefix may contain path params (include_router(sub, prefix="/users/{user_id}")), so only the part before the first one is literal. Prefixes that start with a path param, and routers included without a prefix, are unaffected and still scanned as before.

Benchmark

808 real route paths (the GitHub REST API description), grouped into 36 included routers by first path segment. Microseconds per ASGI dispatch, best of 3:

scenario before after
hit on a late route 2512.0 102.3 24.6x
404 miss 2521.1 131.7 19.1x
mixed workload 1252.2 590.4 2.1x
hit on an early route 231.8 219.5 ~unchanged

Measured on released starlette 1.3.1. The numbers are unchanged against the current Starlette main branch too, since APIRouter.app does its own dispatch and does not go through Starlette's Router.app.

The same 808 routes registered flat on the app, with no included routers, are unaffected (no subtree to skip). Worth noting the grouped app was previously ~2.4x slower than the flat one on a late hit, so structuring an application with include_router carried a real cost; it is now faster than flat in every scenario measured.

Correctness

  • The full test suite passes, with fastapi/routing.py still at 100% coverage - the existing tests already exercise the skip branch.
  • Differential check over 4036 outcomes with and without the change, all identical: every one of the 808 route paths crossed with GET/POST/PUT/PATCH/DELETE, comparing status code and the Allow header so 405s are compared properly, plus misses, plus nested routers and a templated prefix="/{user_id}".

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

@codspeed-hq

codspeed-hq Bot commented Aug 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing Kludex:skip-included-routers-by-prefix (ab6a380) with master (c39c456)1

Open in CodSpeed

Footnotes

  1. No successful run was found on master (15410ee) during the generation of this report, so c39c456 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@YuriiMotov YuriiMotov changed the title Skip included routers whose prefix cannot match the request path ⚡️ Skip included routers whose prefix cannot match the request path Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants