When not to use Python: a practical Python Backend guide
After 90 days of building production systems with Python, you've learned its strengths: rapid development, readable syntax, and a mature ecosystem for APIs and data pipelines. But Python isn't the right tool for every job. Understanding the trade-offs—CPU-heavy hot paths, latency constraints, team dynamics, and scaling challenges—separates pragmatic engineers from those who reach for Python out of habit rather than fit.
CPU-heavy hot paths may need Go or Rust
Python's Global Interpreter Lock (GIL) prevents true parallelism in CPU-bound workloads. If your backend serves requests that require heavy numerical computation, image processing, or cryptographic operations on every request, you'll hit a wall where adding more threads or processes gives you diminishing returns. This isn't theoretical—we've seen it in production with ride-hailing backends processing location matrices and trading systems computing risk models.
Consider a scenario where your API receives 100 concurrent requests, each needing 500ms of CPU time to compute a machine learning model inference, feature engineering, or signal processing. With Python:
import time
from threading import Thread
def cpu_heavy_task():
"""Simulates 500ms of CPU work"""
total = 0
for i in range(50_000_000):
total += i ** 2
return total
# Sequential execution
start = time.time()
for _ in range(100):
cpu_heavy_task()
duration = time.time() - start
print(f"Sequential: {duration:.2f}s for 100 requests")
# Output: ~50 seconds (500ms × 100)You might think: "I'll just use threading or asyncio." But threading doesn't help here—the GIL ensures only one thread executes Python bytecode at a time. Asyncio is purely I/O-bound. You'd need multiprocessing, which adds serialization overhead and per-process memory costs. At 100 concurrent requests, you're looking at spawning dozens of processes, each consuming 50–100MB. On a 4GB container, you've already hit your limit.
Go or Rust handle this elegantly because they compile to native code with true parallelism:
- Go: Goroutines are lightweight, thousands cost minimal overhead, and the runtime handles scheduling. A Go service processes those 100 concurrent CPU tasks in ~5 seconds.
- Rust: Zero-cost abstractions and no GIL means you control parallelism precisely. Same workload, ~5 seconds, with lower memory footprint than Go.
When to stick with Python: if your hot path is I/O-bound (database queries, external API calls, file operations), Python's async/await and connection pooling will serve you well. When to switch: if profiling shows CPU time dominates, and you can't offload that work to a specialized service (like a C extension, or a separate worker tier), that's your signal to consider compiled languages for that specific component.
Very low-latency systems need tuning
If your SLA demands sub-10ms response times on every request, Python becomes friction. Garbage collection pauses, bytecode interpretation, and dynamic typing checks accumulate. We've deployed SaaS platforms and CRM backends in Python where average latency is 50–100ms—perfectly acceptable. But for high-frequency trading, real-time bidding, or autonomous systems, you need microsecond-level predictability.
Python's garbage collector can pause your entire application. Here's what happens:
import gc
import time
# Simulate allocation and GC pause
start = time.time()
gc.disable()
data = [list(range(10000)) for _ in range(100000)]
alloc_time = time.time() - start
gc.enable()
start = time.time()
del data # Triggers collection
gc.collect()
gc_time = time.time() - start
print(f"Allocation: {alloc_time*1000:.2f}ms, GC pause: {gc_time*1000:.2f}ms")
# Output: Allocation: 500ms, GC pause: 150msA 150ms garbage collection pause is devastating in a trading system where markets move milliseconds. You can tune GC thresholds, use generators to reduce memory pressure, or deploy across multiple processes—but you're fighting the language's design.
Latency-critical systems typically use:
- C++ or Rust: Full control over memory, no surprise pauses, deterministic performance.
- Go: Predictable GC pauses (usually 1–10ms), faster baseline latency than Python, easier concurrency.
- Java with tuning: JIT compilation can outperform Python for CPU-heavy code after warmup.
Python can work in lower-latency environments if latency-critical paths are small and delegated to C extensions (NumPy, pandas internals do this well) or if you accept higher tail latencies (p99 instead of p50).
Large teams need strong typing discipline
Python's dynamic typing is a feature when your team is 2–3 senior engineers. At 20+ engineers, it becomes a liability. Without strong typing, refactoring is risky—rename a database column, and you won't know if that change breaks code until tests run or production fails. We've seen e-commerce backends lose data because a float/string conversion happened silently in Python where a typed language would have caught it at compile time.
Type hints exist in Python, but they're optional and unenforced at runtime:
from typing import Optional
def process_payment(amount: float, currency: str) -> bool:
"""Type hint, but nothing stops you from calling this wrong"""
return True
# These don't raise errors at definition time
result = process_payment("not-a-float", 42) # Should fail, but doesn't until you call itYou can enforce types with mypy, Pydantic, or Pyright, but adoption across a large team requires discipline. TypeScript enforces this at compile time; Python enforces it through tooling, testing, and code review. That's overhead a 50-person team can't sustain consistently.
For large teams, consider:
- TypeScript/JavaScript: Types are enforced pre-deployment, refactoring is safer, the ecosystem is massive.
- .NET (C#): Strong typing, excellent tooling, and a mature enterprise ecosystem.
- Go: Simpler than Java, but enforces structure and typing clearly.
Python works fine for large teams if you enforce strict tooling (mypy in strict mode, Pydantic for validation, comprehensive test coverage) and establish a strong code review culture. But it requires discipline; typed languages make it the default.
For most APIs, Python is fast enough
Here's the truth we need to underline: for the majority of web APIs, Python is genuinely fast enough. If you're building REST endpoints backed by PostgreSQL, Redis, and external services, your bottleneck is I/O, not Python's execution speed. A well-written FastAPI service handles thousands of requests per second. We've deployed systems at scale that prove this.
Real-world benchmark (FastAPI on a 4-core machine):
| Scenario | Requests/sec | Avg Latency | Language |
|---|---|---|---|
| Simple JSON response | 8,000+ | 1–2ms | Python (FastAPI) |
| Database query + JSON | 2,000–3,000 | 5–10ms | Python (FastAPI + asyncpg) |
| External API call + transform | 500–1,000 | 100–300ms | Python (bottleneck: external service) |
Most teams don't hit the CPU or latency ceiling. They hit scaling concerns at 10,000+ rps, at which point they add caching, database read replicas, and horizontal scaling—all orthogonal to language choice. Python scales horizontally as well as any other language.
Common mistakes
Mistake 1: Assuming Python is always slow. It's not. Profiling your actual code matters. We've seen teams reject Python for latency concerns without measuring. Nine times out of ten, a database query is the bottleneck, not the Python code.
Mistake 2: Choosing Go or Rust prematurely. Both add complexity. Go's simplicity is real, but you lose the ecosystem and expressiveness of Python. Rust's safety guarantees are powerful but slow down development. Don't optimize for speed before you have a concrete problem.
Mistake 3: Mixing Python and another language poorly. If you decide your hot path needs Go, use it for that specific microservice. Don't half-write it in Python and half in Go. Clear ownership makes operations manageable.
Mistake 4: Ignoring team expertise. If your team knows Python but not Go, shipping a Go service introduces operational risk. The best language is often the one your team can maintain confidently.
Key takeaways
- Python's GIL blocks CPU-bound parallelism; if profiling shows CPU time dominates your hot path, consider Go or Rust for that component.
- Very low-latency systems (trading, real-time bidding) need compiled languages or C extensions; Python's GC pauses are incompatible with microsecond SLAs.
- Large teams (20+ engineers) benefit from language-enforced typing; Python's optional typing requires discipline and tooling overhead.
- For typical web APIs, Python is fast enough; I/O is almost always the bottleneck, not language speed.
- Make the language choice based on profiling and concrete constraints, not assumptions. Premature optimization wastes time; missing a real constraint costs production incidents.
- If you do use multiple languages, keep boundaries clean: one service per language, clear APIs, separate deployment pipelines.






Comments
Loading comments…