How to Send Discord Webhook Messages with Go (Golang)
A practical guide to sending Discord webhook messages in Go using net/http. Send plain text, rich embeds, files, and handle rate limits — no external library needed.
🇷🇺 Also available in Русский
Go is a favorite for backend services, CLI tools, and monitoring agents — exactly the kind of software that benefits from posting alerts to Discord. The best part: you can send a Discord webhook in Go using only the standard library, with no external dependencies.
This guide covers plain text, rich embeds, file uploads, custom usernames, and proper rate-limit handling.
What is a Discord Webhook?
A Discord webhook is a URL that posts messages to a channel when it receives an HTTP POST request. Unlike a bot, it needs no gateway connection and no token exchange — perfect for a lightweight Go service. If you don’t have a URL yet, follow our guide on how to get a Discord webhook URL.
Prerequisites
You need Go 1.16 or newer. No go get required — everything below uses net/http, encoding/json, and bytes from the standard library.
Step 1: Send a Simple Text Message
package main
import (
"bytes"
"encoding/json"
"log"
"net/http"
)
const webhookURL = "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
type Payload struct {
Content string `json:"content,omitempty"`
}
func main() {
body, err := json.Marshal(Payload{Content: "Hello from Go! 🐹"})
if err != nil {
log.Fatal(err)
}
resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(body))
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
log.Println("Message sent successfully!")
} else {
log.Printf("Failed: %s", resp.Status)
}
}
A successful webhook request returns 204 No Content.
Step 2: Send a Rich Embed
Embeds let you format messages with a title, description, colored bar, and fields. Model them as Go structs so the JSON stays type-safe:
type Embed struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Color int `json:"color,omitempty"`
Fields []EmbedField `json:"fields,omitempty"`
Footer *EmbedFooter `json:"footer,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
}
type EmbedField struct {
Name string `json:"name"`
Value string `json:"value"`
Inline bool `json:"inline,omitempty"`
}
type EmbedFooter struct {
Text string `json:"text"`
}
type EmbedPayload struct {
Embeds []Embed `json:"embeds"`
}
Now build and send it:
func sendEmbed() error {
embed := Embed{
Title: "Server Status Report",
Description: "All systems are operational.",
Color: 5763719, // Green in decimal
Fields: []EmbedField{
{Name: "CPU Usage", Value: "23%", Inline: true},
{Name: "Memory", Value: "4.2 GB / 16 GB", Inline: true},
{Name: "Uptime", Value: "14 days", Inline: false},
},
Footer: &EmbedFooter{Text: "Monitoring Agent"},
Timestamp: time.Now().Format(time.RFC3339),
}
body, _ := json.Marshal(EmbedPayload{Embeds: []Embed{embed}})
resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
Discord colors are decimal integers. A few common ones: red 15548997, green 5763719, blurple 5793266, yellow 16776960.
Step 3: Override Username and Avatar
Add these fields to your payload struct to change how each message appears:
type Payload struct {
Content string `json:"content,omitempty"`
Username string `json:"username,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
}
body, _ := json.Marshal(Payload{
Content: "Deployment complete! ✅",
Username: "Deploy Bot",
AvatarURL: "https://example.com/deploy-avatar.png",
})
Step 4: Upload a File
To attach a file, send multipart/form-data instead of JSON. Put the message payload in a payload_json field alongside the file:
func sendFile(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
// JSON payload part
payload, _ := json.Marshal(map[string]string{"content": "Here's the log file:"})
w.WriteField("payload_json", string(payload))
// File part
part, err := w.CreateFormFile("file", filepath.Base(path))
if err != nil {
return err
}
io.Copy(part, file)
w.Close()
req, _ := http.NewRequest("POST", webhookURL, &buf)
req.Header.Set("Content-Type", w.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
Remember to import bytes, mime/multipart, os, io, and path/filepath. See our dedicated guide on sending files with webhooks for size limits and multi-file uploads.
Step 5: Handle Rate Limits
Discord limits webhooks to roughly 5 requests per 2 seconds. When you exceed it, you get 429 Too Many Requests with a retry_after value. Respect it with a small retry helper:
func sendWithRetry(payload []byte, maxRetries int) error {
for attempt := 0; attempt < maxRetries; attempt++ {
resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(payload))
if err != nil {
return err
}
if resp.StatusCode == http.StatusNoContent {
resp.Body.Close()
return nil
}
if resp.StatusCode == http.StatusTooManyRequests {
var r struct {
RetryAfter float64 `json:"retry_after"`
}
json.NewDecoder(resp.Body).Decode(&r)
resp.Body.Close()
time.Sleep(time.Duration(r.RetryAfter * float64(time.Second)))
continue
}
resp.Body.Close()
return fmt.Errorf("unexpected status: %s", resp.Status)
}
return fmt.Errorf("all retries exhausted")
}
Our rate limits guide explains the per-channel and per-webhook buckets in detail.
Security: Store the URL in an Environment Variable
Never hardcode the webhook URL in committed source. Read it from the environment:
webhookURL := os.Getenv("DISCORD_WEBHOOK_URL")
if webhookURL == "" {
log.Fatal("DISCORD_WEBHOOK_URL is not set")
}
A leaked URL lets anyone post to your channel. Review our security best practices for rotation and monitoring.
Next Steps
You now have everything to send Discord messages from any Go program — API servers, cron jobs, or monitoring agents. To design complex layouts visually before coding them, use the free Discord Webhook Builder: build the embed, export the JSON, and drop it into your Go structs.
For automation ideas, see webhook notifications and GitHub Actions integration.
Related Articles
- How to Get a Discord Webhook URL — Grab your webhook URL on desktop or mobile
- How to Send Discord Webhook Messages with Python — The same patterns in Python
- Send Discord Webhooks from JavaScript (Node.js & Browser) — Node.js and fetch examples
- Discord Webhook Rate Limits Explained — Avoid 429 errors
- Discord Webhook Notifications and Automation — Monitoring and CI/CD alert patterns
Try it in our tool
Open Discord Webhook Builder