How It Works Features Pricing Blog
Log In Start Free Trial
Django · Python

Fix RecursionError: maximum recursion depth exceeded in Django

This error occurs when a function calls itself infinitely, exceeding Python's default recursion limit of 1000 frames. In Django, this commonly happens with overridden save() methods that call self.save(), circular signal handlers, or serializers with nested relationships. Fix it by breaking the cycle with conditional checks or using update() instead.

Reading the Stack Trace

Traceback (most recent call last): File "/app/venv/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner response = get_response(request) File "/app/venv/lib/python3.11/site-packages/django/core/handlers/base.py", line 197, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/app/src/views.py", line 18, in update_profile profile.save() File "/app/src/models.py", line 32, in save self.save() File "/app/src/models.py", line 32, in save self.save() File "/app/src/models.py", line 32, in save self.save() [Previous line repeated 995 more times] RecursionError: maximum recursion depth exceeded

Here's what each line means:

Common Causes

1. Overridden save() calling self.save() instead of super().save()

The model overrides save() to add custom logic but calls self.save() instead of super().save(), causing infinite recursion.

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    updated_at = models.DateTimeField(auto_now=True)

    def save(self, *args, **kwargs):
        self.bio = self.bio.strip()
        self.save(*args, **kwargs)  # Bug: should be super().save()

2. Circular post_save signal

A post_save signal handler modifies and saves the same model, triggering the signal again in an infinite loop.

from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=Profile)
def update_profile_timestamp(sender, instance, **kwargs):
    instance.updated_at = timezone.now()
    instance.save()  # Triggers post_save again

The Fix

Replace self.save() with super().save() to call the parent class's save method instead of recursively calling the overridden method. This preserves the custom logic while correctly persisting to the database.

Before (broken)
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    updated_at = models.DateTimeField(auto_now=True)

    def save(self, *args, **kwargs):
        self.bio = self.bio.strip()
        self.save(*args, **kwargs)
After (fixed)
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    updated_at = models.DateTimeField(auto_now=True)

    def save(self, *args, **kwargs):
        self.bio = self.bio.strip()
        super().save(*args, **kwargs)

Testing the Fix

import pytest
from django.contrib.auth.models import User
from models import Profile


@pytest.mark.django_db
def test_profile_save_does_not_recurse():
    user = User.objects.create_user(username='alice', password='testpass')
    profile = Profile(user=user, bio='  Hello World  ')
    profile.save()  # Should not raise RecursionError
    profile.refresh_from_db()
    assert profile.bio == 'Hello World'


@pytest.mark.django_db
def test_profile_save_strips_whitespace():
    user = User.objects.create_user(username='bob', password='testpass')
    profile = Profile(user=user, bio='  Spaces everywhere  ')
    profile.save()
    profile.refresh_from_db()
    assert profile.bio == 'Spaces everywhere'


@pytest.mark.django_db
def test_profile_save_updates_existing():
    user = User.objects.create_user(username='carol', password='testpass')
    profile = Profile.objects.create(user=user, bio='Original')
    profile.bio = '  Updated  '
    profile.save()
    profile.refresh_from_db()
    assert profile.bio == 'Updated'

Run your tests:

pytest

Pushing Through CI/CD

git checkout -b fix/recursion-error-save,git add src/models.py tests/test_models.py,git commit -m "fix: call super().save() instead of self.save() to prevent recursion",git push origin fix/recursion-error-save

Your CI config should look something like this:

name: CI
on:
  pull_request:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: test_db
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install -r requirements.txt
      - run: pytest --tb=short
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db

The Full Manual Process: 18 Steps

Here's every step you just went through to fix this one bug:

  1. Notice the error alert or see it in your monitoring tool
  2. Open the error dashboard and read the stack trace
  3. Identify the file and line number from the stack trace
  4. Open your IDE and navigate to the file
  5. Read the surrounding code to understand context
  6. Reproduce the error locally
  7. Identify the root cause
  8. Write the fix
  9. Run the test suite locally
  10. Fix any failing tests
  11. Write new tests covering the edge case
  12. Run the full test suite again
  13. Create a new git branch
  14. Commit and push your changes
  15. Open a pull request
  16. Wait for code review
  17. Merge and deploy to production
  18. Monitor production to confirm the error is resolved

Total time: 30-60 minutes. For one bug.

Or Let bugstack Fix It in Under 2 minutes

Every step above? bugstack does it automatically.

Step 1: Install the SDK

pip install bugstack

Step 2: Initialize

import bugstack

bugstack.init(api_key=os.environ["BUGSTACK_API_KEY"])

Step 3: There is no step 3.

bugstack handles everything from here:

  1. Captures the stack trace and request context
  2. Pulls the relevant source files from your GitHub repo
  3. Analyzes the error and understands the code context
  4. Generates a minimal, verified fix
  5. Runs your existing test suite
  6. Pushes through your CI/CD pipeline
  7. Deploys to production (or opens a PR for review)

Time from error to fix deployed: Under 2 minutes.

Human involvement: zero.

Try bugstack Free →

No credit card. 5-minute setup. Cancel anytime.

Deploying the Fix (Manual Path)

  1. Run pytest locally to confirm save() works without recursion.
  2. Open a pull request with the super().save() fix.
  3. Wait for CI checks to pass on the PR.
  4. Have a teammate review the model's save method.
  5. Merge to main and verify profile updates work correctly in staging.

Frequently Asked Questions

BugStack tests the save() method with multiple scenarios, verifies no RecursionError is raised, and checks that the custom logic (like stripping whitespace) still works.

All fixes are submitted as pull requests with CI validation. Your team reviews model changes to ensure data integrity before merging.

Use Model.objects.filter(pk=instance.pk).update(field=value) in the signal handler instead of instance.save(). The update() method does not trigger signals.

No. Increasing sys.setrecursionlimit() masks the bug and risks crashing the process with a segfault. Always fix the root cause of the infinite recursion.