All posts Engineering

Why Lettr runs on AWS Lambda with Bref

Why Lettr, a Laravel application, runs on AWS Lambda through Bref: the PHP runtimes and single-file deployment Bref provides, how Lettr's serverless.yml splits HTTP, queues, S3 events, and the scheduler into separate functions with no AWS keys anywhere, the Lambda limits we hit, and why Matthieu Napoli deserves the credit.

Jakub Gause
Jakub Gause
CEO
6 min read

Lettr, our email API, is a Laravel application that has never had a server. Every HTTP request, queue job, cron run, and webhook is an AWS Lambda invocation, and what makes a Laravel app run on Lambda at all is Bref, an open-source project by Matthieu Napoli. We first chose Bref for our email editor, Topol, in August 2022, two and a half years before Laravel Cloud existed. When we started building Lettr in December 2025, Bref was the obvious choice. This article covers what Bref does for us, where Lambda's limits are, and why Matthieu deserves the credit.

A 40-line file from August 2022

This is the first serverless.yml we ever committed, on 29 August 2022, in the Topol repository:

serverless.yml
service: topol-app

provider:
    name: aws
    region: eu-west-1
    runtime: provided.al2

functions:
    app:
        handler: public/index.php
        timeout: 28 # API Gateway has a timeout of 29 seconds
        memorySize: 2048
        layers:
            - ${bref:layer.php-80-fpm}
        events:
            -   httpApi: '*'

package:
    exclude:
        - .ebextensions/**
        - node_modules/**
        - storage/**
        - tests/**

plugins:
    - ./vendor/bref/bref
    - ./vendor/bref/extra-php-extensions

The .ebextensions/** exclusion in this file is a leftover from what Bref replaced. Topol had run on AWS Elastic Beanstalk since 2017: EC2 instances, an Apache config in the repository, deploy hooks to keep storage writable, and instances that needed patching. Bref 1.7 on PHP 8.0 replaced all of this with one function and one file. We spent the following month moving secrets to Parameter Store, adding a GD layer, and trying PHP 8.1. We deleted the Elastic Beanstalk folder in November 2022, and the Topol app has not had a server to patch since.

As of September 2026, this file is 251 lines across 126 commits. It runs PHP 8.4, SQS-driven queue workers, and a long list of scheduled commands. Bref has gone from 1.7 to 2 underneath. Lettr started on Bref 2 and moved to Bref 3 in March 2026, and that upgrade was a 28-line diff to serverless.yml: layers: [${bref:layer.php-84-fpm}] became runtime: php-84-fpm. Three years and three major versions later, the whole deployment is still a single file the team can read.

What Bref does

Lambda does not run PHP. Bref's core contribution is a set of open-source PHP runtimes for Lambda, built and published for every supported PHP version in every AWS region and kept current with PHP releases. Lettr runs on PHP 8.5. The runtime was published before we needed it, and the upgrade was runtime: php-85-fpm. Building and maintaining these runtimes is the expensive part of running PHP on Lambda, and it is the part no team wants to take on themselves.

On top of the runtimes, Bref adds:

  • One file for the whole deployment. A single serverless.yml covers functions, events, memory, timeouts, IAM, and environment variables. serverless deploy turns it into CloudFormation, so anything AWS can do, the file can do too.
  • Per-function scaling. Each entry under functions: scales independently. Our API and our web UI are the same public/index.php, deployed twice with different memory, because they have different traffic.
  • Pay per invocation. There is no idle capacity, so a queue that receives no messages overnight costs nothing overnight.
  • A Laravel bridge that handles what Laravel assumes about a filesystem: caches written under /tmp, compiled views, logs sent to stderr so they land in CloudWatch, sessions and cache stored in DynamoDB, and a ready-made SQS handler for the queue worker.
  • Lift, the companion plugin that builds the S3 bucket and CloudFront distribution for static assets from a few lines of config.

None of this is proprietary. The runtimes are public, the code is on GitHub, and the output is standard CloudFormation.

How Lettr uses Bref

Lettr's serverless.yml follows the same pattern as Topol's, with more functions. Here is a trimmed version, with names generalised:

serverless.yml
provider:
    iam:
        role:
            statements:         # the execution role is the only AWS credential the app has
                - Effect: Allow
                  Action: [dynamodb:GetItem, dynamodb:PutItem, dynamodb:Query]
                  Resource: arn:aws:dynamodb:<region>:<account>:table/<table>
                - Effect: Allow
                  Action: [sqs:SendMessage, sqs:DeleteMessage]
                  Resource: arn:aws:sqs:<region>:<account>:<queue>
                # ... one statement per table, bucket and queue the app touches

functions:
    web:
        handler: public/index.php
        runtime: php-85-fpm
        events:
            - httpApi: '*'

    api:                        # same code, separate Lambda, scales on its own
        handler: public/index.php
        runtime: php-85-fpm
        events:
            - httpApi: { path: '/api/{proxy+}', method: '*' }

    <name>-queue:
        handler: Bref\LaravelBridge\Queue\QueueHandler
        runtime: php-85
        events:
            - sqs: { arn: arn:aws:sqs:<region>:<account>:<queue>, batchSize: 1 }
    # ... one function per queue, each with its own timeout and memory

    webhook-processor:
        handler: handlers/webhooks.php
        runtime: php-85
        events:
            - s3: { bucket: <webhooks-bucket>, event: 's3:ObjectCreated:*' }

    artisan:
        handler: artisan
        runtime: php-85-console
        events:
            - schedule: { rate: rate(1 minute), input: '"schedule:run"' }

HTTP. Three FPM functions run the same Laravel codebase: web for the Inertia app, api for the REST API, and mcp for the MCP server, which gets its own Lambda so the Passport keys it needs never reach the other two.

Queues. Each SQS queue is a separate Lambda with its own timeout and memory. Campaign sends, audience exports, and the AI assistant's long-running template generation all have their own, so a slow job never delays a fast one. SQS invokes the Bref queue handler directly, so there is no worker process to supervise and no Horizon dashboard to watch. Every job is one Lambda invocation with its own log stream, and a campaign to a large audience runs across many workers at once.

Events. Delivery webhooks land in S3 as batches, and S3 fires a Lambda for each object. The handler is forty lines:

handlers/webhooks.php
return new class extends S3Handler
{
    public function handleS3(S3Event $event, Context $context): void
    {
        foreach ($event->getRecords() as $record) {
            app(ProcessWebhookBatch::class)(
                $record->getBucket()->getName(),
                urldecode($record->getObject()->getKey()),
            );
        }
    }
};

Scheduler. One console function runs schedule:run every minute, and Laravel's scheduler decides what is due. Topol's serverless.yml listed each cron entry separately; Lettr's needs one line.

Everything else. Sessions and cache live in DynamoDB. Secrets are ${ssm:/...} references resolved at deploy time, so no secret is ever in the repository or the CI log. Lift builds the CloudFront distribution in front of assets.lettr.com.

No AWS keys anywhere. Every DynamoDB table, S3 bucket, and SQS queue the application touches is granted through the function's own IAM role, declared in the iam block of serverless.yml, so the AWS SDK authenticates with the Lambda execution role. The same applies to deployment: a GitHub Actions job runs serverless deploy on every push to main, after the test suite, by assuming a role through OIDC. No long-lived AWS credential exists in the repository, in CI secrets, or in the Lambda environment.

What Bref does not do

Bref's documentation states Lambda's limits up front, and we hit the real ones.

28 seconds per HTTP request. API Gateway cuts off at 29 seconds. Our AI assistant's chat turn originally ran inside the request and regularly outlived it, so it now runs on a long-timeout queue and the browser receives the result over a broadcast.

4 KB of environment variables per function. Lambda merges provider-level and function-level environment variables and caps the total. Adding one token pushed a queue function 69 bytes over the limit. The fix was deleting variables that matched their config defaults, which we should have done anyway.

No GD in the base runtime. Our brand-kit pipeline measures logo luminance and generates a light/dark twin. Without the GD image library it skipped both, and a white logo published white on white. bref/extra-php-extensions provides the layer; the diagnosis took longer than the fix.

15 minutes per invocation, and no long-lived connections. Lettr's SMTP endpoint holds TCP connections open, which Lambda cannot do. This service is a small Go program on ECS, and it is the only piece of Lettr that is not a Lambda function.

Thank you, Matthieu Napoli

Bref is written and maintained by Matthieu Napoli, who funds the work through GitHub Sponsors. He also maintains oss-serverless, the open-source continuation of the Serverless Framework CLI after version 4 moved to a commercial licence, which is why our deploy step still runs serverless deploy without a subscription. Other contributors like Marco Deleu and Thomas Richard have also carried significant parts of the work.

We have built two commercial products on Bref, one of them since 2022. Every PHP release we wanted has had a runtime ready, and every major upgrade has been boring. This is what good open-source maintenance looks like from the outside. For a company that runs PHP on Lambda, sponsoring Matthieu is a small line next to the AWS bill.

FAQ

Why Bref and not Laravel Vapor or Laravel Cloud?
Bref keeps the deployment as a plain file in the repository, on standard CloudFormation, with nothing between us and AWS. When Topol moved in 2022, Laravel Cloud was still two and a half years away, and Vapor existed but did not give us that. After Laravel Cloud launched we started Lettr and chose Bref almost immediately.
Does cold start matter for an email API?
Rarely. Bref's published figures put cold starts under 0.5% of invocations, and a warm invocation adds a few milliseconds of overhead. Sending email is asynchronous by nature: the API accepts the message and the queue does the rest.
What does it cost?
Lambda bills per invocation and per millisecond, SQS per message, and DynamoDB per request. There is no reserved capacity and no instance to size. The AWS free tier covers roughly a million Lambda requests a month, which is enough to run a serious side project for nothing.
Can I run my Laravel app on Lambda with Bref?
Almost certainly. If it runs on a traditional server, the Bref Laravel guide covers the whole setup. The things that will not work are long-running processes and anything that writes outside /tmp, and Bref's documentation lists them up front.

Bottom line

Bref turned running Laravel on Lambda from a project into a deployment target. We made this bet for Topol in 2022 with a 40-line file, and it has held through three PHP versions, three Bref major versions, and a second product. Lettr's infrastructure is one YAML file, a queue handler, and an S3 event, and none of it is a server anyone has to maintain.

For a Laravel application that sends email, create a free Lettr account and start with the Laravel quickstart.