Skip to content

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

Illustration accompanying Five PHP-FPM workers capped an entire ride-hailing API at roughly 80 requests a second

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.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories