I switched to FastAPI and uv. Here’s what actually got faster — and what didn’t

Elena Vasquez

Elena Vasquez

August 28, 2026

I switched to FastAPI and uv. Here’s what actually got faster — and what didn’t

I did not switch to FastAPI and uv because a benchmark said so. I switched because our Flask service had become a drawer of dicts, our Poetry lock took longer than the test suite, and I was tired of sending contractors a Postman collection I had forgotten to update. That was a year ago. Here is what actually got faster, and what I had to admit did not move.

If you are still deciding whether this pairing is even the right shape, I already wrote when FastAPI plus uv is the right stack and when it isn’t. This is the after-action: we were a Python API shop, we moved, I still have the CI graphs.

What I moved, exactly

The old stack was Flask, a hand-rolled marshmallow layer that people walked around, SQLAlchemy, Poetry, and a worker that was just more Flask functions imported into a scheduler. The new stack is FastAPI, Pydantic models as the boundary, the same database, uv for lock and install, Uvicorn behind the same gateway, and the worker still Python — just not on the request process.

I did not move the company. I moved one service that owned webhooks and a public JSON API. That constraint mattered. If I had tried to replace Django admin in the same pull request, this article would be a postmortem.

What got faster: installs, onboarding, and the Monday laptop

uv is the unglamorous win. Cold CI went from “go make coffee” to “still in the same Slack thread.” A new laptop used to be an afternoon of pyenv folklore and a Poetry version mismatch. Now it is a pin and a sync. I can measure that. I have the before-and-after on the same pipeline.

What I did not expect: fewer “works on my machine” arguments about transitive pins. We still have those arguments. They are shorter. The lockfile is a thing people actually open. Poetry’s lock was a thing people blamed.

I also stopped maintaining a private wiki page titled “if install fails.” That page was six ways to delete a venv. Deleting a venv is not engineering. It was a tax we had normalized.

Laptop showing API documentation next to a coffee cup in daylight

What got faster: the boundary, not the CPU

Adding an endpoint got faster because the model is the contract. I write the request and response shapes, FastAPI draws the OpenAPI, and the contractor stops asking me whether amount is a string. Validation errors got faster to diagnose because they show up as 422s with a field path instead of a 500 and a KeyError in a helper named normalize.

Code review got faster in a specific way. Reviewers argue about the model, not about whether we remembered to check if "id" in body. That is not the same as “we write less code.” We write different code. The dict soup moved into Pydantic and then, sometimes, back out when a vendor sent a field we had been too strict about. The speed is in the argument, not in the keystrokes.

Handoff got faster. I sent /docs to a partner and they built against it. Then we broke them with a change we thought was additive. OpenAPI makes the break visible. It does not make you kind. We added a changelog after the second incident. The framework did not do that. Embarrassment did.

What did not get faster: the request

I wanted the p99 to drop because we were “async now.” It did not, not in a way a user would notice. The old Flask app was not CPU-bound. It was waiting on Postgres and Stripe. FastAPI waited on the same Postgres and the same Stripe. We had been telling ourselves a performance story that was actually a packaging and typing story.

When the p99 did move, it was because we pulled a PDF export off the request path. That work was available in Flask. We used the rewrite as an excuse to do it. Credit the rewrite if you want. Credit the split.

Async also gave us a new way to be slow. Someone used a sync SDK in an async def and held a worker. The dashboards looked like “mystery load.” It was a blocking call. Flask would have used a thread and been honestly slow. FastAPI was politely stuck. I spent a week teaching “do not block the loop” that I would not have spent if we had stayed sync. That week was not faster.

What did not get faster: the parts that were never the framework

Tests that hit the database are still slow. uv does not make pytest and a fixture any kinder. We got faster installs around the tests. The tests themselves needed the same surgery they needed before: fewer end-to-end cases, more boundary tests, a real contract test for the webhook.

Deploys are still deploys. Image build, migrate, drain. uv shaves the dependency layer. It does not shave Kubernetes. Anyone who told you a packaging tool would make Friday safer was selling a feeling.

On-call is still on-call. Stack traces are cleaner when the validation layer fails loudly. They are the same mess when the downstream hangs. We added timeouts we should have added in Flask. Again: the migration was a deadline. The timeout was the actual fix.

Engineer at a whiteboard timeline during a late-afternoon standup

What got slower, honestly

The first month was slower. We were translating dicts into models and discovering every place a client sent extra fields we had been silently accepting. Strict models are a gift after they are a migration tax. I would do it again. I would budget the tax. I did not, and I snapped at a product manager about “just a rename.”

Staff UI got slower as a thought. We did not have Django. We had a JSON API and a React admin that was already there. If we had needed a new internal form in week two, FastAPI would have been the wrong place to grow HTML. We lucked into not needing that. Luck is not a strategy. I would not repeat this switch on a service whose next feature was an admin.

Hiring interviews got slightly weirder. Candidates who only knew Django asked where the admin was. Candidates who only knew FastAPI tutorials wanted to put everything in one process and call it async. I now ask one question I did not ask before: show me where you would put a blocking call. The answers are educational.

uv, without the mythology

What got faster with uv: resolve, install, the “new clone” path, CI cache behavior we could reason about. What did not: the design of the service, the quality of the tests, the discipline of pins. You can pin badly with a fast tool. You just pin badly sooner.

We kept a requirements.txt around for two weeks “just in case.” That was cowardice and it cost us a confusing onboarding. Delete the old toolchain. One command in the README. I say this as the person who left the extra file.

I did not move every repo. A tiny Flask webhook still uses uv now and still uses Flask. That hybrid is the adult version of the blog pairing. FastAPI where the public contract lives. uv wherever Python lives.

The numbers I will stand behind

I will not invent a universal speedup. On our pipeline, a cold install dropped from minutes I would leave the room for to seconds I would not. Endpoint scaffolding dropped from “plus a marshmallow schema I might forget” to “the model is the ticket.” Partner integration time dropped because the docs were not stale by default. User-facing latency did not drop until we moved the export. Incident time dropped on validation mistakes and not on downstream timeouts until we set timeouts.

Those are different columns. People mash them into “FastAPI is faster.” FastAPI was clearer. uv was faster at installing. The architecture work we bundled into the migration was faster at not dying on PDFs. If you only take one of those, take the one you actually need. If you need all three, budget all three.

The conversation I keep having after the switch

People hear “we moved to FastAPI” and ask if they should move their monolith. I tell them what moved for us: the public API surface and the installer. I tell them what did not: the data model, the warehouse jobs, the React admin. If their pain is an installer, they can take uv and leave Flask alone. If their pain is untyped JSON, they can take Pydantic at the edge of Flask and leave the rewrite for later. We bundled those because we had a window. A window is not a principle.

The other conversation is “so async was worth it.” Sometimes. For outbound HTTP we already wanted to overlap, yes. For “we are a modern shop,” no. I will not pretend every route needed async def. Some of our fastest-to-debug handlers are sync on purpose, sitting next to async ones, because the library behind them is a blocking brick and I would rather a thread than a stalled loop. FastAPI lets you mix. Using that mix is the mature version of the switch. Advertising “fully async” on a slide is the immature one.

What I would repeat, and what I would not

I would repeat: types at the HTTP boundary, OpenAPI as a byproduct, uv as the installer, worker split on day one of the rewrite, timeouts as part of the definition of done. I would not repeat: selling the rewrite as a latency project, leaving Poetry files “for rollback,” putting a sync SDK in an async route because the tutorial used httpx and we had a vendor client instead, promising the admin “later” if later is next month.

I would also not repeat doing this in the same quarter as a database migration. We did not, and that is why I still like the result. Two migrations is how you lose the plot and blame the framework.

A year later I still start new I/O APIs in this pairing when the team is Python. I still tell people the request will not get faster by itself. I still treat uv as the default installer even when the framework is not FastAPI. That split is the thing I wish I had said out loud on day one, before someone asked why the homepage TTFB had not moved and I had to say: it was never going to. The lockfile was.

More articles for you