Waarom we verhuisden
Kort antwoord
Onze website draaide op Laravel Vapor, op AWS Lambda in Frankfurt. Op 3 november 2026 zetten we hem over naar Brewless, ons eigen platform voor serverless Laravel-hosting op Scaleway. De verhuizing zelf kostte een middag. Bezoekers krijgen pagina’s nu ongeveer vier keer zo snel, omdat ze uit de CDN komen. Buiten de cache is Lambda bij pieken sterker; daar hebben we de instellingen op aangepast.
Vapor heeft ons jaren goed gediend. Je hebt geen server, AWS patcht de onderlaag en de applicatie schaalt vanzelf mee. Maar alles draait bij een Amerikaans cloudbedrijf, en de HTML van onze site ging zonder CDN rechtstreeks naar Lambda. Elk bezoek startte dus een PHP-proces in Frankfurt, ook voor een pagina die al uren niet was veranderd.
Brewless draait Laravel op Serverless Containers van Scaleway, in het eigen Scaleway-account van de klant, met bunny.net als CDN ervoor. Nieuwe projecten zetten we daar standaard neer. Het was tijd dat onze eigen site hetzelfde deed. En omdat we toch gingen verhuizen, hebben we het gemeten.
Wat we gedaan hebben
Onze website heeft geen eigen database met klantgegevens. Sessies staan in een cookie en bestanden in een bucket. Dat maakt hem een overzichtelijke eerste kandidaat. De stappen:
- Pakket installeren.
composer require brewless/laravel. Er valt niets te registreren; het pakket regelt onder meer dat de container weet onder welke domeinnaam de bezoeker binnenkomt, en zet de juiste cache-headers voor de edge. - Omgeving beschrijven. Een
brewless.ymlin de root van het project, naast de bestaandevapor.yml. Daarin staan de PHP-versie (8.5), de regio (nl-ams), het geheugen van de container, de queue en hoe lang de edge een pagina bewaart (een dag). De release-commando’s die bij Vapor onderdeploystonden, gingen één op één mee. - Variabelen overzetten. De variabelen gaan naar de Secret Manager in het eigen Scaleway-account. Brewless kent alleen de namen, nooit de waarden.
- Deployen.
brewless deploy productionbouwt een image in het eigen account, draait de release-commando’s, zet de container live en controleert of hij gezond antwoordt. In onze BrewCI-pipeline staat dezelfde stap, zodat elke push naarmainvanzelf live gaat. - Domein omzetten.
brewless domain:addkoppelt coding.agency met een certificaat. Daarna wijst de DNS naar bunny.net in plaats van naar API Gateway van AWS.
Dat is het. Er is geen server ingericht, geen load balancer geconfigureerd en geen certificaat aangevraagd. De meeste tijd ging zitten in het controleren van wat er na de verhuizing gebeurde.
Hoe we gemeten hebben
We maten vlak vóór de verhuizing en opnieuw erna, met hetzelfde script, vanaf dezelfde glasvezelverbinding in Meppel. Vijf pagina’s: de homepage, het kennisbank-overzicht, een artikel, een case en de contactpagina. Per situatie draaiden we drie rondes en namen de mediaan.
- Tijd tot de eerste byte (TTFB): 30 verzoeken per pagina per ronde, met curl.
- Load: oplopend van 1 naar 10 naar 50 gelijktijdige verzoeken op de homepage.
- Origin: dezelfde verzoeken met een unieke querystring, zodat de CDN ze niet kan beantwoorden en we de server zelf meten.
- PageSpeed Insights: drie runs per pagina, mobiel en desktop, via de API van Google.
Echte gebruikersdata uit het Chrome User Experience Report hebben we niet: daarvoor heeft onze site te weinig Chrome-verkeer.
De tijd tot de eerste byte
| Pagina | Vapor (Lambda) | Brewless, uit de cache | Brewless, buiten de cache |
|---|---|---|---|
| Homepage | 205 ms | 50 ms | 150–163 ms |
| Kennisbank | 319 ms | 52 ms | 259–266 ms |
| Artikel | 225 ms | 52 ms | 212–235 ms |
| Case | 178 ms | 54 ms | 131–137 ms |
| Contact | 176 ms | 54 ms | 128 ms |
Het verschil zit bijna helemaal in de CDN. Op Vapor stuurde de site wel een header mee dat een pagina een uur bewaard mocht worden, maar er stond niets voor de site dat daar iets mee deed. Nu bewaart bunny.net een pagina een dag lang op de edge in Amsterdam, en vraagt hij hem pas na een nieuwe deploy opnieuw op.
Server tegen server ontlopen de twee elkaar weinig. Een verzoek dat de cache mist, is op Brewless ongeveer even snel als op Lambda. Met één kanttekening: de servertijd schommelt meer. Op momenten zagen we de kennisbank-pagina 400 tot 1.200 milliseconden nodig hebben, waar dat een uur eerder 200 was.
Onder load: weg met de koude starts
Bij oplopend verkeer liet Lambda een vast patroon zien. Zodra er meer gelijktijdige verzoeken kwamen, moest AWS nieuwe instanties opstarten, en die eerste verzoeken duurden 3,4 tot 4 seconden. Wie op dat moment de site opende, wachtte.
| Gelijktijdig | Vapor: verzoeken/s | Vapor: traagste | Brewless: verzoeken/s | Brewless: traagste |
|---|---|---|---|---|
| 1 | 4,2 | 1,1 s | 13,0 | 0,14 s |
| 10 | 40,8 | tot 3,8 s | 111 | 0,17 s |
| 50 | 123 | tot 4,1 s | 148 | tot 2,3 s |
Dit is de homepage, en op Brewless komt die uit de cache. Dat is ook de situatie die een bezoeker meemaakt: de container hoeft er niet voor wakker te worden. Bij 50 gelijktijdige verzoeken was onze eigen internetverbinding waarschijnlijk de grens, niet bunny.net.
Buiten de cache: daar moesten we afstellen
De eerlijke vergelijking is server tegen server, zonder CDN ertussen. Daar viel Brewless de eerste keer tegen. Met unieke URL’s, zodat elk verzoek de container bereikte, haalde hij 7,6 verzoeken per seconde bij 10 gelijktijdige verzoeken. Bij 50 liep de wachttijd op tot 6,5 seconden. Lambda deed in dezelfde test 41 tot 123 verzoeken per seconde.
De oorzaak stond in de instellingen. Een container met 1024 MB geheugen krijgt bij Scaleway 0,56 vCPU. FrankenPHP draait er vier PHP-threads in. En Scaleway stuurde veel meer verzoeken naar één container dan die vier threads aankonden, voordat hij een tweede startte. De verzoeken stonden dus in de rij bij een container die vol zat, terwijl het platform vond dat er nog ruimte was.
De oplossing was één blok in brewless.yml:
web:
memory: 2048
cpu: 1120
min: 0
max: 5
concurrency: 4
Met concurrency: 4 krijgt een container nooit meer verzoeken tegelijk dan hij threads heeft; het vijfde verzoek gaat naar een andere container. min: 0 houden we aan: de container mag slapen als er niemand is, want bijna alle bezoekers krijgen toch de cache. Daarna haalde de origin 16,8 verzoeken per seconde bij 10 gelijktijdige verzoeken: ruim twee keer zoveel. Bij 50 gelijktijdige verzoeken was het 11,5 per seconde, met wachttijden die nog tot 8 seconden opliepen terwijl de nieuwe containers koud opstartten.
Daarmee blijft Lambda bij een plotselinge piek buiten de cache sterker. Dat is het principe: Lambda start voor elk gelijktijdig verzoek een eigen instantie, Brewless schaalt per container van vier verzoeken. Voor onze site maakt dat in de praktijk niets uit, omdat vrijwel elk bezoek uit de edge komt. Voor een applicatie waar elke pagina per gebruiker verschilt, zoals een klantportaal, kies je de CPU en het minimum aantal containers met dat in je achterhoofd.
Wat we onderweg tegenkwamen
- Sessies op publieke pagina’s. De CDN bewaart alleen pagina’s zonder cookies. Onze Markdown-versies van pagina’s, die we voor AI-assistenten aanbieden, kregen een sessiecookie mee. Daardoor bewaarde bunny.net ze nooit en wekte elk verzoek de container. Die routes draaien nu zonder sessie.
- Locks in de cache. Onze OG-afbeeldingen worden gemaakt met een lock, zodat dezelfde afbeelding niet twee keer tegelijk wordt gegenereerd. De cache in een bucket, die Brewless standaard instelt, had in de eerste versie geen locks. Dat gaf fouten tot het pakket brewless/laravel er in versie 0.21 een lock bij kreeg.
- Een server die op zichzelf wacht. Een OG-afbeelding is een screenshot van de eigen pagina. Het verzoek voor de afbeelding houdt een PHP-thread vast, terwijl de screenshotdienst de pagina opvraagt bij dezelfde container. Met vier threads kunnen vier van die verzoeken alles blokkeren. Dat zagen we één keer, als vier time-outs tegelijk. Met concurrency 4 gaat de pagina-aanvraag naar een andere container.
Geen van deze punten is uniek voor Brewless. Ze zijn het gevolg van twee dingen die anders zijn dan op Lambda: een CDN die pagina’s echt bewaart, en containers die meerdere verzoeken tegelijk verwerken.
En PageSpeed?
De PageSpeed-scores gingen ook omhoog. De homepage scoort mobiel nu 100 in plaats van 96, met een Largest Contentful Paint van 1,2 in plaats van 2,7 seconden. Op desktop scoorden de meeste pagina’s al 99 of 100.
Daarbij helpt dat CSS en JavaScript nu van ons eigen domein komen, via dezelfde CDN als de pagina, in plaats van van een aparte CloudFront-host. Op een mobiele verbinding scheelt dat het opzetten van een extra verbinding voordat de pagina kan renderen.
De kern in vijf zinnen
- De verhuizing van Laravel Vapor naar Brewless kostte een middag: een pakket, een
brewless.yml, de variabelen overzetten en deployen. - Bezoekers krijgen pagina’s nu in ongeveer 50 ms in plaats van 176 tot 319 ms, omdat bunny.net ze in Amsterdam bewaart.
- De koude starts van Lambda merken bezoekers niet meer, omdat de container voor een gecachte pagina niet hoeft te starten.
- Buiten de cache is Lambda bij pieken sterker; met meer CPU en concurrency gelijk aan de PHP-threads verdubbelde Brewless zijn capaciteit.
- Stem concurrency af op het aantal PHP-threads en houd publieke pagina’s vrij van sessiecookies, dan doet de CDN het zware werk.
Zelf overstappen?
Draait je Laravel-applicatie op Vapor, Forge of een eigen server en wil je weten wat een overstap naar Brewless voor jou betekent? Neem contact op, dan kijken we samen naar je applicatie.