Python and Django development for platforms that have to keep running

Experienced Django engineering for new builds and, just as often, for existing platforms: the codebase several teams have touched, the version that fell out of support, the page that got slow as the data grew. We work on what exists before proposing anything new.

Written for engineering leaders and product owners running a Django application in production.

A typical production Django system
  1. ClientsBrowserMobile appPartner APIs
  2. Web tierDjangoDRFGunicorn / UvicornStateless containers behind a load balancer
  3. Async workCeleryRedis / SQSEmails, imports, reports, integrations
  4. DataPostgreSQLRedis cacheS3
  5. PlatformAWS ECSTerraformCI/CD
OperateLogsMetricsError trackingBackups

Most Django work is on an existing platform

These are the situations teams usually bring. Each has a well-understood path forward.

  • An inherited codebaseThe original developers are gone, documentation is thin, and nobody wants to touch the billing module.
  • A version out of supportThe application runs on a Django or Python release that no longer receives security fixes.
  • Pages that slowed downIt was fast with a thousand rows. With ten million, list views and reports time out.
  • Background jobs you can’t trustCelery tasks fail silently, run twice, or back up during busy periods.
  • Deploys nobody enjoysManual steps, migrations run by hand, and no quick rollback.
  • Features that take too longBusiness logic is spread across views, models, signals, and templates, so every change is risky.

Django architecture that ages well

Django’s defaults are good. Most long-lived Django problems come from what grew around those defaults without a plan. A few decisions make the biggest difference over years:

  • Apps with real boundaries. Group code by business domain (billing, accounts, catalog), not by technical type, and keep cross-app imports deliberate. This is a modular monolith, and it keeps the option to extract a service later.
  • A home for business logic. Whether you prefer a service layer or disciplined model methods, pick one. Logic scattered across signals, serializers, and views is what makes changes unpredictable.
  • Background work designed for retries. Tasks should be idempotent, enqueued with transaction.on_commit so they never run against uncommitted data, and observable when they fail.
  • PostgreSQL used on purpose. Constraints, indexes, and transactions belong in the database. Use its features (partial indexes, JSONB, full-text search) where they remove application complexity.
  • Settings and secrets kept out of the code. Environment-based configuration, with secrets in a manager, makes environments reproducible.

None of this needs microservices (here is how we decide when a service is worth it). A well-structured Django monolith on containers handles far more traffic than most businesses will ever see.

Where Django performance actually goes

Measure before redesigning. In most slow Django applications the database explains the majority of the time.

Common Django performance problems
ProblemSymptomHow to confirmTypical fix
N+1 queriesList pages and API endpoints slow down as data growsQuery count per request in logs, APM, or the debug toolbarselect_related and prefetch_related; annotate instead of per-row queries
Missing or unused indexesFilters and sorts on large tables are slowEXPLAIN ANALYZE on the real query with production-like dataAdd targeted indexes, including partial or composite ones
Loading too muchHigh memory use and slow serializationProfile the view; check fields and rows returnedonly() / values(), pagination, and smaller serializers
Slow work in the requestRequests wait on email, PDFs, or external APIsTracing shows time outside the databaseMove it to a background task and return immediately
Repeated expensive readsThe same data recomputed on every requestIdentical queries across requestsPer-object or fragment caching with clear invalidation
Connection overheadLatency spikes under loadMany short-lived database connectionsPersistent connections or pooling: PgBouncer, or Django’s built-in pool option (Django 5.1+ with psycopg 3)

Upgrading an old Django version safely

Staying on an unsupported release is a security risk that grows every month. The upgrade itself is predictable if you take it one step at a time.

Django release support windows
  1. Django 3.2 LTS
  2. Django 4.2 LTS
  3. Django 5.0
  4. Django 5.1
  5. Django 5.2 LTS
  6. Django 6.0
  7. Django 6.1

Approximate dates from the Django project’s published release roadmap. Striped bars are out of support. Check djangoproject.com/download before planning.

An LTS-to-LTS upgrade path
  1. Inventory

    Django, Python, and every third-party package, with the versions each supports.

  2. Safety net

    Tests around critical flows; characterization tests where coverage is thin.

  3. Clear warnings

    Run tests with deprecation warnings on and fix them on the current version.

  4. One release at a time

    For example 3.2 → 4.2 → 5.2, each deployed and verified separately.

  5. Python alongside

    Django 5.x needs Python 3.10+; Django 6.0 needs 3.12+.

  6. Stay current

    Schedule upgrades on each LTS so this never becomes a project again.

Tools such as django-upgrade and pyupgrade automate many mechanical changes. The judgment is in the third-party packages: some have newer versions with breaking changes, and a few are abandoned and need replacing. Find those first, because they decide the real size of the upgrade.

An old Django version on its own is rarely a reason to rewrite. A large upgrade is almost always cheaper than replacing the application; we explain how to weigh the two in should you rewrite your application from scratch?

Failure modes worth designing for

Lessons that usually arrive in production. Each has a cheap prevention and an expensive cure.

  • Migrations that lock tablesCreating an index on a large table blocks writes. Use AddIndexConcurrently in a non-atomic migration, and split schema and data changes.
  • Tasks that run twiceRetries and at-least-once queues mean duplicates. Make tasks idempotent with unique keys or state checks.
  • Tasks that run too earlyA task enqueued inside a transaction can read data that isn’t committed yet. Enqueue with transaction.on_commit.
  • Signals as hidden control flowLogic triggered by signals is invisible at the call site and hard to test. Prefer explicit calls for business rules.
  • Admin as the operations toolThe Django admin is excellent for staff, but becomes slow and unsafe on large tables without list_select_related, pagination, and permissions.
  • Timezone and money mistakesKeep USE_TZ on, store UTC, and use Decimal with explicit rounding for currency.

Django production readiness checklist

  • Supported Django and Python versions
  • DEBUG off and secrets out of the repository
  • Migrations reviewed for locking behavior
  • Idempotent background tasks with monitoring
  • Database indexes checked against real queries
  • Error tracking and structured logs
  • Automated deploy with a tested rollback
  • Backups with a verified restore

How Yippify can help

Yippify can join an existing Django platform as additional engineering capacity, lead an upgrade or performance project, or build a new Django application or API. We start with a review of the code, versions, database, background jobs, and deployment, then agree on priorities with you before changing anything significant.

Questions teams ask

Can you take over an existing Django codebase?

Yes. The first step is a review of the code, dependencies, Django and Python versions, database, background jobs, deployment, and test coverage, followed by a written summary of what is healthy, what is risky, and where to begin.

How do you upgrade an old Django version safely?

One supported release at a time, usually from LTS to LTS. Run the test suite with deprecation warnings enabled, fix warnings before upgrading, update third-party packages, and deploy each step separately. Where tests are thin, add characterization tests around the most important flows first.

Which Django version should we be on?

A version that still receives security updates. Django 5.2 is the current long-term support release, supported until April 2028 according to the Django project’s roadmap. Django 4.2 LTS reached end of support in April 2026, and older versions no longer receive security fixes.

Why is our Django application slow?

The most common causes are database access patterns (N+1 queries, missing indexes, loading more rows or columns than needed), slow work done inside the request instead of in a background job, and missing caching. Measure first with query logging, APM, or EXPLAIN ANALYZE before changing architecture.

Django or FastAPI for a new API?

Django with Django REST Framework is a strong default when you need an admin, authentication, an ORM, and a large ecosystem. FastAPI suits smaller, async-heavy services with a narrow scope. Many teams run both: Django for the core product and a small FastAPI service where it fits.

Do you work with Celery, PostgreSQL, and AWS?

Yes. Typical production Django systems include PostgreSQL, Redis, Celery or another task queue, object storage, and containerized deployment on AWS with Terraform and CI/CD.

Need experienced Django help on an existing platform? Talk with Yippify.

Tell us which Django and Python versions you’re on and what hurts most right now: speed, stability, upgrades, or getting features out. We’ll suggest where to start.

  • Review before rewrite
  • Upgrades done one safe step at a time
  • Performance fixed from measurements
Talk about our Django platform

A rough description is enough to start. No specification needed.