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
Here's what each line means:
- File "/app/app/routes.py", line 18, in schedule_reminder: The route handler passes a datetime object directly to the Celery task, which triggers serialization.
- File "/app/venv/lib/python3.12/site-packages/kombu/serialization.py", line 220, in dumps: Kombu attempts to serialize the task arguments using the configured serializer (JSON by default).
- kombu.exceptions.EncodeError: Object of type datetime is not JSON serializable: Python's json module cannot encode datetime objects. They must be converted to ISO format strings first.
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.
@app.route('/remind', methods=['POST'])
def schedule_reminder():
reminder_time = datetime.utcnow()
send_reminder.delay(user.id, reminder_time)
@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:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- 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:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- 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)
- Run pytest locally to confirm tasks accept serialized arguments.
- Open a pull request with the serialization fix.
- Wait for CI checks to pass on the PR.
- Have a teammate review and approve the PR.
- 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.