A typed boundary, a safe migration, and a deploy on merge.

Python has a reputation on the server: the place where data science leaks into production. It is a fair reputation, and it is earned by engineering rather than by the language. A Python service can be exactly as well built as a Node one — validated at the edge, tested around what carries money and data, migrated deliberately, observable, and shipped when the branch merges.

That is the standard we hold it to, and it is the same standard on every page of this site. The three failures below are the ones we are called in for most, and none of them is about the language.

FACT 01

PanoptesAI fuses acoustic, RF, electro-optical/infrared and radar into one validated track — detection to handoff in four seconds. Its case page names the stack, and FastAPI is in it.

FACT 02

A membership platform we built runs on Python and Django with a React front end over PostgreSQL and MongoDB — the Django half of this page, named on its own case page rather than asserted here.

FACT 03

We work in Flask a great deal, and no case page on this site names it — most of that work is internal tooling and integration services that never become published case studies. So the third column below tells you what we do with it rather than pointing at evidence we do not have.

Watch a table lock ↓

30 minutes, working session. Bring the endpoint you are least happy with and the schema change you have been avoiding; you leave with a one-page read either way. Nobody follows up more than once.

The PanoptesAI tracking map: a detection cone and coverage radius drawn over a map, with an active-targets panel listing one tracked object by identifier, position, altitude, speed and heading
Active targets — PanoptesAIIdentifier, position, altitude, speed, heading, status — every field in that panel is a typed response from a service, arriving fast enough to be worth drawing. The case page names FastAPI in the stack. PanoptesAI case →

Six ways a Python service goes wrong, and what is actually causing each

Read the left column and you will recognise at least two of them. The right column is the part that gets skipped in the meeting where somebody proposes rewriting the service in another language.

SYMPTOM 01

Fast on the laptop, unusable on the real table

CAUSE

An ORM relationship that reads like one line and issues four hundred statements, or a filter that has no index behind it. Staging has ten thousand rows and production has ten million, so the plan flips from an index scan to a sequential one and nobody was watching. Nobody has read the query plan, which is a five-minute job that ends the argument.

SYMPTOM 02

The schema change that queued every write for twenty minutes

CAUSE

A naive ALTER on a large table, deployed at ten in the morning, taking an exclusive lock while it rewrites. It is now folklore, and every schema change since has been deferred — which is how a team ends up with a data model it has outgrown and is afraid to touch. The instrument below is this exact event.

SYMPTOM 03

The async service that is secretly synchronous

CAUSE

One blocking library call inside an async handler — a client that was never awaitable, a file read, a CPU-bound loop. Under light load nothing shows. Under real concurrency the whole worker stalls behind it, the mean latency barely moves and the tail turns into a saw. Blocking work belongs in a thread pool, and the pool has to be sized against worker concurrency rather than guessed.

SYMPTOM 04

The build that worked last month

CAUSE

No lock file, a requirements list of unpinned names, and a transitive dependency that shipped a release on Tuesday. The image on the laptop and the image in the pipeline are two different pieces of software that happen to share a name. Pinned and hashed dependencies turn this from a weekly surprise into a decision somebody makes on purpose.

SYMPTOM 05

The background job that stopped on Tuesday

CAUSE

A loop in a process nobody supervises: no retries, no backoff, no dead-letter path, no alert. Somebody noticed on Friday because a customer asked. Background work is a system with its own failure modes, and it needs the same treatment as the request path — idempotency so a replay does not duplicate records, and an alert into a channel a person actually reads.

SYMPTOM 06

Nobody can say which model produced last Tuesday’s output

CAUSE

The model is loaded at import, held in memory per worker and versioned with the application, so changing it is a deploy and reverting it is a redeploy. Once the model has its own version, its own storage and its own rollback, the data team stops waiting on the release train and the service stops carrying a payload it cannot describe.

Two ways to add a column, drawn on the same clock

A table taking steady writes, and one new required column. Above, the change everybody writes first. Below, the same change by expand and contract. Scrub through two minutes and watch the queue: the second one is not cleverer, it is simply four small steps instead of one long one.

SCHEMATIC — ILLUSTRATIVE TIMINGS, NO CLIENT FIGURES T + 000 s
0 s
Naive ALTER — writes waiting
0

Nothing has happened yet. The table is taking its usual writes and every one of them is served.

Expand and contract — writes waiting
0

The same table, the same load, the same new column. Four steps instead of one, each of them short enough that nothing queues behind it.

The point is not that one is difficult. Adding a nullable column, backfilling it in batches, adding the constraint once the data is there and dropping the old path afterwards is ordinary work with a checklist. The point is that a migration is the one change you cannot undo by redeploying, so it is the one that gets reviewed hardest — and once a team has a procedure for it, schema changes stop being deferred.

A street scene with buildings highlighted and a footfall-trend chart overlaid — an illustration of location analytics
Volume with a privacy constraint — location intelligence

Insight without the individual, at a size where the naive step never finishes

Anonymised mobile-location data turned into footfall and behaviour insight for a location-intelligence company. Python is named in the stack on its case page. The hard part is that the aggregation has to hold a privacy constraint at a data volume where a single-pass pandas step simply does not return — so the shape of the pipeline is decided by memory and by batching long before anyone argues about the language. Location intelligence case →

Three names in the title, and an honest weight behind each

The useful version of this section is the one that tells you where the evidence is. All three are in daily use here. Two of them have a published case page naming them and one does not, and that asymmetry is written below rather than smoothed over — because on a page like this, a claim you can check is worth more than a claim you cannot. What does not vary is the engineering standard: the framework decides the idioms, not the bar.

FastAPI
NAMED ON A CASE PAGE

Where new services go by default: Pydantic models as the boundary contract, request validation that produces an error a client can act on, and an OpenAPI document generated from the same types the code runs on, so it stays truthful instead of drifting into fiction. It is in the published stack for PanoptesAI, alongside Python, OpenCV, deep learning and sensor fusion.

The async model is the reason to choose it and the reason to be careful with it — see the third symptom above.

Django
NAMED ON A CASE PAGE

Where a product already runs on it, and where the admin, the auth, the ORM and the migration framework are worth more than the freedom to assemble your own. We built a membership platform on Python and Django with a React and TypeScript front end over PostgreSQL and MongoDB, integrated with Webflow.

A large Django application is the normal end state of a successful one. It gets split module by module, with tests pinning the behaviour that has to stay identical.

Flask
HEAVILY USED · NOT ON A CASE PAGE

We work in Flask a great deal. Most of it is the software that never becomes a case study: internal tooling, integration services, webhook receivers, admin surfaces and the layer wrapped around somebody’s model — load-bearing for the client, and invisible from the outside.

It earns its place where the surface is small and the team wants to see the whole application at once, and where an existing Flask codebase is doing its job and needs maintaining rather than replacing. On a new service that will grow a large typed API surface, we would say what FastAPI’s generated schema buys you by month three before you commit.

The answer to “which framework will you use” is: whichever your team can still maintain after we leave. A framework nobody in-house can read is a liability wearing the costume of an upgrade.

A frame from a PanoptesAI field test: a flock of birds against an overcast sky, each one boxed and labelled with a track identifier and a confidence score
Four confidence semantics, one output — PanoptesAIEvery box here is a candidate with an identifier and a score, and the service behind them has to reconcile acoustic, RF, electro-optical/infrared and radar — four instruments that disagree in different ways — into one track an operator can act on. Detection to handoff in four seconds. PanoptesAI case →

Four systems, and what was hard about each

In every one of these, Python is the service around something that has to be right and fast. Each has a case page behind it, except the last, which is named honestly for what it is.

Location intelligence
Python · SQL
Privacy-safe

Aggregation that has to hold a constraint at volume

Anonymised mobile-location data turned into footfall and behaviour insight for a location-intelligence company. The hard part is volume with a privacy constraint attached: the output has to be genuinely useful about a place while being unable to say anything about a person, at a data size where the obvious single-pass implementation does not finish. That is a memory and batching problem long before it is a framework one. Location intelligence case →

PanoptesAI
FastAPI · sensor fusion
Four seconds

Reconciling instruments that each disagree differently

Acoustic, RF, electro-optical/infrared and radar fused into one validated track, detection to handoff in four seconds. The hard part is that four sensors produce four different kinds of confidence, and an operator needs one object with one status rather than four opinions. The service in front of that is a FastAPI service, and the console in this page’s first screen is what it serves. PanoptesAI case →

AI video agent
Perception service
Shared budget

A Python service inside somebody else’s latency budget

Real-time distance and head-angle tracking running beside speech recognition, speech synthesis and a 3-D character in a browser. We prototyped on OpenCV, then benchmarked two pipelines rather than picking by reputation, and shipped the fastest available. The hard part is that this service does not own its budget — it shares a few hundred milliseconds with everything else in the experience, so “fast enough” is defined by neighbours it does not control. Face-tracking case →

Operational pipeline
n8n · LLM classification
300+ / week

The one where the right answer was not a Python service

CRM, email, project management and reporting joined into one automated flow, with a model on the judgement steps only: 300+ records a week automated, roughly 80% less manual entry in month one. It is on this page because it is built from the same unglamorous half that a good background worker is — retries with backoff, idempotency so a replay does not duplicate a record, dead-letter handling and an alert somebody reads. And because it is honest to say that we reached for a workflow tool rather than writing a service, which is sometimes the correct call. Process automation →

Bring one slow endpoint and one deferred migration to the Data Flow Audit. Those two answer most of what we would otherwise spend a week discovering.

Four people around a kitchen table at the end of a working day, cake and tea in front of them, all turned toward the camera

What a Python service of ours has on the day it ships

These are not options on a quote. They are what the work includes, and the reason the second year of the service costs what the first one did.

THE BOUNDARY

Pydantic models as the contract, validation that produces an error a client can act on, an OpenAPI document generated from the same types the code runs on, pagination that survives inserts, idempotency keys where a retry could duplicate something, and versioning that lets an old client keep working after today’s deploy.

THE SCHEMA

Migrations reviewed as carefully as code, because they are the only change a redeploy cannot undo. Expand and contract as the default shape, backfills in batches, and a written procedure your team can follow the week after we finish.

THE RUNTIME

Gunicorn or Uvicorn worker models chosen rather than copied, memory per worker measured so the instance size is a decision, connection pools sized against worker concurrency, timeouts on every outbound call, and blocking work moved off the loop into a pool that is sized for it.

THE BUILD

Pinned and hashed dependencies with a lock file in the repository, a virtual environment in CI, wheels rather than surprise source builds in the image, and a container that is the same piece of software on a laptop and in the pipeline.

THE TESTS

Coverage concentrated where money and data move rather than spread evenly for a number. Behaviour pinned before anything is refactored, and a suite stable enough that a red build still means something in month six.

THE SIGNALS

Structured logs with a correlation id that survives a queue hop, metrics with cardinality you can afford, and alerts tuned so people still read them in month three. A senior architect signs off the design, and you talk to the engineers doing the work rather than to an account layer.

The photograph above is from the membership-platform build — the Django half of this page, and the end of a working day with the client’s team. Relationship-support platform case →

Two people from Fluvius at a developer conference, conference lanyards on, the hall lit blue behind them
Where the arguments are still live

Opinions with a date on them

Nearly everything on this page is a position that was correct at some point and will be revised. Async clients that were unusable three years ago are fine now; a packaging tool that was a curiosity is the default; the advice about the GIL changed underneath everyone. We keep the opinions current by being where they are argued out, and by rebuilding our own things often enough to notice when one goes stale. About Fluvius →

The service in front of a model, and the line we draw before starting

This is where most of our Python lives, and it is also where the most territory gets defended. So the boundary gets written down in week one: your data or ML people own the model and the method; we own everything between it and the product.

VERSIONED SEPARATELY

The model has its own version, its own storage and its own rollback, so changing it is not a deploy and reverting it is not a redeploy. After that, every output can be traced to the version that produced it — which is the question somebody asks eventually, usually under pressure.

WARM, AND MEASURED

Load time, memory per worker and the first-request penalty all measured rather than assumed, because together they decide the instance size and therefore the bill. Batching where the traffic shape rewards it, and left alone where it only adds latency.

THE CONTRACT

A typed request and response between the product and the model, so the data team can change what is inside without breaking a client, and the product team can read the schema instead of the notebook.

WHOSE WORK IS WHOSE

We do not rewrite anyone’s model on principle, and we do not port it to another language to feel better about it. Their work stays theirs; we build the boundary, the versioning, the deployment and the monitoring around it, and we write that division down before anybody starts.

If the deeper question is about the model itself rather than the service around it, that is the neighbouring page: PyTorch, TensorFlow & Pandas covers training, evaluation and what a model costs to own.

The work, as we actually sell it

Timelines are indicative ranges and depend on scope. Every one of these starts small enough to cancel, and each produces something you keep.

A Python service built properly

6–14 WEEKS

Typed boundary, generated and truthful OpenAPI, tests around what carries money and data, migrations reviewed like code, and a deploy that happens when the branch merges. Delivered as a working slice inside your product rather than as a standalone repository.

The slow endpoint, with numbers

2–6 WEEKS

Query plans read on your data, N+1 removed, select_related and prefetch_related used deliberately, and indexes added where they earn their write cost. You get the before-and-after measurements rather than an assurance.

Zero-downtime schema change

2–5 WEEKS

Expand and contract as a repeatable pattern, backfills that run in batches without holding a lock, and a written procedure your team follows on the next change without us in the room.

Async, done deliberately

2–6 WEEKS

Finding the blocking calls hiding inside async handlers, moving them where they belong, sizing pools against worker concurrency so the database is not exhausted, and setting the outbound timeouts nobody set.

The service in front of a model

4–10 WEEKS

Model versioned separately from code, warm-up and memory per worker understood, batching where it helps, and a rollback that does not require redeploying the application to reach.

Background work that survives

2–6 WEEKS

Retries with backoff, idempotency so a replay is safe, dead-letter handling with somewhere for a failed job to go, and alerting into a channel your team actually reads. RabbitMQ where the routing and the delivery guarantees matter, Celery or RQ for Python workers, and BullMQ over Redis where the workers are Node — which is common on a mixed team and no reason to run two brokers.

Staged modernisation of a large Django app

12–26 WEEKS

Module by module, with tests pinning the behaviour that has to stay identical, and the system in service the whole way through. Length depends on surface area, and the first module is deliberately small.

A reproducible build

1–3 WEEKS

Pinned and hashed dependencies, a lock file in the repository, a clean environment in CI and an image that stops depending on a system library nobody declared. The shortest engagement here and often the one that stops the most weekly noise.

What you are probably thinking

“Python is too slow for this.”
Sometimes true, and when it is we will say so rather than sell you a rewrite of the wrong thing. Far more often the service is slow because of one blocking call, one missing index or one N+1 — and we would rather show you the query plan than have the language argument. If the profile really does point at the language, that is a finding worth having in writing.
“Our Django app has grown into a monolith.”
That is the normal end state of a successful Django app rather than a moral failure. It gets split module by module with tests pinning the behaviour that has to stay identical, and it stays in service the whole way through. The first module is chosen for being boring and well understood, not for being the most painful.
“You’ll rewrite our data scientists’ work.”
No. Their work stays theirs. We build the service around it — boundary, versioning, deployment, monitoring — and we write that division down before we start, so nobody is defending territory in month two. Porting a model to another language to feel better about it is not on the table.
“Which framework will you use?”
Whichever your team can still maintain. If you already run Django, we work in Django; if it is Flask, we work in Flask, and we do a great deal of that. New services default to FastAPI for the typed boundary and the generated schema, unless the surface is small enough that the ceremony costs more than it returns. A framework nobody in-house can read is a liability rather than an upgrade.
“We have been putting off a schema change for a year.”
That is usually one bad morning still doing damage. The fix is a procedure rather than courage: expand and contract, a backfill in batches, the constraint added once the data is already there, and a rehearsal on a copy at production size. After the first one lands quietly, the backlog behind it moves.
“We tried an outside dev shop and it went badly.”
Usually the same three causes: no senior person accountable, a demo-grade codebase, and a handover that never happened. Here you get direct access to the engineers doing the work, no account-manager layer, a senior architect signing off every project, and tests and documentation as part of the deliverable.
“You’re in Europe.”
US entity — Fluvius USA Inc, Sacramento, California — with US contracts and US payment terms. EU hours with US-morning overlap, so there is a written handoff waiting at the start of your day and an overlap window for the conversation that needs one.

The record behind the pages

200+
Clients served
10+
Years of happy clients
7+
Years our longest clients have stayed

Many of our clients have been with us for 7+ years straight. The Upwork and Clutch records are independently verifiable.

Looking for this as an engagement rather than a stack? Full-Stack Web Development sells the team and the outcome, and Legacy Modernization covers the staged split of a large application. This page is the tool-level view underneath both. See also all our work.

Not ready to talk? Take the checklist.

One page: 14 questions about your Python service you should be able to answer in under a minute. Whether your dependencies are pinned and hashed. Which async handlers contain a blocking call. Whether the pool size matches worker concurrency. Which migrations take a lock and how long they hold it. Where the model version lives. What happens to a background job that fails at three in the morning. And which endpoints have no timeout on their outbound calls.

Bring the endpoint you like least, and the migration you keep postponing.

A free 30-minute working session, not a sales call and not a demo — there is nothing to demonstrate. Show us the slow path and the schema change you have been avoiding, and we tell you what we would do first and in what order.

You keep a one-page read either way: the likely cause of the slow path with the measurement we would take to confirm it, the safe shape of the migration you have been deferring, and the one test we would write before touching anything. Nobody follows up more than once.

RELATED SERVICES → AI Integration AI Business Process Automation Chatbots & RAG Data Engineering & AI Data Readiness All services