SendGrid Email Scheduling — Delaying Email Delivery with send_at
In this tutorial, you will learn about SendGrid Email Scheduling. We cover key concepts, practical examples, and best practices to help you master this topic.
SendGrid email scheduling allows you to set a future delivery time using the send_at parameter, enabling delayed sends, timezone-aware delivery, and drip campaign scheduling without maintaining your own scheduling infrastructure.
What You'll Learn
- How to schedule emails with send_at
- How to schedule batch sends
- Timezone-aware scheduling best practices
Why It Matters
Scheduling emails from your application requires background job queues, Cron Jobs, or worker processes. SendGrid's built-in scheduling handles this server-side, reducing application complexity and ensuring delivery even if your application is down at the scheduled time.
Real-World Use
DodaTech uses email scheduling for drip campaigns: welcome email sent immediately, day 1 follow-up scheduled 24 hours later, day 3 offer scheduled, and day 7 check-in scheduled. All scheduling is handled by SendGrid, freeing the application from managing timing logic.
from datetime import datetime, timedelta
import time
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
def schedule_welcome_drip(user_email, user_name):
sg = SendGridAPIClient(SENDGRID_API_KEY)
now = int(time.time())
# Email 1: Send immediately
email1 = Mail(
from_email='noreply@dodatech.com',
to_emails=user_email,
subject=f'Welcome to DodaTech, {user_name}!',
html_content='<h1>Welcome!</h1><p>Thanks for joining.</p>'
)
sg.send(email1)
# Email 2: Schedule for 24 hours later
email2 = Mail(
from_email='noreply@dodatech.com',
to_emails=user_email,
subject='Getting Started with DodaTech',
html_content='<h1>Tip 1: Complete Your Profile</h1>'
)
email2.send_at = now + 86400 # 24 hours
sg.send(email2)
# Email 3: Schedule for 3 days later
email3 = Mail(
from_email='offers@dodatech.com',
to_emails=user_email,
subject='Special Offer Inside!',
html_content='<h1>Exclusive 30-day trial</h1>'
)
email3.send_at = now + 259200 # 3 days
sg.send(email3)
print(f"Scheduled 3 emails for {user_email}")
Batch Scheduling
def schedule_batch_campaign(recipients, template_id, template_data, delay_hours=0):
"""Schedule a campaign for multiple recipients with optional delay"""
sg = SendGridAPIClient(SENDGRID_API_KEY)
now = int(time.time())
results = []
for recipient in recipients:
message = Mail(
from_email='campaign@dodatech.com',
to_emails=recipient['email']
)
message.template_id = template_id
message.dynamic_template_data = {
**template_data,
'userName': recipient['name']
}
if delay_hours > 0:
message.send_at = now + (delay_hours * 3600)
try:
response = sg.send(message)
results.append({
'email': recipient['email'],
'status': 'scheduled' if delay_hours > 0 else 'sent',
'send_at': message.send_at if hasattr(message, 'send_at') else 'immediate'
})
except Exception as e:
results.append({
'email': recipient['email'],
'status': 'error',
'error': str(e)
})
return results
Timezone-Aware Scheduling
from datetime import datetime
import pytz
def schedule_timezone_aware(email, user_timezone_str, preferred_hour=10):
"""Schedule email at a preferred hour in the user's timezone"""
user_tz = pytz.timezone(user_timezone_str)
now_utc = datetime.now(pytz.UTC)
now_user = now_utc.astimezone(user_tz)
# Calculate next occurrence of preferred_hour in user's timezone
if now_user.hour < preferred_hour:
schedule_today = now_user.replace(
hour=preferred_hour, minute=0, second=0, microsecond=0
)
else:
schedule_today = now_user.replace(
hour=preferred_hour, minute=0, second=0, microsecond=0
) + timedelta(days=1)
# Convert back to UTC timestamp
schedule_utc = schedule_today.astimezone(pytz.UTC)
send_at = int(schedule_utc.timestamp())
message = Mail(
from_email='noreply@dodatech.com',
to_emails=email,
subject='Your Daily Digest',
html_content='<h1>Daily Summary</h1>'
)
message.send_at = send_at
sg = SendGridAPIClient(SENDGRID_API_KEY)
sg.send(message)
print(f"Scheduled for {schedule_today} in {user_timezone_str}")
Common Mistakes
1. Scheduling Too Far in Advance
SendGrid allows scheduling up to 72 hours in advance. Emails scheduled beyond that are rejected.
2. Using Local Time Instead of UTC
The send_at parameter expects a Unix timestamp in UTC. Sending a local time timestamp results in wrong delivery time.
3. Not Handling Past Timestamps
If send_at is in the past, SendGrid sends the email immediately. Always validate that the timestamp is in the future.
4. Scheduling Without Confirmation
Scheduled emails are queued and cannot be canceled via the API. Implement a separate cancellation mechanism using suppression groups.
5. Over-Scheduling Batch Emails
Scheduling thousands of emails at the same second can trigger rate limits. Add random delays between sends in the batch.
Practice Questions
- What parameter controls email scheduling in SendGrid?
- What format does send_at expect?
- How far in advance can you schedule?
- What happens if send_at is in the past?
- How do you handle timezone-aware scheduling?
Answers
- The
send_atparameter. 2. A Unix timestamp (seconds since epoch). 3. Up to 72 hours. 4. The email is sent immediately. 5. Convert the user's preferred delivery time to a UTC Unix timestamp.
Challenge
Build an email scheduling service that: schedules emails with validation, supports timezone-aware delivery at a preferred hour, handles the 72-hour limit gracefully, and provides a scheduling dashboard showing pending, sent, and failed scheduled emails.
FAQ
Mini Project
Build a drip campaign scheduler that: creates a multi-email sequence (Day 0, Day 1, Day 3, Day 7), schedules each email using send_at with timezone awareness, tracks scheduled vs sent status, and provides a cancellation mechanism via suppression groups for users who unsubscribe mid-campaign.
What's Next
- Learn about sandbox mode for testing without sending
- Explore bounce handling and suppression management
- Continue to event Webhook setup for delivery tracking
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro