API Design | Aug 28, 2025 | 10 min read | By Pratim Maloji Bhosale | Reviewed by Bhushan Gaikwad
Pratim Bhosale is a Senior Developer Experience Engineer (Developer Advocate) at Treblle, specializing in full‑text, vector, and hybrid search. A recognized Google Developer Expert for Go, she speaks regularly at industry events, including WeAreDevelopers and JOTB, and participates in webinars on API design, governance, and integrating AI in search workflows. Before joining Treblle, Pratim worked as a full‑stack engineer at UBS and contributed to SurrealDB’s DevTools advocacy. Her work has earned industry recognition, including being named one of India’s top 91 engineers by the Economic Times and receiving the Kedar Tumne Innovation Award.
When building apps that rely on external data, choosing how to receive updates is crucial. In this article, we compare API polling and webhooks, explain when to use each, and walk through practical Go examples to help you implement the right approach.
Imagine you’re building a job aggregator application that finds jobs across different platforms and offers them under a single platform to its users. This saves them time as they don’t have to search through different job portals.
How would you start implementing this?
One of the earliest approaches you could think of is to integrate with each job board’s API to fetch job data and display it in your app. Then, in order to keep up with the job updates on all the platforms, you would periodically need to fetch all the new jobs on all these platforms.
This job of periodically fetching objects from a server (via its API) is called API polling.
Here’s how the logic for polling the jobs API would look:
package main
import (
"fmt"
"strings"
"time"
)
type ExampleJob struct {
ID int `json:"id"`
Title string `json:"title"`
Company string `json:"company"`
Location string `json:"location"`
Type string `json:"type"`
SalaryRange string `json:"salary_range"`
Skills []string `json:"skills"`
PostedDate string `json:"posted_date"`
}
var (
jobCounter = 0
jobs = []ExampleJob{
{
ID: 1,
Title: "Senior Software Engineer",
Company: "TechCorp",
Location: "San Francisco, CA (Remote)",
Type: "Full-time",
SalaryRange: "$120,000 - $180,000",
Skills: []string{"Go", "Kubernetes", "AWS", "Microservices"},
PostedDate: "2 days ago",
},
{
ID: 2,
Title: "UX/UI Designer",
Company: "DesignHub",
Location: "New York, NY (Hybrid)",
Type: "Full-time",
SalaryRange: "$90,000 - $140,000",
Skills: []string{"Figma", "Sketch", "User Research", "Prototyping"},
PostedDate: "1 week ago",
},
{
ID: 3,
Title: "AI Research Scientist",
Company: "AILabs",
Location: "Boston, MA (On-site)",
Type: "Full-time",
SalaryRange: "$150,000 - $220,000",
Skills: []string{"Machine Learning", "Python", "PyTorch", "NLP"},
PostedDate: "3 days ago",
},
}
)
// In a real application, this function would make an HTTP request to an API endpoint
func fetchNextJob() (ExampleJob, bool) {
if jobCounter >= len(jobs) {
return ExampleJob{}, false
}
// Simulate getting the next job from the API
job := jobs[jobCounter]
// Move to next job, loop back to start if at the end
jobCounter = (jobCounter + 1) % len(jobs)
return job, true
}
func main() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
fmt.Println("Starting job poller. Press Ctrl+C to stop.")
fmt.Println("Polling for jobs every 2 seconds...")
// This is the main polling loop
for range ticker.C {
if job, ok := fetchNextJob(); ok {
fmt.Println("\\n=== New Job Available ===")
fmt.Printf("Position: %s\\n", job.Title)
fmt.Printf("Company: %s\\n", job.Company)
fmt.Printf("Location: %s\\n", job.Location)
fmt.Printf("Type: %s | Posted: %s | %s\\n", job.Type, job.PostedDate, job.SalaryRange)
fmt.Printf("Required Skills: %s\\n", strings.Join(job.Skills, ", "))
fmt.Println("=======================")
} else {
fmt.Println("No more jobs available")
}
}
}
You can directly copy this code snippet in your IDE, save it, and run it using go run api_polling.go
to see job updates every two seconds.
If you want to try more examples, you can prompt your AI coding agents to give you a simple example of API polling in your desired language.
In a final attempt to explain the concept of API polling, if you were a nosy neighbor who wants to be always updated about what’s happening with the love life of your neighbor, you would regularly knock on their door and ask if there were any updates in their dating life.
All polling logics include a flavour of the following flow:
Hoping that API polling will no longer be jargon, let’s move to a more technical definition.
API polling is a client-driven communication pattern. The client makes repeated requests to the server at regular intervals to check for updated data. In other words, the client “asks” the server over and over: “Do you have anything new for me?”
From the server’s perspective, each poll request behaves like any other GET: it either returns new data or returns the same old data. In many polling setups (especially for long-running tasks), the server may include a status flag. A common pattern is: the client first sends an initial request that starts a job (e.g. a document conversion), the server replies with a job ID and a “processing” status.
Then the client repeatedly polls a status endpoint with that ID. The server might reply with 202 Accepted
if the job is still in progress, and finally 200 OK
with results when it’s done.
In general, polling leaves most of the control on the client side: it decides when and how often to ask for updates, rather than the server pushing updates.
When you keep knocking on the server’s door every few seconds, asking, “Anything new yet?” your requests are using up more resources both for your app and for the server. If too many clients poll too often, the server can quickly become overloaded, leading to slower response times or even outages.
This constant polling means your app sends a lot of unnecessary HTTP requests, which the server must handle using up CPU cycles, memory, and network traffic. If most of these requests find nothing new, it’s just wasted work for both sides. Over time, this can slow down the server and even affect other users who need real information., especially if most requests return no new data.
In 2007, software developer Jeff Lindsay spoke about using user defined callbacks made with HTTP POST.
Unfortunately, web stacks are stateless request processors, so you can’t really use sockets. You could use Amazon SQS or some other queuing system, but queues often just move the polling to somewhere else. What we need is something simple, stateless, and ideally real-time. We need to push.
This is where webhooks come in. Webhooks are essentially user defined callbacks made with HTTP POST. To support webhooks, you allow the user to specify a URL where your application will post to and on what events. Now your application is pushing data out wherever your users want. It’s pretty much like re-routing STDOUT
on the command line.
Let’s take the same example above and see how we can get job updates sent to the client via a webhook system instead of the client constantly polling the fetchNextJob()
function.
Our client will now expose an endpoint on which it will receive job updates.
This is a simulation example:
func webhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}
var job ExampleJob
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&job); err != nil {
http.Error(w, "Failed to decode webhook payload", http.StatusBadRequest)
return
}
fmt.Println("\\n=== New Job Received via Webhook ===")
fmt.Printf("Position: %s\\n", job.Title)
fmt.Printf("Company: %s\\n", job.Company)
fmt.Printf("Location: %s\\n", job.Location)
fmt.Printf("Type: %s | Posted: %s | %s\\n", job.Type, job.PostedDate, job.SalaryRange)
fmt.Printf("Required Skills: %s\\n", strings.Join(job.Skills, ", "))
fmt.Println("====================================")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Webhook received successfully")
}
// This function simulates an external service sending webhooks to our server
func simulateWebhookClient() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
jobCounter := 0
for range ticker.C {
job := jobs[jobCounter]
jobCounter = (jobCounter + 1) % len(jobs)
payload, err := json.Marshal(job)
if err != nil {
log.Printf("Error marshalling job: %v", err)
continue
}
resp, err := http.Post("<http://localhost:8080/webhook>", "application/json", bytes.NewBuffer(payload))
if err != nil {
log.Printf("Error sending webhook: %v", err)
continue
}
resp.Body.Close()
}
}
func main() {
go simulateWebhookClient()
http.HandleFunc("/webhook", webhookHandler)
fmt.Println("Webhook server starting on port 8080...")
fmt.Println("Listening for job notifications at <http://localhost:8080/webhook>")
fmt.Println("A simulation client is sending a new job every 5 seconds.")
fmt.Println("Press Ctrl+C to stop.")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Could not start server: %s\\n", err)
}
}
Here http://localhost:8080/webhook
is the exposed endpoint and the webhookHandler
receives this request, decodes the data, and prints it. This example starts a goroutine simulateWebhookClient()
to simulate the behaviour of an actual client.
Now that we’re aware what API polling and webhooks are, we come to the most common question: When to go the polling route and when to use webhooks?
Scenario | Polling | Webhooks |
---|---|---|
High update volume, low latency need | More efficient | Can overwhelm |
Clients behind NAT / no webhook support | Works | Doesn’t work |
Rare but critical events | Wasteful | Best choice |
Real-time data needed | Delayed | Immediate |
Your laptop at home (behind NAT) can make requests to Google, but Google can’t just send a request to your laptop unless you set up special routing or tunneling. This could be possible via polling a public API.
My conclusion here as a backend software developer is that while webhooks are overall superior to API polling, the best option is always to support both forms of update. This way, if webhooks are not available, unreliable, or blocked by network restrictions, your system can still fall back to polling to get updates reliably.
While most use cases will clearly win with having webhooks, a more nuanced approach is to constantly observe what is happening with your infrastructure and architecture that you have decided to go ahead with. Treblle gives you that visibility. When you're polling, Treblle shows you how often you're hitting the API, how long each request takes, and what the response looks like.
You can spot repeated requests with identical responses, a clear sign that your polling interval might be too tight. Or worse, you might find your client silently retrying on timeouts, hammering the API without realizing it. Treblle logs each call with detailed context so you can backtrack what happened and when.
With webhooks, while Treblle does not natively listen for webhooks, it can still help you monitor.
Need real-time insight into how your APIs are used and performing?
Treblle helps you monitor, debug, and optimize every API request.
Explore TreblleNeed real-time insight into how your APIs are used and performing?
Treblle helps you monitor, debug, and optimize every API request.
Explore TreblleAll incoming POST requests to that endpoint, including the payload, headers, response, and duration. You can route your sending logic through a controller/middleware Treblle tracks.
So you get:
This is useful for debugging missed or duplicate webhooks and confirming delivery from third-party services like Stripe, GitHub, or your own systems.
If your server is the one sending webhooks to others, you can also use Treblle to monitor those outgoing POST requests as long as the SDK is installed in the part of your backend that sends them.
In the end, whether you choose API polling or webhooks depends on the needs and limitations of your architecture. Polling can be a good starting point when webhooks aren’t available, but it may use more resources and deliver slower updates.
Webhooks, when possible, allow for faster and more efficient communication between systems. Understanding both options gives you the flexibility to design applications that are responsive and efficient, no matter the use case.
Need real-time insight into how your APIs are used and performing?
Treblle helps you monitor, debug, and optimize every API request.
Explore TreblleNeed real-time insight into how your APIs are used and performing?
Treblle helps you monitor, debug, and optimize every API request.
Explore TreblleAPI headers are a fundamental part of how web services communicate, yet they’re often glossed over in documentation or misunderstood in practice. This guide breaks down what API headers are, how they work in both requests and responses, and why you should pay attention to them.
Scaling API monitoring in MuleSoft can quickly get messy when juggling multiple environments. This guide shows how Treblle brings unified observability and intelligence into the MuleSoft ecosystem
Slow API onboarding can stall growth, frustrate developers, and increase costs. This guide explores key API integration best practices to accelerate Time to First Integration and boost business impact.