Build1 publisher3 min readPublished
Five PHP-FPM workers capped an entire ride-hailing API at roughly 80 requests a second
A dev.to build log measures the GPS endpoint of a ride-hailing platform at a 62ms median, 97 percent of it framework overhead, and finds the real ceiling in an untuned PHP-FPM worker count on an eight-core box.
The Engineer · Build desk

What happened
- A dev.to build log sets out the architecture of a ride-hailing platform in UAT, covering riders, drivers, an operations desk, cash and wallet payments and self-hosted maps, and the author says real scale is not what any of it describes.
- The backend is Laravel with Horizon for queues and Reverb for WebSockets, alongside a separate Next.js safety console and three Flutter apps sharing one Dart package of models, API client and design system.
- The planned move of GPS pings to Go was deferred once the real bottleneck turned out to be pm.max_children = 5, the untuned PHP-FPM default, on an eight-core box.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision Anyone copying the sidecar design from this post buys a second runtime and a new deploy target to raise a ceiling that one line in the PHP-FPM pool config sets.
- constraint Five workers shared across the whole API means capacity cannot be reasoned about per endpoint: the slowest I/O-bound request in flight decides the latency a booking sees.
- cost At fleet volumes, presence traffic buys about 1.9ms of useful work for 62ms of worker time, and what it spends is worker capacity denied to riders.
- exposure Per-country isolation stops at the instance boundary, because separate databases still compete for one Postgres's connections and CPU.
Ninety-seven percent of a 62ms ping is framework overhead by the author's own loopback measurement, which leaves roughly 1.9ms for the work the endpoint exists to do [19][20][25]. That ratio is the case for a Go presence service, and the design was drawn: a WebSocket RPC writing straight to Redis, keeping GPS out of the PHP HTTP kernel [18]. The driver app posts a position every few seconds, and the post calls that endpoint the highest-volume thing in the system by an order of magnitude [17].
Then the config file. `pm.max_children` was 5, the stock PHP-FPM default, never tuned, on an eight-core box, which is five concurrent requests for the whole API [21]. At a 62ms median that is roughly 80 req/s [22]. The ceiling matters less than the queueing behind it: five slow I/O-bound requests, a routing call and a report query among them, stall every other endpoint, and a rider's booking queues behind GPS pings [23]. The value went to 24 [24]. Same 62ms median, 24 workers, and the ceiling is about 387 req/s, roughly 4.8 times what five allowed [26]. The Go sidecar was built and ships off by default [8]. The post describes itself as "a set of architecture decisions I can point at a number for, and a few that a number reversed" [28].
The separation runs along change rate [3]. Pricing is scoped: the pickup resolves to a service area, that area is recorded on the trip, and from then on it decides the fare table, the surge, the match radius and how long a driver has to answer an offer [10]. That is what makes one Laravel deployment per country workable, each with its own database and its own worker containers against a shared Postgres instance [5][4].
Matching runs a Redis `GEORADIUS ... ASC` around the pickup and walks the result first-eligible-wins [11]. Drivers set their own willingness to travel, so the scan goes to the platform ceiling and each candidate is then filtered against the radius that driver chose [12]. A wider scan surfaces work a driver could not previously see; it never lets them jump ahead of someone closer [12]. Accepting an offer locks only the target trip row, which the post says is not enough, so Postgres carries a partial unique index, `one_active_trip_per_driver` on `trips (captain_id)`, restricted to the four in-flight statuses [14].
Presence can leave PHP. Pricing has to stay. Every GPS fix writes a route point, and completion reads that trail to finalize the fare [15]. "The trail is the fare," the author wrote, giving that as the reason it stays in PHP [16].
The map stack is self-hosted: OSRM for routing, Photon for geocoding, TileServer GL for imagery [9]. The per-geocode price of the hosted services those replace is not in the published text [27]. For the 80 req/s number to mean anything on another deployment, two things have to hold: an untuned `pm.max_children = 5`, and a median per request near 62ms. The measurement was taken over loopback on a system in UAT, and the post says nothing in it describes real scale [2][19].
What to watch
- Whether 24 PHP-FPM children fit the box's memory, since raising the pool trades a queueing ceiling for resident memory per worker.
- Whether the Go presence path is switched on, and what a ping costs once Redis writes bypass the PHP kernel.
- Whether the partial unique index starts surfacing as accept failures to drivers under concurrent offers.