How It Works Features Pricing Blog Error Guides
Log In Start Free Trial
Celery · Python

Fix EncodeError: kombu.exceptions.EncodeError: Object of type datetime is not JSON serializable in Celery

This error occurs when a Celery task argument or return value contains a type that cannot be serialized with the configured serializer (default: JSON). Python datetime objects, SQLAlchemy models, and other complex types are not JSON-serializable. Fix it by converting arguments to JSON-safe types like strings, dicts, or IDs before passing them to tasks.

Reading the Stack Trace

Traceback (most recent call last): File "/app/app/routes.py", line 18, in schedule_reminder send_reminder.delay(user.id, reminder_time) File "/app/venv/lib/python3.12/site-packages/celery/app/task.py", line 425, in delay return self.apply_async(args, kwargs) File "/app/venv/lib/python3.12/site-packages/celery/app/task.py", line 575, in apply_async return app.send_task( File "/app/venv/lib/python3.12/site-packages/kombu/serialization.py", line 220, in dumps payload = encoder(data) File "/app/venv/lib/python3.12/site-packages/kombu/utils/json.py", line 65, in dumps return _dumps(s, cls=cls or _default_encoder, **dict(default_kwargs, **kwargs)) File "/usr/lib/python3.12/json/encoder.py", line 180, in default raise TypeError(f'Object of type {o.__class__.__name__} is not JSON serializable') kombu.exceptions.EncodeError: Object of type datetime is not JSON serializable

Here's what each line means:

Common Causes

1. Passing datetime objects to tasks

A datetime object is passed as a task argument but the JSON serializer cannot encode it.

from datetime import datetime

@app.route('/remind', methods=['POST'])
def schedule_reminder():
    reminder_time = datetime.utcnow()
    send_reminder.delay(user.id, reminder_time)  # datetime not serializable

2. Passing ORM model instances to tasks

A SQLAlchemy model instance is passed to a task instead of its ID, but model objects are not serializable.

send_welcome_email.delay(user)  # User object, not user.id

3. Task returns a non-serializable object

The task's return value contains a complex object that cannot be serialized for the result backend.

@celery.task
def get_report():
    return {'generated_at': datetime.utcnow(), 'data': [...]}  # datetime in return

The Fix

Convert datetime objects to ISO 8601 strings with .isoformat() before passing them to Celery tasks, then parse them back with datetime.fromisoformat() inside the task. The same pattern applies to any non-serializable type: pass primitive values or IDs, not complex objects.

Before (broken)
@app.route('/remind', methods=['POST'])
def schedule_reminder():
    reminder_time = datetime.utcnow()
    send_reminder.delay(user.id, reminder_time)
After (fixed)
@app.route('/remind', methods=['POST'])
def schedule_reminder():
    reminder_time = datetime.utcnow().isoformat()
    send_reminder.delay(user.id, reminder_time)

@celery.task
def send_reminder(user_id, reminder_time_iso):
    reminder_time = datetime.fromisoformat(reminder_time_iso)
    # ... process reminder

Testing the Fix

import pytest
from datetime import datetime
from app.tasks import send_reminder
from app import create_app

@pytest.fixture
def app():
    app = create_app()
    app.config['CELERY_ALWAYS_EAGER'] = True
    app.config['TESTING'] = True
    return app

def test_task_accepts_iso_string(app):
    with app.app_context():
        result = send_reminder(1, datetime.utcnow().isoformat())
        assert result is not None

def test_task_rejects_raw_datetime(app):
    # Verify that passing a raw datetime raises during serialization
    with pytest.raises(Exception):
        send_reminder.delay(1, datetime.utcnow())

def test_iso_roundtrip():
    original = datetime(2026, 4, 10, 14, 30, 0)
    iso = original.isoformat()
    parsed = datetime.fromisoformat(iso)
    assert original == parsed

Run your tests:

pytest tests/ -v

Pushing Through CI/CD

git checkout -b fix/celery-serialization-datetime,git add app/routes.py app/tasks.py tests/test_tasks.py,git commit -m "fix: convert datetime to ISO string before passing to Celery task",git push origin fix/celery-serialization-datetime

Your CI config should look something like this:

name: CI
on:
  pull_request:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v --tb=short
        env:
          CELERY_BROKER_URL: redis://localhost:6379/0

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 tasks accept serialized arguments.
  2. Open a pull request with the serialization fix.
  3. Wait for CI checks to pass on the PR.
  4. Have a teammate review and approve the PR.
  5. Merge to main and verify tasks execute correctly in staging.

Frequently Asked Questions

BugStack tests task calls with the converted arguments, verifies serialization roundtrips correctly, and runs your full suite before marking it safe to deploy.

BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.

Pickle supports more types but is a security risk because it can execute arbitrary code during deserialization. Stick with JSON and convert types explicitly.

Pass database IDs and re-query inside the task, or serialize to a dict with only JSON-safe primitive types. Never pass ORM model instances.