Back to Blog

Why You Should Migrate From PHP to Go

PHP ties up a whole worker for every request, while a single Go process can hold thousands of connections at once. That difference in how the two handle concurrency is the real reason teams migrate, and you can move one service at a time instead of rewriting everything.

Why You Should Migrate From PHP to Go

When PHP struggles at scale, the problem is usually not that the language is slow. It is the way it handles concurrent requests. Under PHP-FPM, every request holds a full worker process for its entire duration, so any request that spends its time waiting, on a slow query, a third-party call, or a connection that stays open, pins a worker that could be serving someone else. Real-time features like live dashboards, streaming responses, and high-fan-out APIs reach that limit fast, and adding workers or machines pushes the ceiling back without removing it.

Go's concurrency model removes the ceiling instead of raising it, and that is the real reason teams migrate from PHP to Go. This post walks through why the model matters, the deployment and cost wins that come with it, what teams report after making the move, and how to migrate one service at a time instead of betting the product on a rewrite.

TL;DR

  • Migrate for the concurrency model. PHP's process-per-request design ties up a whole worker for the life of each request. Go's goroutines let one process juggle thousands of concurrent connections. That is the win, and it compounds as you add real-time and connection-heavy features.
  • The memory math is dramatic. A PHP-FPM worker holds roughly 30 to 60 MB once your framework loads; a goroutine starts at about 8 KB. Fifty concurrent streams can saturate an FPM pool while a single Go process barely notices them.
  • Practitioners report real gains. Engineers from Cloudflare and the fintech Curve describe one service going from around 10 requests per second to thousands after moving to Go, with new engineers productive in one to two weeks (Go Time #316).
  • Deployment gets radically simpler. Go compiles to a single static binary. No runtime to install, no Composer step, no FPM tuning, ideal for microservices and Kubernetes.
  • You do not have to rewrite everything. The winning pattern is the strangler approach: migrate the one service that hurts, run it beside your PHP, and expand from proof, not faith.

Table of Contents

The Real Reason to Migrate Is Concurrency

Understanding the mechanics is what turns "Go is faster" from a slogan into a decision you can defend, so it is worth being precise about what each runtime actually does with a request.

Classic PHP runs share-nothing, process-per-request. A request arrives, PHP-FPM hands it to a worker, the worker runs your code from a clean slate to the response, then resets. It is a robust design, simple and well isolated, but the cost shows up as memory.

A single PHP-FPM worker typically holds 30 to 60 MB of resident memory once your framework is loaded, so serving 50 requests at once means 50 workers, roughly 1.5 to 3 GB of RAM spent mostly on waiting.

Now make those 50 requests Server-Sent Events streams or WebSocket connections that each stay open for minutes. Fifty long-lived connections can saturate an entire FPM pool, and the 51st user waits in line. You cannot buy your way out of this cheaply, because the resource being consumed is a whole process per concurrent connection.

Go inverts that math. A goroutine, Go's unit of concurrency, starts with a stack of about 8 KB, and the runtime multiplexes thousands of them onto a small pool of OS threads. When a goroutine blocks on I/O, the scheduler parks it and runs another on the same thread. One Go process can hold tens of thousands of open connections while using a fraction of the memory an equivalent FPM fleet would need. A connection handler is just an ordinary function:

example.gogo
func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
	// Each connection is one cheap goroutine. Blocking here parks
	// this goroutine and frees the thread for others, no pool to exhaust.
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "streaming unsupported", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/event-stream")

	for event := range s.events(r.Context()) {
		fmt.Fprintf(w, "data: %s\n\n", event)
		flusher.Flush()
	}
}

The net/http server runs each request in its own goroutine automatically, so this code holds a live streaming connection without reserving a heavyweight worker. That is the architectural fit you are buying. If your product is drifting toward real-time features, high-fan-out API gateways, or services that hold many concurrent connections, PHP's model fights you and Go's model carries you. If goroutines and channels are new to you, the Go Concurrency Fundamentals course is where this model clicks.

What Teams Gain After the Move

The most credible primary-source account of a real PHP-to-Go migration comes from the Go Time podcast, episode 316, where engineers from Cloudflare and the fintech Curve walked through moving production PHP systems to Go (Go Time #316, Changelog). It is worth anchoring on because it is practitioners describing their own systems, not a vendor case study or a blog inferring numbers from the outside.

Three things stand out:

  • The scale headroom is real. One service reportedly went from handling around 10 requests per second under the old setup to thousands after the move.
  • The performance floor is high. "Even bad Go is incredibly performant" was how they described the runtime, meaning you get a lot of throughput before you ever need to optimize.
  • Onboarding is fast. New engineers were productive in one to two weeks rather than months, because Go's surface area is small.

The Curve team's Go lineage traces back to Monzo, another fintech that built heavily on Go, which is part of why the pattern spread across that space.

Beyond that primary source there are more Go-in-production data points worth knowing. HelloFresh built and open-sourced its API gateway, Janus, in Go (Janus on GitHub), the kind of high-fan-out routing layer where Go's concurrency shines. Individual engineers routinely report that a single batch job rewritten from PHP to Go runs dramatically faster on a fraction of the resources.

Go vs PHP: The Technical Wins

Concurrency is the headline, but three concrete differences do the work once you commit. Here they are side by side, then the ones that matter most.

DimensionPHPGo
ConcurrencyProcess-per-request; Fibers add concurrency, not parallelismGoroutines on an M:N scheduler, true parallelism across cores
TypingDynamic, gradual type hintsStatic, checked at compile time
DeploymentCode + Composer deps + FPM + web serverOne static binary, no runtime to install
ExecutionInterpreted, JIT-assistedCompiled to native machine code

Concurrency is decisive. PHP 8.1 added Fibers, a genuine improvement, but be precise about what they do: Fibers give you cooperative concurrency for structuring async I/O, not parallelism across CPU cores. They let one worker interleave waiting tasks. They do not let one PHP process saturate 16 cores with CPU work the way Go's scheduler does.

Go maps many goroutines onto many OS threads and runs them genuinely in parallel, while parking them cheaply when they block. You get both cheap concurrency and real parallelism from the same primitive. Here is a worker pool, a pattern you reach for constantly in Go, that has no clean single-process equivalent in classic PHP:

example.gogo
func process(jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
	defer wg.Done()
	for job := range jobs {
		results <- job.Run() // each worker pulls the next job when free
	}
}

func main() {
	jobs := make(chan Job, 100)
	results := make(chan Result, 100)
	var wg sync.WaitGroup

	// Fan out to 8 workers sharing one queue, all in one process.
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go process(jobs, results, &wg)
	}

	go func() {
		for _, j := range loadJobs() { // pull the batch from your work source
			jobs <- j
		}
		close(jobs)
	}()

	go func() { wg.Wait(); close(results) }()
	for r := range results {
		record(r)
	}
}

Deployment is the win people underrate. A Go build produces a single static binary. Shipping it can be as simple as copying that file to a server or into a scratch container, with no PHP runtime, no Composer install, no FPM tuning, and no web server config in front of it. For microservices in Kubernetes, that tiny self-contained artifact pays off every single deploy.

Static typing plays a strong supporting role: the compiler catches a class of errors at build time that PHP surfaces at runtime, and that safety net matters more as a service grows and more hands touch it.

One more point that settles the "but modern PHP is fast now" objection. PHP did close much of the raw-speed gap, with the PHP 8 JIT and worker runtimes like FrankenPHP and RoadRunner that keep your app booted in memory across requests. Here is the tell: FrankenPHP and RoadRunner are both written in Go. When the PHP ecosystem needed to build high-concurrency application servers, it reached for Go's runtime to do it. If concurrency is your problem, you can adopt a Go-powered runtime in front of PHP, or you can write the connection-heavy service in Go directly and skip the middle layer.

The Benchmark Truth (and the Real Payoff)

You will see headline numbers claiming Go is 10 to 30 times faster than PHP. Those come from synthetic suites like the TechEmpower benchmarks, which push runtimes under idealized, CPU-and-framework-bound conditions (TechEmpower Framework Benchmarks). They are real, and they are not a forecast of your production latency, so do not migrate expecting that multiplier.

The real payoff is more durable than a benchmark. In production, most of a request's time goes to the database and the network, which Go and PHP wait on identically, so that raw multiplier shrinks fast. Teams that measure honestly after a migration often report single-digit to low-double-digit percent gains on average end-to-end latency.

But that average hides where Go actually wins: tail-latency stability and cost per request. Go's p99 and p99.9 latencies stay flat under load because you are not gambling on FPM pool availability for each request, and you serve the same traffic on dramatically fewer machines because one process absorbs concurrency a pool cannot. Steadier tails and a smaller fleet are what you feel in production and on the bill, and they hold up long after a synthetic number stops mattering.

Faster to Hire, Faster to Ship

The people math favors migrating too. Go is famously fast to onboard. Its small surface area, 25 keywords and one enforced formatter, means a competent engineer from PHP or any other language becomes productive in a week or two, which matches the one-to-two-week onboarding the Cloudflare and Curve engineers described.

You are not limited to hiring people who already know Go, because people learn it quickly, and the enforced formatting plus static typing means the code they write looks and behaves consistently from day one.

Go also tends to carry a salary premium in market surveys, a signal of demand outpacing supply, though the exact figures quoted from sources like Glassdoor are directional and vary by region and role, so treat them as a hiring tailwind rather than a line in a business case. Where Go's ecosystem is strong is exactly where you are migrating to it: networking, infrastructure, CLIs, and high-throughput services. That is the work goroutines were built for, and the library and tooling story there is mature and first-class.

How to Migrate Without Betting the Company

The safest way to migrate is also the one that works: do not rewrite everything at once. Full rewrites are one of the most reliable ways to stall a product, because working code encodes years of bug fixes and edge cases you would throw away (Joel on Software). The Cloudflare and Curve teams did not do a big-bang rewrite. They used the strangler pattern, and it is the blueprint to copy.

It works like this. Find the one service that is actually hurting, the SSE stream saturating your pool, the fan-out API pinning your workers, the gateway that cannot hold enough connections. Rebuild just that service in Go, put it behind the same routing layer, and run it beside your PHP.

Route a slice of traffic to it, watch the memory and tail latency, then route more. Your PHP keeps serving everything else the whole time, so there is no flag day and no all-or-nothing bet. When that first service proves out, and it usually does, you have a real data point and a template for the next one.

This is also why migrating is a per-service decision, not a company-wide identity. You do not have to become "a Go shop." Keep PHP where it fits, the CMS, the content pages, the Laravel admin your team ships fast in, and move the connection-heavy, high-throughput services to Go where the concurrency model is a structural fit.

FAQ

Is Go faster than PHP?

On synthetic, CPU-bound benchmarks, yes, often by a large factor (TechEmpower). In production the average-latency gap is usually smaller, because most of a request's time goes to the database and network, which both languages wait on identically. The durable win is concurrency efficiency and tail-latency stability under heavy load: Go holds thousands of connections in one process and keeps p99 latency flat where an FPM pool would be exhausted, which also means fewer machines for the same traffic.

Should I rewrite my whole PHP app in Go?

No, migrate incrementally. Full rewrites are one of the most reliable ways to stall a product (Joel on Software). Use the strangler pattern: move the one painful, high-concurrency service to Go, run it beside your PHP, route traffic gradually, and expand once it proves out. You get the migration wins without a flag-day risk.

What kinds of services should I migrate first?

The connection-heavy and high-fan-out ones: WebSocket and SSE endpoints, real-time features, API gateways, streaming responses, and any service where many requests sit open waiting on I/O. These are exactly where PHP's process-per-request model runs out of room and where Go's goroutines pay off immediately, so they give you the clearest, fastest proof of value.

Can PHP handle WebSockets and real-time features?

It can, with help, but it fights you. Classic PHP-FPM ties up a worker per connection, so many long-lived connections exhaust the pool. Runtimes like Swoole and FrankenPHP keep workers alive and add event-driven concurrency to make it viable. Go handles the same workload natively with goroutines, one cheap goroutine per connection, which is why connection-heavy services are the most common reason teams migrate.

Is Go harder to learn than PHP?

The syntax is not. Go has only 25 keywords and one enforced formatting style, and most engineers are productive within one to two weeks. The genuinely new part for a PHP developer is the concurrency model, goroutines and channels, and handling errors as returned values instead of exceptions. Those take a little practice, and they are exactly the skills that make the migration worth it.

Ready to Write Production Go?

The best way to evaluate Go is to write some. On LevelUpGo, every lesson is a hands-on exercise you solve in the browser. Your code runs on a real Go toolchain and is checked against the exercise's tests, so you get immediate feedback on working Go instead of reading about it.

A path for a PHP developer:

  • Go Fundamentals covers types, structs, and error handling, the parts that feel different coming from PHP. Free to start, no credit card.
  • Composite Types covers slices, maps, and structs, the everyday building blocks for the data you move through a service.
  • Go Concurrency Fundamentals teaches goroutines, channels, and select, the features that let one Go process do what a whole PHP-FPM pool cannot.
  • The roadmap lays out the path from your first program to a concurrent capstone project.

For more comparisons that help you decide, read Switching From Python to Go, Go vs Rust in 2026, and Go vs Node.js and the supply-chain question.

Sources

Sources cited in this post, with primary practitioner accounts listed first:

Ready to master Go?

Join LevelUpGo and start building real projects with interactive, hands-on lessons.

Start Learning Free