Email 2FA: When My OTP Emails Refused to Send (Go + RabbitMQ + Elastic Email)
Context:
Building the Email 2FA (Two-Factor Authentication) feature for my backend.
Stack: Go, GORM, RabbitMQ, Elastic Email.
This is me, understanding how everything connects. This is not a tutorial, just real notes from debugging and clarity.
Starting Point
I wanted users to get an OTP (One-Time Password) via email for 2FA.
Simple idea: user requests → generate code → send email to user’s email address.
In reality, it went more like this:
User clicks "Send OTP" → Client
Backend generates OTP → Code logic
Producer - Publishes message to RabbitMQ → Queue
Consumer picks it up → Message picker
Delivers email via Elastic Email (SMTP)
And somewhere in that chain, something always went wrong.
Error 1: “relation ‘otp_verifications’ does not exist”
This one was simple in hindsight:
GORM tried to insert into a table that didn’t exist yet.
I had created this OtpVerification model below, but didn’t migrate it to my database.
type OtpVerification struct {
ID uuid.UUID `gorm:"primarykey"`
UserID uuid.UUID
OtpCode string
ExpiresAt time.Time
Verified bool
CreatedAt time.Time
}
The Fix
The fix was just to have it running in the database: db.AutoMigrate(&models.OtpVerification{}).
Lesson: GORM doesn’t magically create tables; migration is my responsibility, geez!!.
Error 2: “Unknown task type from email_otp_queue”
I am using RabbitMQ to send tasks to a background worker.
I wanted to publish a message to a queue and have another process handle the actual email sending.
Producer = the Go service that publishes the task in connection with the RabbitMQ (queue)
Consumer = the background worker that reads and processes the task for delivery.
My mistake: when I published the OTP message, I didn’t add targeted user’s email address and specify the type properly.
Here’s the code that was broken:
utils.PublishMessage(
os.Getenv("EMAIL_OTP_QUEUE"),
otp.UserID.String(), // userID instead of email
generateOtp,
"", // empty description
"",
user.Email,
"",
)
The consumer was expecting a "Type": "otp" message to know which handler to call.
But I sent an empty string. So RabbitMQ successfully added it to the queue, but the consumer didn’t know what to do because of “unknown task type.” This mistake was an oversight; I copied the code block from a different feature and forgot to modify it.
The Fix
After understanding what was happening, I updated it:
utils.PublishMessage(
os.Getenv("EMAIL_OTP_QUEUE"),
user.Email, // email
generateOtp,
"otp", // OTP
"",
"",
"",
)
Now the logs looked like this:
Published task to email_otp_queue for oyi.....@gmail.com
Sent otp email to oyi.....@gmail.com
Finally sent the OTP to my inbox, and yeah, I got it.
Lesson:
- Always review and verify copied code blocks
Error 3: “535 Authentication failed: Access denied”
This one was from Elastic Email (deliverer). I had changed my account email to oyi.....@gmail.com, but my SMTP username was still the old aod.....@gmail.com. Elastic Email SMTP credentials are account-level, not tied to the sender identity only.
Even though I verified the new email, the actual SMTP authentication was still using the old one.
The dashboard said:
“For testing purposes, you can only send to your registered email address.”
I knew about Elastic Email restrictions on free accounts to send only to the verified sender address for testing, which I had done.
The Fix
I created a new SMTP using the new, active (verified) account, deleted the old one, and it worked, I got the OTP email.

Lessons:
You can verify multiple “from” addresses on Elastic Email
But your SMTP username remains the one you registered with (main issue)
If you change your login email, you may need a new API key
How It All Connects (my mental model now)
User → /request-otp → Service layer → PublishMessage()
→ RabbitMQ queue → EmailConsumer → SendMail() → Elastic Email → Inbox
Each layer has its responsibility:
Service layer - business logic (create OTP, store it in the db)
Publisher - posts a task to RabbitMQ i.e queue
Consumer - listens, reads the message, and executes email sending
Elastic Email (SMTP) - actually delivers the email to the user
RabbitMQ is just the middleman, like a post office between the app and the worker that sends the mail. Both the publisher (producer) and consumer are connected to the RabbitMQ
Key Lessons
Every queue message must match what the consumer expects, fields, types, and purpose.
SMTP credentials are tied to your sending account, not your email variable.
RabbitMQ doesn’t execute, it only transfers.
Always test both ends: producer and consumer.
Logs are your ally, Oyindamola, read them slowly.
The real “fix” is understanding the flow, not just changing code.
Clarity: The architecture makes sense now. It taught me how background queues actually work, how they all connect to achieve the end goal.