<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hermann D. Schimpf</title>
    <description>The latest articles on DEV Community by Hermann D. Schimpf (@hschimpf).</description>
    <link>https://dev.to/hschimpf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F362439%2F9af79350-c363-40b7-bae7-80b34b35bbac.png</url>
      <title>DEV Community: Hermann D. Schimpf</title>
      <link>https://dev.to/hschimpf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hschimpf"/>
    <language>en</language>
    <item>
      <title>What Replaced queue:work in My Laravel App (And Why I Stopped Praying to Supervisor at 3 AM)</title>
      <dc:creator>Hermann D. Schimpf</dc:creator>
      <pubDate>Fri, 16 Oct 2026 15:39:41 +0000</pubDate>
      <link>https://dev.to/hschimpf/what-replaced-queuework-in-my-laravel-app-and-why-i-stopped-praying-to-supervisor-at-3-am-41nh</link>
      <guid>https://dev.to/hschimpf/what-replaced-queuework-in-my-laravel-app-and-why-i-stopped-praying-to-supervisor-at-3-am-41nh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Versión en español &lt;a href="https://dev.to/hschimpf/que-reemplazo-a-queuework-en-mi-aplicacion-laravel-y-por-que-deje-de-rezarle-a-supervisor-a-las-3-42ph"&gt;aquí&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71jkhpl0rgbr22l5h85k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71jkhpl0rgbr22l5h85k.png" alt="Banner Image" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In the &lt;a href="https://dev.to/hschimpf/where-does-my-laravel-apps-data-live-in-an-ephemeral-environment-where-not-even-the-disk-can-save-4k18"&gt;previous chapter&lt;/a&gt; we figured out where our data lives: files in &lt;strong&gt;S3&lt;/strong&gt;, cache and sessions in &lt;strong&gt;DynamoDB&lt;/strong&gt;, and a NoSQL database that speaks Eloquent. But one question was left hanging in the air: if a Lambda function is born, responds, and dies in milliseconds, who stays awake processing jobs in the background?&lt;/p&gt;

&lt;p&gt;On a traditional server, the answer is &lt;code&gt;supervisord&lt;/code&gt; babysitting a long-running process. In an ephemeral environment where nothing lives longer than 15 minutes, that recipe simply doesn't work.&lt;/p&gt;

&lt;p&gt;In this chapter we're going to replace that daemon with &lt;strong&gt;SQS + a Lambda worker&lt;/strong&gt;. We'll do it in three acts: first the setup &lt;em&gt;(surprisingly short)&lt;/em&gt;, then the lifecycle of a job &lt;em&gt;(where the magic happens)&lt;/em&gt;, and finally the topic almost nobody explains in full: what happens when a job fails, who retries it, and where it ends up.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Serverless Treasure Map
&lt;/h2&gt;

&lt;p&gt;Quick recap, so we know where we are:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbrh82u77jvw4h7p18azb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbrh82u77jvw4h7p18azb.png" alt="AWS Serverless architecture" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We already have &lt;strong&gt;Lambda&lt;/strong&gt; running our Laravel app, &lt;strong&gt;CloudFront&lt;/strong&gt; splitting traffic between &lt;strong&gt;S3&lt;/strong&gt; &lt;em&gt;(assets)&lt;/em&gt; and &lt;strong&gt;Lambda&lt;/strong&gt; &lt;em&gt;(the app)&lt;/em&gt;, and &lt;strong&gt;DynamoDB&lt;/strong&gt; serving as cache, sessions, and our primary database. Today's agenda:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Managing our event queue with &lt;strong&gt;SQS&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;and a second Lambda function for the Laravel Queue &lt;em&gt;worker&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember the component table from the &lt;a href="https://dev.to/hschimpf/how-i-managed-to-take-my-laravel-application-to-the-serverless-world-and-how-i-almost-died-trying-3mhd"&gt;second chapter&lt;/a&gt;? Today we cross off this row:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Dedicated Server&lt;/th&gt;
&lt;th&gt;AWS Serverless&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Event Queue&lt;/td&gt;
&lt;td&gt;&lt;del&gt;SSH $ &lt;code&gt;php artisan queue:work&lt;/code&gt; / maybe supervisor&lt;/del&gt;&lt;/td&gt;
&lt;td&gt;SQS + Lambda 🚀&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  2. Why &lt;code&gt;queue:work&lt;/code&gt; Doesn't Fit in Serverless
&lt;/h2&gt;

&lt;h3&gt;
  
  
  a) How queues work on a traditional server
&lt;/h3&gt;

&lt;p&gt;Oversimplifying a bit, &lt;code&gt;php artisan queue:work&lt;/code&gt; is a &lt;code&gt;while (true)&lt;/code&gt; loop: it asks the queue if there's any work; if there is, it processes it; if not, it sleeps for a few seconds and asks again. A resident process hogging memory and CPU 24/7, even if the queue has been empty for 3 days.&lt;/p&gt;

&lt;p&gt;And because it's a long-running process, it needs a babysitter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;supervisor&lt;/strong&gt; &lt;em&gt;(or systemd)&lt;/em&gt; to resurrect it when it dies.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;queue:restart&lt;/code&gt; after every deploy, because the process still has the old code loaded in memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--max-jobs&lt;/code&gt; and &lt;code&gt;--max-time&lt;/code&gt; to recycle it before memory leaks turn it into a black hole.&lt;/li&gt;
&lt;li&gt;And to scale? More processes → more RAM → bigger instances → all accompanied by a bigger bill.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;queue:work&lt;/code&gt; is an employee getting paid to hit F5 on their inbox every three seconds. Eight hours a day. Even when nothing comes in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) Why that doesn't exist in Lambda
&lt;/h3&gt;

&lt;p&gt;A Lambda invocation lives, at most, for 15 minutes &lt;sup id="fnref1"&gt;1&lt;/sup&gt;. There is no resident process, no &lt;code&gt;while (true)&lt;/code&gt;, and nobody asking &lt;em&gt;"got any work?"&lt;/em&gt;. If you try running &lt;code&gt;queue:work&lt;/code&gt; inside a Lambda... it works for 15 minutes, dies, and nobody brings it back. And if you resurrect it with a cron job, you're paying for a Lambda 24/7 just to poll. Congratulations, you just reinvented EC2, only more expensive.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Flipping the model: from &lt;em&gt;pull&lt;/em&gt; to &lt;em&gt;push&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;The solution isn't to &lt;em&gt;adapt&lt;/em&gt; the worker to Lambda—it's to flip it upside down. Instead of the worker &lt;strong&gt;pulling&lt;/strong&gt; work &lt;em&gt;(pull)&lt;/em&gt;, the queue &lt;strong&gt;invokes&lt;/strong&gt; the worker when work arrives &lt;em&gt;(push)&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Laravel calls &lt;code&gt;dispatch()&lt;/code&gt; and pushes the job to SQS.&lt;/li&gt;
&lt;li&gt;SQS detects new messages and invokes a Lambda function with those messages.&lt;/li&gt;
&lt;li&gt;Lambda processes the jobs, finishes, and disappears.&lt;/li&gt;
&lt;li&gt;An empty queue = zero Lambda invocations.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The &lt;strong&gt;worker&lt;/strong&gt; stops being a process that &lt;strong&gt;waits&lt;/strong&gt; and becomes an event that &lt;strong&gt;happens&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. The New Queue Stack: SQS as the Mailbox, Lambda as the Worker
&lt;/h2&gt;

&lt;h3&gt;
  
  
  a) SQS in 30 seconds for Laravel devs
&lt;/h3&gt;

&lt;p&gt;Amazon SQS &lt;em&gt;(Simple Queue Service)&lt;/em&gt; is a managed message queue &lt;sup id="fnref2"&gt;2&lt;/sup&gt;. The bare minimum you need to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Message&lt;/strong&gt;: for us, the serialized JSON payload of the job. The exact same one you'd see in the &lt;code&gt;payload&lt;/code&gt; column of the &lt;code&gt;jobs&lt;/code&gt; table if you were using the &lt;code&gt;database&lt;/code&gt; driver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility timeout&lt;/strong&gt;: when someone picks up a message, SQS &lt;em&gt;hides&lt;/em&gt; it for X seconds. If nobody deletes it within that window, it reappears so another consumer can try again. It's the equivalent of Laravel's &lt;code&gt;retry_after&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead Letter Queue &lt;em&gt;(DLQ)&lt;/em&gt;&lt;/strong&gt;: a second queue where SQS moves messages that have failed too many times. The &lt;em&gt;"lost and found box"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard vs FIFO&lt;/strong&gt;: standard queues don't guarantee order &lt;em&gt;(and they're the ones we'll use)&lt;/em&gt;; FIFO queues do, in exchange for lower throughput.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;With the vocabulary out of the way, let's jump into the code.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) Wiring it up with Serverless Lift
&lt;/h3&gt;

&lt;p&gt;Starting from the &lt;code&gt;serverless.yml&lt;/code&gt; we built in the &lt;a href="https://dev.to/hschimpf/how-i-managed-to-take-my-laravel-application-to-the-serverless-world-and-how-i-almost-died-trying-3mhd"&gt;second chapter&lt;/a&gt;, we add two environment variables and a Lift construct &lt;sup id="fnref3"&gt;3&lt;/sup&gt; &lt;sup id="fnref4"&gt;4&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;service: laravel
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;provider:
&lt;/span&gt;  name: aws
  environment:
    # ...
&lt;span class="gi"&gt;+   QUEUE_CONNECTION: sqs
+   SQS_QUEUE: ${construct:jobs.queueUrl}
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;functions:
&lt;/span&gt;  web:
    handler: Bref\LaravelBridge\Http\OctaneHandler
    runtime: php-85
    # ...
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;constructs:
&lt;/span&gt;  website:
    type: server-side-website
    # ...
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ jobs:
+   type: queue
+   worker:
+     handler: Bref\LaravelBridge\Queue\QueueHandler
+     runtime: php-85
+     timeout: 60  # seconds, the ceiling for the longest job
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;plugins:
&lt;/span&gt;  - ./vendor/bref/bref
  - serverless-lift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;QUEUE_CONNECTION: sqs&lt;/code&gt; enables the driver Laravel ships with out of the box, and &lt;code&gt;SQS_QUEUE&lt;/code&gt; receives the full URL of the queue Lift just created &lt;em&gt;(if you prefer passing only the queue name, you'll also need &lt;code&gt;SQS_PREFIX&lt;/code&gt; &lt;sup id="fnref3"&gt;3&lt;/sup&gt;)&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Even though on a traditional server the queue runs via CLI with &lt;code&gt;artisan&lt;/code&gt;, here we don't use &lt;code&gt;php-85-console&lt;/code&gt;. Just like with &lt;code&gt;OctaneHandler&lt;/code&gt;, we use the &lt;code&gt;php-85&lt;/code&gt; event runtime with Bref's &lt;code&gt;QueueHandler&lt;/code&gt; class.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And what did Lift do behind the scenes with that &lt;code&gt;jobs&lt;/code&gt; construct? &lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created the &lt;strong&gt;SQS queue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Created a &lt;strong&gt;Dead Letter Queue&lt;/strong&gt; with a 14-day retention period &lt;em&gt;(the maximum SQS allows)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Created the &lt;code&gt;worker&lt;/code&gt; &lt;strong&gt;Lambda function&lt;/strong&gt; with Bref's handler.&lt;/li&gt;
&lt;li&gt;Configured the &lt;strong&gt;event source mapping&lt;/strong&gt;: the AWS piece that connects the SQS queue to the Lambda function and invokes it when messages arrive. Our replacement for &lt;code&gt;queue:work&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;And granted &lt;code&gt;sqs:SendMessage&lt;/code&gt; permission to &lt;strong&gt;all&lt;/strong&gt; functions in the stack &lt;sup id="fnref5"&gt;5&lt;/sup&gt;. That's why, unlike the DynamoDB tables from the &lt;a href="https://dev.to/hschimpf/where-does-my-laravel-apps-data-live-in-an-ephemeral-environment-where-not-even-the-disk-can-save-4k18"&gt;previous chapter&lt;/a&gt;, we didn't have to write a single line of IAM here.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Credentials, just like with DynamoDB, are assigned to each function's execution role. They rotate automatically and nobody ever sees them.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) New infrastructure, same old code
&lt;/h3&gt;

&lt;p&gt;This is my favorite part. Nothing you've already written has any idea anything changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A job&lt;/span&gt;
&lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// A queued mailable&lt;/span&gt;
&lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WelcomeMail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// A notification implementing ShouldQueue&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VerifyEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of that goes to SQS, and a Lambda processes it. &lt;code&gt;ShouldQueue&lt;/code&gt; listeners, chained jobs... almost everything works the exact same way. Another point for Laravel's abstraction 💪.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Laravel's SQS driver caps &lt;code&gt;delay()&lt;/code&gt; / &lt;code&gt;#[Backoff]&lt;/code&gt; at 900 seconds (15 minutes), which is the SQS limit &lt;sup id="fnref6"&gt;6&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;If you need &lt;strong&gt;multiple queues&lt;/strong&gt; &lt;em&gt;(one for urgent emails, another with lower concurrency to avoid hammering an external API, a FIFO queue for strict ordering)&lt;/em&gt;, each one is an additional &lt;code&gt;queue&lt;/code&gt; construct, and since Laravel 13.26 they can be routed with &lt;code&gt;Queue::forward()&lt;/code&gt; &lt;sup id="fnref7"&gt;7&lt;/sup&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. What Actually Happens When You Dispatch a Job?
&lt;/h2&gt;

&lt;p&gt;Up to this point it all looks like magic: we added a block in &lt;code&gt;serverless.yml&lt;/code&gt;, called &lt;code&gt;dispatch()&lt;/code&gt;, and the job ran. But in production, magic only lasts until the first incident. Let's walk step by step through &lt;em&gt;who&lt;/em&gt; does &lt;em&gt;what&lt;/em&gt;, from the moment a message enters SQS until it disappears.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Laravel dispatches, SQS receives
&lt;/h3&gt;

&lt;p&gt;When you call &lt;code&gt;SendWelcomeEmail::dispatch($user-&amp;gt;uuid)&lt;/code&gt;, Laravel serializes the job and the &lt;code&gt;sqs&lt;/code&gt; driver makes a &lt;code&gt;SendMessage&lt;/code&gt; call to the SQS API to drop it into the queue. And that's it—our &lt;code&gt;web&lt;/code&gt; Lambda has done its part and can finish its execution.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The message body is the same old JSON (&lt;code&gt;displayName&lt;/code&gt;, &lt;code&gt;job&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt;, &lt;code&gt;attempts&lt;/code&gt;). To Laravel, SQS is just another mailbox to drop that exact same envelope into.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) SQS notifies, Lambda wakes up
&lt;/h3&gt;

&lt;p&gt;The &lt;em&gt;event source mapping&lt;/em&gt; that Lift created handles &lt;em&gt;long polling&lt;/em&gt; for us &lt;sup id="fnref8"&gt;8&lt;/sup&gt;: AWS keeps an internal process polling the queue and, as soon as it detects messages, invokes our &lt;code&gt;worker&lt;/code&gt; Lambda function with a batch of up to &lt;code&gt;batchSize&lt;/code&gt; messages &lt;em&gt;(Lift defaults this to 1; we'll see why and when to change it shortly)&lt;/em&gt;. Polling still exists; the difference is that instead of your &lt;code&gt;queue:work&lt;/code&gt; process doing it, AWS takes care of it now.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: That AWS polling counts as &lt;code&gt;ReceiveMessage&lt;/code&gt; calls in SQS, even when the queue is empty. Fortunately, the free tier includes &lt;strong&gt;1 million monthly requests&lt;/strong&gt; &lt;sup id="fnref9"&gt;9&lt;/sup&gt;: with an idle queue, that constant check generates ~21,000 requests per day &lt;em&gt;(~630,000 per month)&lt;/em&gt;, so you pay nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) Bref receives the batch, Laravel runs the job
&lt;/h3&gt;

&lt;p&gt;When Lambda wakes up, it hands the SQS event to Bref's &lt;code&gt;QueueHandler&lt;/code&gt; &lt;sup id="fnref10"&gt;10&lt;/sup&gt;. If we peek inside its &lt;code&gt;handleSqs()&lt;/code&gt; method &lt;em&gt;(simplified)&lt;/em&gt;, we can see exactly what happens with each message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handleSqs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;SqsEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;makeWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'resetScope'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resetLaravel&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRecords&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$sqsRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;calculateJobTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRemainingTimeInMillis&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="nv"&gt;$worker&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;runSqsJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;marshalJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sqsRecord&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;gatherWorkerOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$timeout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasFailed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isDeleted&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three key details in those few lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;That &lt;code&gt;Worker&lt;/code&gt; is Laravel's.&lt;/strong&gt; It literally extends &lt;code&gt;Illuminate\Queue\Worker&lt;/code&gt; &lt;sup id="fnref11"&gt;11&lt;/sup&gt;. Bref didn't reinvent how to run a job: it passes each message to the exact same engine that &lt;code&gt;queue:work&lt;/code&gt; uses. Direct consequence: &lt;code&gt;#[Tries]&lt;/code&gt;, &lt;code&gt;#[Backoff]&lt;/code&gt;, &lt;code&gt;$timeout&lt;/code&gt;, &lt;code&gt;failed()&lt;/code&gt;, middleware, &lt;code&gt;ShouldBeUnique&lt;/code&gt;, &lt;code&gt;retryUntil()&lt;/code&gt;, and the &lt;code&gt;JobProcessing&lt;/code&gt;/&lt;code&gt;JobProcessed&lt;/code&gt;/&lt;code&gt;JobFailed&lt;/code&gt; events... &lt;strong&gt;everything works just like on a traditional server&lt;/strong&gt;. There's no &lt;em&gt;"supported features"&lt;/em&gt; table to check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State is cleaned up between jobs.&lt;/strong&gt; That &lt;code&gt;resetLaravel()&lt;/code&gt; callback flushes log context, forgets &lt;em&gt;scoped&lt;/em&gt; container instances, and clears resolved facades before each run. Why? For the exact same reason we saw with Octane in the &lt;a href="https://dev.to/hschimpf/how-i-managed-to-take-my-laravel-application-to-the-serverless-world-and-how-i-almost-died-trying-3mhd"&gt;second chapter&lt;/a&gt;: the Lambda instance &lt;em&gt;(and its PHP process)&lt;/em&gt; &lt;strong&gt;survives from one job to the next&lt;/strong&gt;. Without that cleanup, the second job would inherit the in-memory leftovers of the first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleting from SQS is explicit.&lt;/strong&gt; Only if the job finished without failing does Bref call &lt;code&gt;$job-&amp;gt;delete()&lt;/code&gt; to remove it from the queue. If it failed or was already deleted &lt;em&gt;(or released back to the queue, as we'll see next)&lt;/em&gt;, it leaves it alone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  d) Lambda's &lt;code&gt;timeout&lt;/code&gt; vs. the job's &lt;code&gt;$timeout&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Did you spot that &lt;code&gt;calculateJobTimeout($context-&amp;gt;getRemainingTimeInMillis())&lt;/code&gt; inside the loop? Bref doesn't use a fixed timeout: it checks how many milliseconds the Lambda invocation has left to live &lt;em&gt;(minus a 1-second safety margin)&lt;/em&gt; and assigns that time to the job. With that value, the &lt;code&gt;Worker&lt;/code&gt; schedules a &lt;code&gt;pcntl_alarm()&lt;/code&gt; &lt;sup id="fnref11"&gt;11&lt;/sup&gt;: if the job runs over, the alarm goes off, the process exits with &lt;code&gt;exit(1)&lt;/code&gt;, and the invocation fails. The job is only marked as failed &lt;em&gt;(and sent to &lt;code&gt;failed_jobs&lt;/code&gt;)&lt;/em&gt; if you defined &lt;code&gt;#[FailOnTimeout]&lt;/code&gt; or &lt;code&gt;$failOnTimeout = true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two practical takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The worker's &lt;code&gt;timeout&lt;/code&gt; in &lt;code&gt;serverless.yml&lt;/code&gt; is, in practice, &lt;strong&gt;the ceiling for the longest job&lt;/strong&gt; you can process. Set it with your heaviest task in mind.&lt;/li&gt;
&lt;li&gt;With a &lt;code&gt;batchSize&lt;/code&gt; greater than 1, all jobs in the batch &lt;strong&gt;share that same clock&lt;/strong&gt;: if you process long-running tasks, keep the batch small &lt;em&gt;(or leave it at 1)&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch out for &lt;code&gt;#[Timeout]&lt;/code&gt; / &lt;code&gt;$timeout&lt;/code&gt;&lt;/strong&gt;: If you define a custom timeout on the job class, Laravel overrides the value calculated by Bref. If that number exceeds the time remaining in Lambda, AWS will kill the invocation cold before PHP's alarm ever rings. &lt;strong&gt;Recommendation&lt;/strong&gt;: use a value well below the worker's &lt;code&gt;timeout&lt;/code&gt;, especially with &lt;code&gt;batchSize&lt;/code&gt; greater than 1, where the last jobs in the batch start with less time available.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. Who Takes Over When Something Blows Up?
&lt;/h2&gt;

&lt;p&gt;We all love watching the happy path play out by the book: the job comes in, runs, and gets deleted from SQS. But in the real world, external APIs go down, validations fail, and processes run out of memory.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You're probably wondering: &lt;em&gt;"If Lift already created a &lt;strong&gt;Dead Letter Queue&lt;/strong&gt; in SQS and Laravel already has its own &lt;code&gt;failed_jobs&lt;/code&gt; table, aren't they doing &lt;strong&gt;the exact same thing&lt;/strong&gt;?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer is &lt;strong&gt;no&lt;/strong&gt;. When we combine Laravel's &lt;code&gt;Worker&lt;/code&gt; with SQS and Lambda, &lt;strong&gt;two retry and failure mechanisms&lt;/strong&gt; coexist in the same stack: one on our application side and another on the AWS infrastructure side. And even though they sound similar, they tackle completely different problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Retries are handled by Laravel, not SQS
&lt;/h3&gt;

&lt;p&gt;Let's start on our application side. Picture an ordinary job configured with retries and backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Queue\Attributes\Backoff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Queue\Attributes\Tries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Tries(3)]&lt;/span&gt;
&lt;span class="na"&gt;#[Backoff(30)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$user_uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ... if this throws an exception, Laravel retries in 30 seconds, up to 3 times&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ... once all 3 attempts are exhausted, Laravel calls this&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;handle()&lt;/code&gt; throws an exception, Laravel's &lt;code&gt;Worker&lt;/code&gt; does what it always does: if attempts remain, it calls &lt;code&gt;$job-&amp;gt;release(30)&lt;/code&gt;. And here's the trick. Laravel's native &lt;code&gt;SqsJob&lt;/code&gt; simply updates the message's &lt;em&gt;visibility timeout&lt;/em&gt; in SQS, but Bref overrides it and does something else &lt;sup id="fnref12"&gt;12&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;released&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'attempts'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'attempts'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// deletes the original message...&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;deleteMessage&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'QueueUrl'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ReceiptHandle'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ReceiptHandle'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="c1"&gt;// ...and sends a brand-new copy, with the updated attempt counter and backoff delay&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'QueueUrl'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'MessageBody'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'DelaySeconds'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;secondsUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$delay&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;It's not &lt;em&gt;"forwarding"&lt;/em&gt;. It's &lt;strong&gt;deleting&lt;/strong&gt; the email and composing a &lt;strong&gt;brand-new&lt;/strong&gt; one, identical, with &lt;code&gt;[Attempt 2]&lt;/code&gt; in the subject line and scheduled to send in 30 seconds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why? Because that way the attempt counter travels &lt;strong&gt;inside the payload&lt;/strong&gt; &lt;em&gt;(&lt;code&gt;attempts()&lt;/code&gt; reads it from there, not from SQS's &lt;code&gt;ApproximateReceiveCount&lt;/code&gt;)&lt;/em&gt;, and &lt;code&gt;backoff&lt;/code&gt; translates into a real &lt;code&gt;DelaySeconds&lt;/code&gt;. As a result, &lt;code&gt;#[Tries(3)]&lt;/code&gt; and &lt;code&gt;#[Backoff(30)]&lt;/code&gt; behave &lt;strong&gt;exactly&lt;/strong&gt; like they do on a server. Bref deliberately chose Laravel's semantics over SQS's &lt;sup id="fnref3"&gt;3&lt;/sup&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you don't define &lt;code&gt;#[Tries]&lt;/code&gt; / &lt;code&gt;$tries&lt;/code&gt;, Bref's handler defaults to 3 &lt;sup id="fnref13"&gt;13&lt;/sup&gt;. And if you use exponential backoff &lt;em&gt;(&lt;code&gt;#[Backoff([10, 60, 300])]&lt;/code&gt;)&lt;/em&gt;, that works too: each re-queued message carries the delay corresponding to that attempt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) The side effect: SQS never finds out about the failure
&lt;/h3&gt;

&lt;p&gt;When an exception is thrown inside &lt;code&gt;handle()&lt;/code&gt;, Laravel's &lt;code&gt;Worker&lt;/code&gt; catches it in its own &lt;code&gt;try/catch&lt;/code&gt;: it reports it to the logs, decides whether to retry or mark the job as failed, and moves on. Because that exception never bubbles up to &lt;code&gt;handleSqs()&lt;/code&gt;, &lt;strong&gt;as far as AWS Lambda is concerned, the function succeeded without errors&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This means SQS's native retry mechanism &lt;em&gt;(visibility timeout → &lt;code&gt;maxReceiveCount&lt;/code&gt; → DLQ)&lt;/em&gt; &lt;strong&gt;never finds out&lt;/strong&gt; that a job failed due to a PHP exception. Not on the first attempt, and not on the third.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Laravel is that teammate who says "all good" in the daily standup while production is on fire. And SQS believes them.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) First layer: when the job exhausts its attempts in Laravel
&lt;/h3&gt;

&lt;p&gt;When a job fails for the third time and runs out of attempts, Laravel gives up: it calls the class's &lt;code&gt;failed()&lt;/code&gt; method and records the failure using the driver configured in &lt;code&gt;config/queue.php&lt;/code&gt; &lt;em&gt;(&lt;code&gt;queue.failed&lt;/code&gt;)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The problem is that, by default, Laravel tries to save that record in a &lt;code&gt;failed_jobs&lt;/code&gt; table inside a SQL database... which we said goodbye to in the &lt;a href="https://dev.to/hschimpf/where-does-my-laravel-apps-data-live-in-an-ephemeral-environment-where-not-even-the-disk-can-save-4k18"&gt;previous chapter&lt;/a&gt;. Without a relational database, the quick way out is to disable it altogether with &lt;code&gt;QUEUE_FAILED_DRIVER: null&lt;/code&gt; &lt;sup id="fnref3"&gt;3&lt;/sup&gt;: the exception still ends up in CloudWatch logs, but the job itself isn't stored anywhere. I don't recommend it.&lt;/p&gt;

&lt;p&gt;The good news is that Laravel already includes native support for storing &lt;code&gt;failed_jobs&lt;/code&gt; in &lt;strong&gt;DynamoDB&lt;/strong&gt; &lt;sup id="fnref14"&gt;14&lt;/sup&gt;. We only need to tweak &lt;code&gt;config/queue.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'failed'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'QUEUE_FAILED_DRIVER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'database-uuids'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// When using driver=dynamodb there's no need to specify 'database'&lt;/span&gt;
    &lt;span class="c1"&gt;// 'database' =&amp;gt; env('DB_CONNECTION', 'sqlite'),&lt;/span&gt;
    &lt;span class="s1"&gt;'key'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_ACCESS_KEY_ID'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'secret'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_SECRET_ACCESS_KEY'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'token'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_SESSION_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'region'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_DEFAULT_REGION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'us-east-1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'table'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DYNAMODB_FAILED_JOBS_TABLE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'failed_jobs'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;QUEUE_FAILED_DRIVER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb&lt;/span&gt;
        &lt;span class="na"&gt;DYNAMODB_FAILED_JOBS_TABLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;FailedJobsTable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we declare the table under &lt;code&gt;resources&lt;/code&gt; in &lt;code&gt;serverless.yml&lt;/code&gt;. Laravel uses &lt;code&gt;application&lt;/code&gt; as the partition key &lt;em&gt;(the app name)&lt;/em&gt; and &lt;code&gt;uuid&lt;/code&gt; as the sort key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;FailedJobsTable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::DynamoDB::Table&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failed_jobs&lt;/span&gt;
        &lt;span class="na"&gt;BillingMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;
        &lt;span class="na"&gt;AttributeDefinitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;        &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;KeySchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;HASH&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;        &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RANGE&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;TimeToLiveSpecification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;expires_at&lt;/span&gt;
            &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Just like we did in the &lt;a href="https://dev.to/hschimpf/where-does-my-laravel-apps-data-live-in-an-ephemeral-environment-where-not-even-the-disk-can-save-4k18"&gt;previous chapter&lt;/a&gt; for the cache and session tables, we use &lt;code&gt;TimeToLiveSpecification&lt;/code&gt;; Laravel saves each failed job with an expiration timestamp &lt;em&gt;(&lt;code&gt;expires_at&lt;/code&gt;, 7 days by default &lt;sup id="fnref14"&gt;14&lt;/sup&gt;)&lt;/em&gt;. This driver doesn't even implement &lt;code&gt;queue:flush&lt;/code&gt;: DynamoDB takes care of cleaning up expired records automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finally, we grant the execution role permissions on the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;iam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;statements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt;   &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
                    &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;FailedJobsTable.Arn&lt;/span&gt;
                    &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:PutItem&lt;/span&gt;
                        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:Query&lt;/span&gt;
                        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:GetItem&lt;/span&gt;
                        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:DeleteItem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To list or retry failed jobs—since there's no SSH in Lambda—we use the &lt;code&gt;bref:cli&lt;/code&gt; bridge we saw in the &lt;a href="https://dev.to/hschimpf/how-i-managed-to-take-my-laravel-application-to-the-serverless-world-and-how-i-almost-died-trying-3mhd"&gt;second chapter&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless bref:cli &lt;span class="nt"&gt;--args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"queue:failed"&lt;/span&gt;
serverless bref:cli &lt;span class="nt"&gt;--args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"queue:retry all"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Wait, so what is the DLQ that Lift created even for?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  d) Second layer: when the infrastructure fails
&lt;/h3&gt;

&lt;p&gt;A message only reaches the &lt;em&gt;Dead Letter Queue&lt;/em&gt; when &lt;strong&gt;the entire Lambda invocation fails&lt;/strong&gt;, without giving Laravel a chance to catch the error. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A job exceeded the remaining invocation time (&lt;code&gt;pcntl_alarm&lt;/code&gt; went off and execution terminated with &lt;code&gt;exit(1)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;It ran out of memory &lt;em&gt;(OOM)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;A fatal PHP error outside the scope of the &lt;code&gt;Worker&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A boot failure: a missing extension, a configuration error, a &lt;code&gt;composer install&lt;/code&gt; that left something out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In any of these scenarios, Lambda dies before Bref can delete the message, so SQS's native mechanism kicks in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wait (&lt;em&gt;visibility timeout&lt;/em&gt;)&lt;/strong&gt;: the message stays hidden until its visibility timeout expires. Lift automatically sets it to &lt;strong&gt;6 times the worker's &lt;code&gt;timeout&lt;/code&gt;&lt;/strong&gt; following AWS's recommendation &lt;sup id="fnref15"&gt;15&lt;/sup&gt; &lt;em&gt;(with the 60 seconds from our example, it reappears after 6 minutes)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry&lt;/strong&gt;: once visible again, SQS hands it back to Lambda.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Off to the DLQ&lt;/strong&gt;: if the invocation fails 3 times in a row &lt;em&gt;(&lt;code&gt;maxRetries&lt;/code&gt; by default in Lift)&lt;/em&gt;, SQS removes the message from the main queue and stores it in the &lt;strong&gt;Dead Letter Queue&lt;/strong&gt; for up to &lt;strong&gt;14 days&lt;/strong&gt;, preventing it from being lost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To inspect what landed in the DLQ or re-queue it without opening the AWS console, Lift gives us these commands &lt;sup id="fnref16"&gt;16&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless &lt;span class="nb"&gt;jobs&lt;/span&gt;:logs &lt;span class="nt"&gt;--tail&lt;/span&gt;      &lt;span class="c"&gt;# live worker logs&lt;/span&gt;
serverless &lt;span class="nb"&gt;jobs&lt;/span&gt;:failed           &lt;span class="c"&gt;# inspect messages in the DLQ&lt;/span&gt;
serverless &lt;span class="nb"&gt;jobs&lt;/span&gt;:failed:retry     &lt;span class="c"&gt;# move everything back to the main queue&lt;/span&gt;
serverless &lt;span class="nb"&gt;jobs&lt;/span&gt;:failed:purge     &lt;span class="c"&gt;# delete everything and move on with life&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, nobody wants to run &lt;code&gt;serverless jobs:failed&lt;/code&gt; every day just in case. If you add the &lt;code&gt;alarm&lt;/code&gt; property to the construct, Lift automatically sets up a CloudWatch alarm that sends you an email as soon as a message lands in the DLQ &lt;sup id="fnref4"&gt;4&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;constructs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;queue&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
        &lt;span class="na"&gt;alarm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alerts@my-domain.com&lt;/span&gt;  &lt;span class="c1"&gt;# email alert when the DLQ receives messages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;After the first &lt;code&gt;serverless deploy&lt;/code&gt;, AWS will send a confirmation email to the address configured in &lt;code&gt;alarm&lt;/code&gt; &lt;em&gt;(don't forget to confirm it to activate the alerts)&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  e) The two layers, side by side
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;First layer: &lt;code&gt;failed_jobs&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Second layer: SQS DLQ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What failed?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Our code: exception in &lt;code&gt;handle()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Infrastructure: timeout, OOM, fatal error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Who retries?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Laravel (&lt;code&gt;#[Tries]&lt;/code&gt;, &lt;code&gt;#[Backoff]&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;SQS (&lt;em&gt;visibility timeout&lt;/em&gt;, &lt;code&gt;maxRetries&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What gets retried?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;That job, after configured &lt;code&gt;backoff&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The in-flight job and rest of batch (6× timeout)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Where does it end?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;failed_jobs&lt;/code&gt; table (DynamoDB, 7d)&lt;/td&gt;
&lt;td&gt;Dead Letter Queue (SQS, 14 days)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;How do I retry it?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bref:cli --args="queue:retry ..."&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;serverless jobs:failed:retry&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;How do I find out?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;failed()&lt;/code&gt;, CloudWatch, &lt;code&gt;queue:failed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Email alert (&lt;code&gt;alarm:&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Take a look at the &lt;strong&gt;What gets retried?&lt;/strong&gt; row in the second layer. When Lambda dies mid-batch &lt;em&gt;(due to timeout or OOM)&lt;/em&gt;, the jobs that had already finished were deleted one by one with &lt;code&gt;$job-&amp;gt;delete()&lt;/code&gt;, so they don't run again. SQS only re-delivers &lt;strong&gt;what remained undeleted&lt;/strong&gt;: the job that was halfway through execution and the ones waiting behind it in the batch.&lt;/p&gt;

&lt;p&gt;The danger lies in that half-finished job: when SQS retries it, &lt;strong&gt;it will run again from the very beginning&lt;/strong&gt;. If it had already charged a credit card or sent an email a second before Lambda died and isn't &lt;strong&gt;idempotent&lt;/strong&gt;, it will do it a second time &lt;em&gt;(and you'll get acquainted with the word "refund" much sooner than you'd like)&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Golden rule: always design idempotent jobs.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. Tuning the Worker Knobs for Production
&lt;/h2&gt;

&lt;p&gt;The 8 lines from section 3 are enough to get started, but now that we know what happens under the hood, we can tune the knobs for production. Here's what a battle-tested configuration looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;constructs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;queue&lt;/span&gt;
        &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;       &lt;span class="c1"&gt;# queue maxReceiveCount: AWS recommends &amp;gt;= 5&lt;/span&gt;
        &lt;span class="na"&gt;maxConcurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;  &lt;span class="c1"&gt;# the "clean" brake: Lambda stops polling, messages wait&lt;/span&gt;
        &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bref\LaravelBridge\Queue\QueueHandler&lt;/span&gt;
            &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-85&lt;/span&gt;
            &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${bref-extra:gd-php-85}&lt;/span&gt;
            &lt;span class="na"&gt;reservedConcurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;25&lt;/span&gt;  &lt;span class="c1"&gt;# hard cap &amp;amp; reservation; always &amp;gt;= maxConcurrency&lt;/span&gt;
            &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
        &lt;span class="na"&gt;batchSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  a) &lt;code&gt;timeout&lt;/code&gt;: the ceiling for the longest job
&lt;/h3&gt;

&lt;p&gt;It defines both the maximum duration of an invocation and the queue's &lt;em&gt;visibility timeout&lt;/em&gt; in SQS &lt;em&gt;(which Lift multiplies by 6)&lt;/em&gt;. In my case I bumped it to 300 seconds because the worker processes heavy images, and resizing a 20 MB photo takes time. If your jobs only send emails or call an API, 30 or 60 seconds is more than enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) &lt;code&gt;batchSize&lt;/code&gt;: fewer invocations, more responsibility
&lt;/h3&gt;

&lt;p&gt;Lift leaves it at 1 by default to keep error handling simple &lt;sup id="fnref17"&gt;17&lt;/sup&gt;. Raising it to 10 means a burst of 500 jobs is processed in 50 invocations instead of 500, reducing &lt;em&gt;cold-starts&lt;/em&gt; and costs. In exchange, remember the two conditions we covered earlier: all jobs in the batch &lt;strong&gt;share the same Lambda clock&lt;/strong&gt;, and our jobs must be &lt;strong&gt;idempotent&lt;/strong&gt; in case an invocation dies mid-batch.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There's also &lt;code&gt;maxBatchingWindow&lt;/code&gt;: how many seconds SQS waits to gather a full batch before invoking Lambda. Zero by default. If you don't mind a job waiting a few seconds for company to arrive, you can raise it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) &lt;code&gt;reservedConcurrency&lt;/code&gt; vs &lt;code&gt;maxConcurrency&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If 1,000 jobs arrive at once, Lambda scales up fast to hundreds of simultaneous invocations. And that sounds wonderful until those 1,000 jobs slam into a rate-limited external API, our SMTP server, or DynamoDB itself all at the same time.&lt;/p&gt;

&lt;p&gt;There are two ways to put a ceiling on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reservedConcurrency&lt;/code&gt;&lt;/strong&gt; &lt;em&gt;(function property)&lt;/em&gt;: the function never runs more than N simultaneous instances. But it also &lt;strong&gt;reserves&lt;/strong&gt; those N from your account's total concurrency quota, even when sitting idle &lt;sup id="fnref18"&gt;18&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;maxConcurrency&lt;/code&gt;&lt;/strong&gt; &lt;em&gt;(Lift construct property)&lt;/em&gt;: limits how many concurrent invocations this queue's event source mapping can trigger, without reserving anything &lt;sup id="fnref19"&gt;19&lt;/sup&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sweet spot is combining both: &lt;code&gt;maxConcurrency&lt;/code&gt; as a clean brake &lt;em&gt;(messages wait in the queue without burning attempts)&lt;/em&gt; and &lt;code&gt;reservedConcurrency&lt;/code&gt; slightly above it, as a safety net in case a bug dispatches jobs in an infinite loop.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up&lt;/strong&gt;: if you use only &lt;code&gt;reservedConcurrency&lt;/code&gt;, anything above that limit gets &lt;em&gt;throttled&lt;/em&gt;, and every rejection counts as a delivery attempt in SQS. With &lt;code&gt;maxRetries: 3&lt;/code&gt; &lt;em&gt;(Lift's default)&lt;/em&gt;, a traffic spike can send jobs straight to the DLQ without ever having executed once. That's why AWS recommends setting &lt;code&gt;maxRetries&lt;/code&gt; to at least 5 &lt;sup id="fnref20"&gt;20&lt;/sup&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  d) &lt;code&gt;memorySize&lt;/code&gt;: more memory is also more CPU
&lt;/h3&gt;

&lt;p&gt;As we saw in the &lt;a href="https://dev.to/hschimpf/how-i-managed-to-take-my-laravel-application-to-the-serverless-world-and-how-i-almost-died-trying-3mhd"&gt;second chapter&lt;/a&gt;, CPU power in Lambda scales proportionally with allocated memory. A heavy job that takes 10 seconds with 512 MB might finish in 3 seconds with 1,769 MB; and since AWS bills per GB-second, giving it more memory often &lt;strong&gt;ends up being cheaper&lt;/strong&gt;. If your worker does CPU-intensive work, you can assign its own &lt;code&gt;memorySize&lt;/code&gt; and find the sweet spot with &lt;code&gt;aws-lambda-power-tuning&lt;/code&gt; &lt;sup id="fnref21"&gt;21&lt;/sup&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  e) Tailored layers: extensions only where needed
&lt;/h3&gt;

&lt;p&gt;Did you notice the &lt;code&gt;${bref-extra:gd-php-85}&lt;/code&gt; layer in the configuration? My worker needs the GD extension to process images, but the &lt;code&gt;web&lt;/code&gt; HTTP function doesn't. Because they are separate Lambda functions, each one loads &lt;strong&gt;only&lt;/strong&gt; the extensions it actually uses: web requests stay lightweight, and the extra weight is carried exclusively by the background worker.&lt;/p&gt;

&lt;h3&gt;
  
  
  f) Does the worker need a warmer?
&lt;/h3&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/hschimpf/how-i-managed-to-take-my-laravel-application-to-the-serverless-world-and-how-i-almost-died-trying-3mhd"&gt;second chapter&lt;/a&gt; we set up a &lt;em&gt;warmer&lt;/em&gt; every 5 minutes to avoid &lt;em&gt;cold-starts&lt;/em&gt; on web requests. Does the worker need one? &lt;strong&gt;Not at all.&lt;/strong&gt; A 200 ms &lt;em&gt;cold-start&lt;/em&gt; on an HTTP route is felt by a user staring at a loading screen; in a background queue, an extra 200 ms won't keep anyone up at night.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. What Tutorials Don't Tell You (and You Find Out in Production)
&lt;/h2&gt;

&lt;p&gt;Things that aren't in the docs &lt;em&gt;(or were buried in the fine print and I learned the hard way)&lt;/em&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  a) &lt;code&gt;SerializesModels&lt;/code&gt; and DynamoDB composite keys
&lt;/h3&gt;

&lt;p&gt;Did you notice that in &lt;code&gt;SendWelcomeEmail&lt;/code&gt; &lt;em&gt;(section 5.a)&lt;/em&gt; I passed &lt;code&gt;$user_uuid&lt;/code&gt; as a &lt;code&gt;string&lt;/code&gt; instead of the &lt;code&gt;User&lt;/code&gt; model? That wasn't an accident. &lt;code&gt;SerializesModels&lt;/code&gt; stores only &lt;code&gt;getKey()&lt;/code&gt; &lt;em&gt;(&lt;code&gt;PK&lt;/code&gt;)&lt;/em&gt; and tries to restore the model with a traditional SQL query that doesn't include the &lt;code&gt;SK&lt;/code&gt; or support DynamoDB.&lt;/p&gt;

&lt;p&gt;For your own jobs, the simplest approach is passing the &lt;code&gt;uuid&lt;/code&gt; to the constructor and fetching the model inside &lt;code&gt;handle()&lt;/code&gt;. However, if you use queued notifications &lt;em&gt;(&lt;code&gt;$user-&amp;gt;notify()&lt;/code&gt;)&lt;/em&gt;, Laravel serializes the &lt;code&gt;User&lt;/code&gt; model whether you like it or not: in that case, you have no choice but to override &lt;code&gt;getQueueableId()&lt;/code&gt; and &lt;code&gt;newQueryForRestoration()&lt;/code&gt; on your model to encode and reconstruct &lt;code&gt;PK + SK&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) &lt;code&gt;Bus::batch()&lt;/code&gt; also needs somewhere to store its progress
&lt;/h3&gt;

&lt;p&gt;By default it looks for the relational &lt;code&gt;job_batches&lt;/code&gt; table, but Laravel includes native DynamoDB support: you just need to create the &lt;code&gt;job_batches&lt;/code&gt; table &lt;em&gt;(partition key &lt;code&gt;application&lt;/code&gt;, sort key &lt;code&gt;id&lt;/code&gt;)&lt;/em&gt;, grant IAM permissions, and set &lt;code&gt;queue.batching.driver&lt;/code&gt; to &lt;code&gt;dynamodb&lt;/code&gt; &lt;sup id="fnref22"&gt;22&lt;/sup&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Your local environment doesn't need to touch AWS
&lt;/h3&gt;

&lt;p&gt;Since our job code doesn't change based on the driver, on your machine you can keep using &lt;code&gt;QUEUE_CONNECTION=database&lt;/code&gt; &lt;em&gt;(or &lt;code&gt;sync&lt;/code&gt;)&lt;/em&gt; with good old &lt;code&gt;queue:work&lt;/code&gt;, or emulate SQS locally with tools like &lt;strong&gt;MiniStack&lt;/strong&gt; &lt;sup id="fnref23"&gt;23&lt;/sup&gt; or &lt;strong&gt;LocalStack&lt;/strong&gt; &lt;sup id="fnref24"&gt;24&lt;/sup&gt;. &lt;code&gt;QUEUE_CONNECTION=sqs&lt;/code&gt; pointing to AWS is only needed in &lt;code&gt;serverless.yml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  d) The limit is 1 MiB per message
&lt;/h3&gt;

&lt;p&gt;That's SQS's hard limit &lt;sup id="fnref25"&gt;25&lt;/sup&gt;. Jobs should carry lightweight identifiers, not base64-encoded files or collections of 10,000 items. If you're anywhere near that limit, your design is screaming at you to have the job fetch the data when it runs instead of receiving it in the payload.&lt;/p&gt;

&lt;h3&gt;
  
  
  e) The AWS SDK cleanup from the second chapter
&lt;/h3&gt;

&lt;p&gt;If you used &lt;code&gt;aws/aws-sdk-php&lt;/code&gt; with &lt;code&gt;removeUnusedServices&lt;/code&gt; in &lt;code&gt;composer.json&lt;/code&gt; to shrink your deploy size, make sure to add &lt;code&gt;Sqs&lt;/code&gt; to the list of allowed services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"extra"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"aws/aws-sdk-php"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"DynamoDb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"S3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Sqs"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why is it so easy to forget? Because tests run with &lt;code&gt;QUEUE_CONNECTION=sync&lt;/code&gt; or &lt;code&gt;Queue::fake()&lt;/code&gt;, so CI passes everything in green without ever instantiating &lt;code&gt;SqsClient&lt;/code&gt;... until the first real &lt;code&gt;dispatch()&lt;/code&gt; in production blows up because the class was stripped out.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Costs: The Worker That Only Charges When It Works
&lt;/h2&gt;

&lt;p&gt;Let's update the tab, just like in every chapter.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before (EC2)&lt;/strong&gt;: the worker lived on one of the &lt;code&gt;t3.small&lt;/code&gt; instances from the &lt;a href="https://dev.to/hschimpf/why-i-migrated-my-laravel-app-to-aws-serverless-and-why-it-could-save-you-time-and-money-dem"&gt;first chapter&lt;/a&gt; &lt;em&gt;(~$15/month per instance)&lt;/em&gt;. A resident &lt;code&gt;queue:work&lt;/code&gt; process with reserved RAM and &lt;code&gt;supervisord&lt;/code&gt; running 24/7, even when the queue spent 90% of the day empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Now (SQS + Lambda)&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQS&lt;/strong&gt;: the first million monthly requests are free; after that it costs &lt;strong&gt;\$0.40 per million&lt;/strong&gt; &lt;sup id="fnref26"&gt;26&lt;/sup&gt;. Adding up the ~630,000 requests from idle polling and 3 requests per job &lt;em&gt;(&lt;code&gt;SendMessage&lt;/code&gt;, &lt;code&gt;ReceiveMessage&lt;/code&gt;, and &lt;code&gt;DeleteMessage&lt;/code&gt;)&lt;/em&gt;, you can process up to &lt;strong&gt;~120,000 jobs per month for $0.00&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt;: the perpetual free tier includes &lt;strong&gt;1 million invocations&lt;/strong&gt; and &lt;strong&gt;400,000 GB-seconds&lt;/strong&gt; per month. With no jobs in the queue, compute costs literally zero.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's put numbers on a month with &lt;strong&gt;100,000 jobs&lt;/strong&gt; &lt;em&gt;(averaging ~500 ms with 512 MB of RAM)&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Item&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Within Free Tier&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Full Price &lt;em&gt;(no Free Tier)&lt;/em&gt;&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQS Polling (~630k req/mo)&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;$0.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQS Messages (300k req/mo)&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;$0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda Worker (100k jobs)&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;$0.44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.81&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Even without the free tier, processing 100,000 jobs costs less than a dollar a month!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And most importantly: with DynamoDB on &lt;code&gt;PAY_PER_REQUEST&lt;/code&gt;, Lambda billing per millisecond, and SQS billing per request, &lt;strong&gt;almost no component in the queue system charges a flat fee just for existing&lt;/strong&gt; &lt;em&gt;(except for SQS polling, which in my case falls within the free tier)&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. What's Next?
&lt;/h2&gt;

&lt;p&gt;Today we retired &lt;code&gt;supervisord&lt;/code&gt; and the 24/7 &lt;code&gt;queue:work&lt;/code&gt; process for good: we replaced them with an SQS queue and a Lambda worker that only wakes up when there's work to do. We saw how Bref reuses Laravel's own &lt;code&gt;Worker&lt;/code&gt; &lt;em&gt;(so our jobs don't change)&lt;/em&gt;, how its &lt;code&gt;release()&lt;/code&gt; trick preserves retries and backoff, and how &lt;code&gt;failed_jobs&lt;/code&gt; in DynamoDB coexists with SQS's &lt;em&gt;Dead Letter Queue&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But there's still a loose end between the &lt;a href="https://dev.to/hschimpf/where-does-my-laravel-apps-data-live-in-an-ephemeral-environment-where-not-even-the-disk-can-save-4k18"&gt;previous chapter&lt;/a&gt; and this one. In section 6 I mentioned that my worker loads the GD extension to resize images; now, if in the previous chapter we saw that files travel straight from the browser to S3 without touching our application... &lt;strong&gt;who tells Laravel that a file finished uploading so it can queue that job?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the next chapter we'll close that loop with &lt;strong&gt;S3 Event Notifications&lt;/strong&gt;, and we'll see why the solution is &lt;em&gt;not&lt;/em&gt; wiring S3 directly to the queue we just built.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A question for you&lt;/strong&gt;: what are you running your Laravel queues on in production today, or what was the worst stunt a queue worker ever pulled on you at 3 AM? Let me know in the comments: the best production war stories come from there, not from post-mortems.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/queues" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/queues&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/queue.md" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/queue.md&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/permissions.md" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/permissions.md&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-DelaySeconds" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-DelaySeconds&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/queues#multiple-queues" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/queues#multiple-queues&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-scaling.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-scaling.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/sqs/pricing/#How_are_Amazon_SQS_requests_priced.3F" rel="noopener noreferrer"&gt;https://aws.amazon.com/sqs/pricing/#How_are_Amazon_SQS_requests_priced.3F&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;&lt;a href="https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/QueueHandler.php#L53-L78" rel="noopener noreferrer"&gt;https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/QueueHandler.php#L53-L78&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;&lt;a href="https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/Worker.php#L10-L39" rel="noopener noreferrer"&gt;https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/Worker.php#L10-L39&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn12"&gt;
&lt;p&gt;&lt;a href="https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/SqsJob.php#L13-L38" rel="noopener noreferrer"&gt;https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/SqsJob.php#L13-L38&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn13"&gt;
&lt;p&gt;&lt;a href="https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/QueueHandler.php#L107-L117" rel="noopener noreferrer"&gt;https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/QueueHandler.php#L107-L117&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn14"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/queues#storing-failed-jobs-in-dynamodb" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/queues#storing-failed-jobs-in-dynamodb&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn15"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/queue.md#retry-delay" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/queue.md#retry-delay&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn16"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/queue.md#commands" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/queue.md#commands&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn17"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/queue.md#batch-size" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/queue.md#batch-size&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn18"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn19"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/queue.md#max-concurrency" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/queue.md#max-concurrency&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn20"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-configure.html#events-sqs-queueconfig" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-configure.html#events-sqs-queueconfig&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn21"&gt;
&lt;p&gt;&lt;a href="https://github.com/alexcasalboni/aws-lambda-power-tuning" rel="noopener noreferrer"&gt;https://github.com/alexcasalboni/aws-lambda-power-tuning&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn22"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/queues#storing-batches-in-dynamodb" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/queues#storing-batches-in-dynamodb&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn23"&gt;
&lt;p&gt;&lt;a href="https://github.com/ministackorg/ministack" rel="noopener noreferrer"&gt;https://github.com/ministackorg/ministack&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn24"&gt;
&lt;p&gt;&lt;a href="https://github.com/localstack/localstack" rel="noopener noreferrer"&gt;https://github.com/localstack/localstack&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn25"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn26"&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/sqs/pricing/" rel="noopener noreferrer"&gt;https://aws.amazon.com/sqs/pricing/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>serverless</category>
      <category>laravel</category>
      <category>sqs</category>
      <category>lambda</category>
    </item>
    <item>
      <title>Qué Reemplazó a queue:work en Mi Aplicación Laravel (Y Por Qué Dejé de Rezarle a Supervisor a las 3 AM)</title>
      <dc:creator>Hermann D. Schimpf</dc:creator>
      <pubDate>Fri, 16 Oct 2026 15:32:47 +0000</pubDate>
      <link>https://dev.to/hschimpf/que-reemplazo-a-queuework-en-mi-aplicacion-laravel-y-por-que-deje-de-rezarle-a-supervisor-a-las-3-42ph</link>
      <guid>https://dev.to/hschimpf/que-reemplazo-a-queuework-en-mi-aplicacion-laravel-y-por-que-deje-de-rezarle-a-supervisor-a-las-3-42ph</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;English version &lt;a href="https://dev.to/hschimpf/what-replaced-queuework-in-my-laravel-app-and-why-i-stopped-praying-to-supervisor-at-3-am-41nh"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71jkhpl0rgbr22l5h85k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71jkhpl0rgbr22l5h85k.png" alt="Banner Image" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;En el &lt;a href="https://dev.to/hschimpf/donde-persisten-los-datos-de-mi-aplicacion-laravel-en-un-entorno-efimero-donde-ni-el-disco-te-2c5d"&gt;capítulo anterior&lt;/a&gt; resolvimos dónde viven los datos: archivos en &lt;strong&gt;S3&lt;/strong&gt;, caché y sesiones en &lt;strong&gt;DynamoDB&lt;/strong&gt;, y una base de datos NoSQL que habla Eloquent. Pero quedó una pregunta flotando en el aire: si una función Lambda nace, responde y muere en milisegundos, ¿quién se queda despierto procesando los jobs en segundo plano?&lt;/p&gt;

&lt;p&gt;En un servidor tradicional la respuesta es &lt;code&gt;supervisord&lt;/code&gt; vigilando un proceso eterno. En un entorno efímero donde nada vive más de 15 minutos, esa receta simplemente no aplica.&lt;/p&gt;

&lt;p&gt;En este capítulo vamos a reemplazar ese daemon por &lt;strong&gt;SQS + un worker en Lambda&lt;/strong&gt;. Lo haremos en tres actos: primero la configuración &lt;em&gt;(sorprendentemente corta)&lt;/em&gt;, después el ciclo de vida de un job &lt;em&gt;(donde está la magia)&lt;/em&gt;, y finalmente el tema que casi nadie explica completo: qué pasa cuando un job falla, quién lo reintenta, y dónde termina.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. El Mapa del Tesoro Serverless
&lt;/h2&gt;

&lt;p&gt;Rápido, para ubicarnos:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbrh82u77jvw4h7p18azb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbrh82u77jvw4h7p18azb.png" alt="AWS Serverless architecture" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ya tenemos &lt;strong&gt;Lambda&lt;/strong&gt; ejecutando nuestra App Laravel, &lt;strong&gt;CloudFront&lt;/strong&gt; repartiendo tráfico entre &lt;strong&gt;S3&lt;/strong&gt; &lt;em&gt;(assets)&lt;/em&gt; y &lt;strong&gt;Lambda&lt;/strong&gt; &lt;em&gt;(la app)&lt;/em&gt;, y &lt;strong&gt;DynamoDB&lt;/strong&gt; como caché, sesiones y nuestra base de datos principal. Hoy toca:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gestionar nuestra cola de eventos con &lt;strong&gt;SQS&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;y una segunda función Lambda para el &lt;em&gt;worker&lt;/em&gt; de Laravel Queue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;¿Recuerdan la tabla de componentes del &lt;a href="https://dev.to/hschimpf/como-logre-llevar-mi-aplicacion-laravel-al-mundo-serverless-y-como-casi-muero-en-el-intento-4b1h"&gt;segundo capítulo&lt;/a&gt;? Hoy tachamos esta fila:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Componente&lt;/th&gt;
&lt;th&gt;Servidor Dedicado&lt;/th&gt;
&lt;th&gt;AWS Serverless&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cola de Eventos&lt;/td&gt;
&lt;td&gt;&lt;del&gt;SSH $ &lt;code&gt;php artisan queue:work&lt;/code&gt; / quizás supervisor&lt;/del&gt;&lt;/td&gt;
&lt;td&gt;SQS + Lambda 🚀&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  2. Por Qué &lt;code&gt;queue:work&lt;/code&gt; No Encaja en Serverless
&lt;/h2&gt;

&lt;h3&gt;
  
  
  a) Cómo funciona la cola en un servidor tradicional
&lt;/h3&gt;

&lt;p&gt;Simplificando mucho, &lt;code&gt;php artisan queue:work&lt;/code&gt; es un &lt;code&gt;while (true)&lt;/code&gt;: le pregunta a la cola si hay trabajo; si hay, lo procesa; si no, duerme unos segundos y vuelve a preguntar. Un proceso residente que ocupa memoria y CPU las 24 horas, aunque la cola esté vacía desde hace 3 días.&lt;/p&gt;

&lt;p&gt;Y como es un proceso, necesita niñera:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;supervisor&lt;/strong&gt; &lt;em&gt;(o systemd)&lt;/em&gt; para resucitarlo cuando muere.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;queue:restart&lt;/code&gt; después de cada deploy, porque el proceso tiene el código viejo cargado en memoria.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--max-jobs&lt;/code&gt; y &lt;code&gt;--max-time&lt;/code&gt; para reciclarlo antes de que los memory leaks lo conviertan en un agujero negro.&lt;/li&gt;
&lt;li&gt;¿Y para escalar? Más procesos → más RAM → instancias más grandes → todo acompañado de una factura mayor.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;queue:work&lt;/code&gt; es un empleado al que le pagan por presionar F5 en la bandeja de entrada cada tres segundos. Ocho horas. Aunque no llegue nada.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) Por qué eso no existe en Lambda
&lt;/h3&gt;

&lt;p&gt;Una invocación de Lambda vive, como máximo, 15 minutos &lt;sup id="fnref1"&gt;1&lt;/sup&gt;. No hay proceso residente, no hay &lt;code&gt;while (true)&lt;/code&gt;, no hay nadie preguntando &lt;em&gt;"¿hay trabajo?"&lt;/em&gt;. Si intentan correr &lt;code&gt;queue:work&lt;/code&gt; dentro de una Lambda... funciona 15 minutos, muere, y nadie lo resucita. Y si lo resucitan con un cron, están pagando una Lambda las 24 horas para hacer polling. Felicidades, reinventaron EC2, pero más caro.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) La inversión del modelo: de &lt;em&gt;pull&lt;/em&gt; a &lt;em&gt;push&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;La solución no es &lt;em&gt;adaptar&lt;/em&gt; el worker a Lambda, es darlo vuelta. En vez de que el worker &lt;strong&gt;busque&lt;/strong&gt; trabajo &lt;em&gt;(pull)&lt;/em&gt;, la cola &lt;strong&gt;invoca&lt;/strong&gt; al worker cuando hay trabajo &lt;em&gt;(push)&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Laravel hace &lt;code&gt;dispatch()&lt;/code&gt; y registra el job en SQS.&lt;/li&gt;
&lt;li&gt;SQS detecta que hay mensajes e invoca a una función Lambda con esos mensajes.&lt;/li&gt;
&lt;li&gt;Lambda procesa los jobs, termina y desaparece.&lt;/li&gt;
&lt;li&gt;Una cola vacía = cero invocaciones Lambda.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;El &lt;strong&gt;worker&lt;/strong&gt; deja de ser un proceso que &lt;strong&gt;espera&lt;/strong&gt; y pasa a ser un evento que &lt;strong&gt;ocurre&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. El Nuevo Stack de Colas: SQS como Buzón, Lambda como Worker
&lt;/h2&gt;

&lt;h3&gt;
  
  
  a) SQS en 30 segundos para devs Laravel
&lt;/h3&gt;

&lt;p&gt;Amazon SQS &lt;em&gt;(Simple Queue Service)&lt;/em&gt; es una cola de mensajes administrada &lt;sup id="fnref2"&gt;2&lt;/sup&gt;. Lo mínimo que necesitan saber:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mensaje&lt;/strong&gt;: para nosotros, el payload JSON serializado del job. El mismo que verían en la columna &lt;code&gt;payload&lt;/code&gt; de la tabla &lt;code&gt;jobs&lt;/code&gt; si usaran el driver &lt;code&gt;database&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility timeout&lt;/strong&gt;: cuando alguien toma un mensaje, SQS lo &lt;em&gt;oculta&lt;/em&gt; durante X segundos. Si en ese tiempo nadie lo borra, reaparece para que otro lo intente. Es el equivalente al &lt;code&gt;retry_after&lt;/code&gt; de Laravel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead Letter Queue &lt;em&gt;(DLQ)&lt;/em&gt;&lt;/strong&gt;: una segunda cola donde SQS mueve los mensajes que fallaron demasiadas veces. La &lt;em&gt;"caja de objetos perdidos"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard vs FIFO&lt;/strong&gt;: las colas estándar no garantizan orden &lt;em&gt;(y son las que vamos a usar)&lt;/em&gt;; las FIFO sí, a cambio de menos throughput.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Con el vocabulario listo, pasemos al código.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) Conectando los cables con Serverless Lift
&lt;/h3&gt;

&lt;p&gt;Partimos del &lt;code&gt;serverless.yml&lt;/code&gt; que construimos en el &lt;a href="https://dev.to/hschimpf/como-logre-llevar-mi-aplicacion-laravel-al-mundo-serverless-y-como-casi-muero-en-el-intento-4b1h"&gt;segundo capítulo&lt;/a&gt; y agregamos dos variables de entorno y un construct de Lift &lt;sup id="fnref3"&gt;3&lt;/sup&gt; &lt;sup id="fnref4"&gt;4&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;service: laravel
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;provider:
&lt;/span&gt;  name: aws
  environment:
    # ...
&lt;span class="gi"&gt;+   QUEUE_CONNECTION: sqs
+   SQS_QUEUE: ${construct:jobs.queueUrl}
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;functions:
&lt;/span&gt;  web:
    handler: Bref\LaravelBridge\Http\OctaneHandler
    runtime: php-85
    # ...
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;constructs:
&lt;/span&gt;  website:
    type: server-side-website
    # ...
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ jobs:
+   type: queue
+   worker:
+     handler: Bref\LaravelBridge\Queue\QueueHandler
+     runtime: php-85
+     timeout: 60  # segundos, el techo del job más largo
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;plugins:
&lt;/span&gt;  - ./vendor/bref/bref
  - serverless-lift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;QUEUE_CONNECTION: sqs&lt;/code&gt; activa el driver que Laravel ya trae de fábrica, y &lt;code&gt;SQS_QUEUE&lt;/code&gt; recibe la URL completa de la cola que Lift acaba de crear &lt;em&gt;(si prefieren pasar solo el nombre, van a necesitar también &lt;code&gt;SQS_PREFIX&lt;/code&gt; &lt;sup id="fnref3"&gt;3&lt;/sup&gt;)&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Aunque en un servidor tradicional la cola se corre por CLI con &lt;code&gt;artisan&lt;/code&gt;, acá no usamos &lt;code&gt;php-85-console&lt;/code&gt;. Al igual que con &lt;code&gt;OctaneHandler&lt;/code&gt;, usamos el runtime de eventos &lt;code&gt;php-85&lt;/code&gt; con la clase &lt;code&gt;QueueHandler&lt;/code&gt; de Bref.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;¿Y qué hizo Lift por detrás con ese construct &lt;code&gt;jobs&lt;/code&gt;? &lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creó la &lt;strong&gt;cola SQS&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Creó una &lt;strong&gt;Dead Letter Queue&lt;/strong&gt; con retención de 14 días &lt;em&gt;(el máximo que permite SQS)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Creó la &lt;strong&gt;función Lambda&lt;/strong&gt; &lt;code&gt;worker&lt;/code&gt; con el handler de Bref.&lt;/li&gt;
&lt;li&gt;Configuró el &lt;strong&gt;event source mapping&lt;/strong&gt;: la pieza de AWS que conecta la cola SQS con la función Lambda, y la invoca cuando hay mensajes. Nuestro reemplazo de &lt;code&gt;queue:work&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Y le dio permiso &lt;code&gt;sqs:SendMessage&lt;/code&gt; a &lt;strong&gt;todas&lt;/strong&gt; las funciones del stack &lt;sup id="fnref5"&gt;5&lt;/sup&gt;. Por eso, a diferencia de las tablas DynamoDB del &lt;a href="https://dev.to/hschimpf/donde-persisten-los-datos-de-mi-aplicacion-laravel-en-un-entorno-efimero-donde-ni-el-disco-te-2c5d"&gt;capítulo anterior&lt;/a&gt;, acá no escribimos ni una línea de IAM.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Las credenciales, al igual que con DynamoDB, las tiene asignadas el rol de ejecución de cada función. Rotan solas y nadie las ve.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) Nueva infraestructura, el mismo código de siempre
&lt;/h3&gt;

&lt;p&gt;Esta es la parte que más me gusta. Nada de lo que ya escribieron se entera del cambio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Un job&lt;/span&gt;
&lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Un mail encolado&lt;/span&gt;
&lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WelcomeMail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Una notificación que implementa ShouldQueue&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VerifyEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Todo eso va a SQS, y una Lambda lo procesa. Los listeners &lt;code&gt;ShouldQueue&lt;/code&gt;, los jobs encadenados... casi todo sigue funcionando igual. Otro punto a favor para la abstracción de Laravel 💪.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Importante&lt;/strong&gt;: El driver SQS de Laravel limita el &lt;code&gt;delay()&lt;/code&gt; / &lt;code&gt;#[Backoff]&lt;/code&gt; hasta 900 segundos (15 minutos), que es el límite de SQS &lt;sup id="fnref6"&gt;6&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;Si necesitan &lt;strong&gt;varias colas&lt;/strong&gt; &lt;em&gt;(una para emails urgentes, otra con menos concurrencia para no reventar una API externa, una FIFO para procesar en orden)&lt;/em&gt;, cada una es un construct &lt;code&gt;queue&lt;/code&gt; adicional, y desde Laravel 13.26 se enrutan con &lt;code&gt;Queue::forward()&lt;/code&gt; &lt;sup id="fnref7"&gt;7&lt;/sup&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. ¿Qué Pasa Realmente Cuando Despachan un Job?
&lt;/h2&gt;

&lt;p&gt;Hasta acá todo parece magia: configuramos un bloque en &lt;code&gt;serverless.yml&lt;/code&gt;, hicimos &lt;code&gt;dispatch()&lt;/code&gt; y el job se ejecutó. Pero en producción la magia dura hasta el primer incidente. Veamos paso a paso &lt;em&gt;quién&lt;/em&gt; hace &lt;em&gt;qué&lt;/em&gt;, desde que el mensaje entra a SQS hasta que desaparece.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Laravel despacha, SQS recibe
&lt;/h3&gt;

&lt;p&gt;Cuando llaman a &lt;code&gt;SendWelcomeEmail::dispatch($user-&amp;gt;uuid)&lt;/code&gt;, Laravel serializa el job y el driver &lt;code&gt;sqs&lt;/code&gt; ejecuta una llamada &lt;code&gt;SendMessage&lt;/code&gt; a la API de SQS para dejarlo en la cola. Y listo; nuestra Lambda &lt;code&gt;web&lt;/code&gt; ya cumplió su parte y puede terminar su ejecución.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;El cuerpo del mensaje es el JSON de siempre (&lt;code&gt;displayName&lt;/code&gt;, &lt;code&gt;job&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt;, &lt;code&gt;attempts&lt;/code&gt;). Para Laravel, SQS es solo otro buzón donde depositar ese mismo sobre.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) SQS avisa, Lambda despierta
&lt;/h3&gt;

&lt;p&gt;El &lt;em&gt;event source mapping&lt;/em&gt; que creó Lift hace el &lt;em&gt;long polling&lt;/em&gt; por nosotros &lt;sup id="fnref8"&gt;8&lt;/sup&gt;: AWS mantiene un proceso interno consultando la cola y, apenas detecta mensajes, invoca a nuestra función Lambda &lt;code&gt;worker&lt;/code&gt; con un lote de hasta &lt;code&gt;batchSize&lt;/code&gt; mensajes &lt;em&gt;(Lift lo deja en 1 por defecto; ya veremos por qué y cuándo cambiarlo)&lt;/em&gt;. El polling sigue existiendo; la diferencia es que, en vez del proceso &lt;code&gt;queue:work&lt;/code&gt;, ahora AWS se encarga de hacerlo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Importante&lt;/strong&gt;: Ese polling de AWS cuenta como llamadas &lt;code&gt;ReceiveMessage&lt;/code&gt; en SQS, incluso con la cola vacía. Por suerte, la capa gratuita incluye &lt;strong&gt;1 millón de requests mensuales&lt;/strong&gt; &lt;sup id="fnref9"&gt;9&lt;/sup&gt;: con la cola en reposo, ese chequeo constante genera ~21.000 requests por día &lt;em&gt;(~630.000 al mes)&lt;/em&gt;, por lo que no pagan nada.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) Bref recibe el lote, Laravel ejecuta el job
&lt;/h3&gt;

&lt;p&gt;Cuando Lambda despierta, le entrega el evento de SQS al &lt;code&gt;QueueHandler&lt;/code&gt; de Bref &lt;sup id="fnref10"&gt;10&lt;/sup&gt;. Si espiamos su método &lt;code&gt;handleSqs()&lt;/code&gt; &lt;em&gt;(simplificado)&lt;/em&gt; se ve exactamente qué pasa con cada mensaje:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handleSqs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;SqsEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;makeWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'resetScope'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resetLaravel&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRecords&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$sqsRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;calculateJobTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRemainingTimeInMillis&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="nv"&gt;$worker&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;runSqsJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;marshalJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sqsRecord&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;gatherWorkerOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$timeout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasFailed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isDeleted&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tres detalles clave en esas pocas líneas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ese &lt;code&gt;Worker&lt;/code&gt; es el de Laravel.&lt;/strong&gt; Literalmente extiende de &lt;code&gt;Illuminate\Queue\Worker&lt;/code&gt; &lt;sup id="fnref11"&gt;11&lt;/sup&gt;. Bref no reinventó cómo correr un job: le pasa cada mensaje al mismo motor que usa &lt;code&gt;queue:work&lt;/code&gt;. Consecuencia directa: &lt;code&gt;#[Tries]&lt;/code&gt;, &lt;code&gt;#[Backoff]&lt;/code&gt;, &lt;code&gt;$timeout&lt;/code&gt;, &lt;code&gt;failed()&lt;/code&gt;, middlewares, &lt;code&gt;ShouldBeUnique&lt;/code&gt;, &lt;code&gt;retryUntil()&lt;/code&gt; y los eventos &lt;code&gt;JobProcessing&lt;/code&gt;/&lt;code&gt;JobProcessed&lt;/code&gt;/&lt;code&gt;JobFailed&lt;/code&gt;... &lt;strong&gt;todo funciona igual que en un servidor tradicional&lt;/strong&gt;. No hay una tabla de &lt;em&gt;"features soportadas"&lt;/em&gt; que consultar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entre job y job se limpia el estado.&lt;/strong&gt; Ese callback &lt;code&gt;resetLaravel()&lt;/code&gt; vacía el contexto de logs, olvida las instancias &lt;em&gt;scoped&lt;/em&gt; del contenedor y limpia las facades resueltas antes de cada ejecución. ¿Por qué? Por la misma razón que vimos con Octane en el &lt;a href="https://dev.to/hschimpf/como-logre-llevar-mi-aplicacion-laravel-al-mundo-serverless-y-como-casi-muero-en-el-intento-4b1h"&gt;segundo capítulo&lt;/a&gt;: la instancia de Lambda &lt;em&gt;(y su proceso PHP)&lt;/em&gt; &lt;strong&gt;sobrevive entre un job y el siguiente&lt;/strong&gt;. Sin esa limpieza, el segundo job heredaría la basura en memoria del primero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El borrado en SQS es explícito.&lt;/strong&gt; Solo si el job terminó sin fallar, Bref llama a &lt;code&gt;$job-&amp;gt;delete()&lt;/code&gt; para quitarlo de la cola. Si falló o ya fue eliminado &lt;em&gt;(o liberado de vuelta a la cola, como veremos enseguida)&lt;/em&gt;, no lo toca.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  d) El &lt;code&gt;timeout&lt;/code&gt; de Lambda vs. el &lt;code&gt;$timeout&lt;/code&gt; del job
&lt;/h3&gt;

&lt;p&gt;¿Vieron ese &lt;code&gt;calculateJobTimeout($context-&amp;gt;getRemainingTimeInMillis())&lt;/code&gt; en el loop? Bref no usa un timeout fijo: consulta cuántos milisegundos le quedan de vida a la invocación de Lambda &lt;em&gt;(menos 1 segundo de margen)&lt;/em&gt; y le asigna ese tiempo al job. Con ese valor, el &lt;code&gt;Worker&lt;/code&gt; programa un &lt;code&gt;pcntl_alarm()&lt;/code&gt; &lt;sup id="fnref11"&gt;11&lt;/sup&gt;: si el job se pasa, suena la alarma, el proceso termina con &lt;code&gt;exit(1)&lt;/code&gt; y la invocación falla. El job solo se marca como fallido &lt;em&gt;(y va a &lt;code&gt;failed_jobs&lt;/code&gt;)&lt;/em&gt; si definieron &lt;code&gt;#[FailOnTimeout]&lt;/code&gt; o &lt;code&gt;$failOnTimeout = true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Dos implicaciones prácticas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El &lt;code&gt;timeout&lt;/code&gt; del worker en &lt;code&gt;serverless.yml&lt;/code&gt; es, en la práctica, &lt;strong&gt;el techo del job más largo&lt;/strong&gt; que pueden procesar. Configúrenlo pensando en su tarea más pesada.&lt;/li&gt;
&lt;li&gt;Con un &lt;code&gt;batchSize&lt;/code&gt; mayor a 1, todos los jobs del lote &lt;strong&gt;comparten ese mismo reloj&lt;/strong&gt;: si procesan tareas largas, conviene mantener un lote pequeño &lt;em&gt;(o dejarlo en 1)&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ojo con &lt;code&gt;#[Timeout]&lt;/code&gt; / &lt;code&gt;$timeout&lt;/code&gt;&lt;/strong&gt;: Si definen un timeout propio en la clase del job, Laravel pisa el valor calculado por Bref. Si ese número supera el tiempo que le queda a Lambda, AWS matará la invocación en seco antes de que suene la alarma de PHP. &lt;strong&gt;Recomendación&lt;/strong&gt;: usen un valor bastante menor al &lt;code&gt;timeout&lt;/code&gt; del worker, sobre todo con &lt;code&gt;batchSize&lt;/code&gt; mayor a 1, donde los últimos jobs del lote arrancan con menos tiempo disponible.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. ¿Quién Se Hace Cargo Cuando Algo Explota?
&lt;/h2&gt;

&lt;p&gt;A todos nos gusta ver cuando el camino feliz se cumple al pie de la letra: el job entra, se ejecuta y se borra de SQS. Pero en el mundo real las APIs externas se caen, las validaciones fallan y los procesos se quedan sin memoria.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Seguramente se estarán preguntando: &lt;em&gt;"Si Lift ya nos creó una &lt;strong&gt;Dead Letter Queue&lt;/strong&gt; en SQS y Laravel ya tiene su propia tabla &lt;code&gt;failed_jobs&lt;/code&gt;, ¿acaso no están haciendo &lt;strong&gt;lo mismo&lt;/strong&gt;?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;La respuesta es &lt;strong&gt;no&lt;/strong&gt;. Cuando combinamos el &lt;code&gt;Worker&lt;/code&gt; de Laravel con SQS y Lambda, aparecen &lt;strong&gt;dos mecanismos de reintento y fallo&lt;/strong&gt; conviviendo en el mismo stack: uno del lado de nuestra aplicación y otro del lado de la infraestructura de AWS. Y aunque suenen parecidos, atacan problemas completamente distintos.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Los reintentos los maneja Laravel, no SQS
&lt;/h3&gt;

&lt;p&gt;Empecemos por el lado de nuestra aplicación. Imaginen un job común y corriente configurado con reintentos y backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Queue\Attributes\Backoff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Queue\Attributes\Tries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Tries(3)]&lt;/span&gt;
&lt;span class="na"&gt;#[Backoff(30)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$user_uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ... si esto lanza una excepción, Laravel reintenta en 30 segundos, hasta 3 veces&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ... agotados los 3 intentos, Laravel llama acá&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuando &lt;code&gt;handle()&lt;/code&gt; lanza una excepción, el &lt;code&gt;Worker&lt;/code&gt; de Laravel hace lo de siempre: si quedan intentos, llama a &lt;code&gt;$job-&amp;gt;release(30)&lt;/code&gt;. Y acá está el truco. El &lt;code&gt;SqsJob&lt;/code&gt; nativo de Laravel simplemente modifica el &lt;em&gt;visibility timeout&lt;/em&gt; del mensaje en SQS, pero Bref lo sobreescribe y hace otra cosa &lt;sup id="fnref12"&gt;12&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;released&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'attempts'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'attempts'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// borra el mensaje original...&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;deleteMessage&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'QueueUrl'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ReceiptHandle'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ReceiptHandle'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="c1"&gt;// ...y envía una copia nueva, con el contador de intentos y el delay del backoff&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'QueueUrl'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'MessageBody'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'DelaySeconds'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;secondsUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$delay&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;No es un &lt;em&gt;"reenviar"&lt;/em&gt;. Es &lt;strong&gt;borrar&lt;/strong&gt; el correo y redactar uno &lt;strong&gt;nuevo&lt;/strong&gt;, idéntico, con &lt;code&gt;[Intento 2]&lt;/code&gt; en el asunto y programado para dentro de 30 segundos.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;¿Por qué? Porque así el contador de intentos viaja &lt;strong&gt;dentro del payload&lt;/strong&gt; &lt;em&gt;(&lt;code&gt;attempts()&lt;/code&gt; lo lee de ahí, no del &lt;code&gt;ApproximateReceiveCount&lt;/code&gt; de SQS)&lt;/em&gt;, y el &lt;code&gt;backoff&lt;/code&gt; se traduce a un &lt;code&gt;DelaySeconds&lt;/code&gt; real. Resultado: &lt;code&gt;#[Tries(3)]&lt;/code&gt; y &lt;code&gt;#[Backoff(30)]&lt;/code&gt; se comportan &lt;strong&gt;exactamente&lt;/strong&gt; como en un servidor. Bref eligió deliberadamente la semántica de Laravel por sobre la de SQS &lt;sup id="fnref3"&gt;3&lt;/sup&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Si no definen &lt;code&gt;#[Tries]&lt;/code&gt; / &lt;code&gt;$tries&lt;/code&gt;, el handler de Bref usa 3 por defecto &lt;sup id="fnref13"&gt;13&lt;/sup&gt;. Y si usan backoff exponencial &lt;em&gt;(&lt;code&gt;#[Backoff([10, 60, 300])]&lt;/code&gt;)&lt;/em&gt;, también funciona: cada reenvío lleva el delay que corresponde a ese intento.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) El efecto secundario: SQS nunca se entera del fallo
&lt;/h3&gt;

&lt;p&gt;Cuando en &lt;code&gt;handle()&lt;/code&gt; se lanza una excepción, el &lt;code&gt;Worker&lt;/code&gt; de Laravel la atrapa en su propio &lt;code&gt;try/catch&lt;/code&gt;: la reporta en los logs, decide si reintenta o da el job por perdido, y sigue de largo. Como esa excepción nunca llega hasta &lt;code&gt;handleSqs()&lt;/code&gt;, &lt;strong&gt;para AWS Lambda la función terminó sin errores&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Esto significa que el mecanismo nativo de reintentos de SQS &lt;em&gt;(visibility timeout → &lt;code&gt;maxReceiveCount&lt;/code&gt; → DLQ)&lt;/em&gt; &lt;strong&gt;jamás se entera&lt;/strong&gt; de que un job falló por una excepción de PHP. Ni en el primer intento, ni en el tercero.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Laravel es ese compañero que en la daily dice "todo bien" con producción en llamas. Y SQS le cree.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) Primera capa: cuando el job agota sus intentos en Laravel
&lt;/h3&gt;

&lt;p&gt;Cuando un job falla por tercera vez y agota sus intentos, Laravel se rinde: ejecuta el método &lt;code&gt;failed()&lt;/code&gt; de la clase y registra el fallo usando el driver configurado en &lt;code&gt;config/queue.php&lt;/code&gt; &lt;em&gt;(&lt;code&gt;queue.failed&lt;/code&gt;)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;El problema es que, por defecto, Laravel intenta guardar ese registro en una tabla &lt;code&gt;failed_jobs&lt;/code&gt; de una base de datos SQL... de la cual nos despedimos en el &lt;a href="https://dev.to/hschimpf/donde-persisten-los-datos-de-mi-aplicacion-laravel-en-un-entorno-efimero-donde-ni-el-disco-te-2c5d"&gt;capítulo anterior&lt;/a&gt;. Sin base relacional, la salida rápida es desactivarlo completamente con &lt;code&gt;QUEUE_FAILED_DRIVER: null&lt;/code&gt; &lt;sup id="fnref3"&gt;3&lt;/sup&gt;: la excepción igual queda en los logs de CloudWatch, pero el job no se guarda en ningún lado. No lo recomiendo.&lt;/p&gt;

&lt;p&gt;La buena noticia es que Laravel ya incluye soporte nativo para guardar &lt;code&gt;failed_jobs&lt;/code&gt; en &lt;strong&gt;DynamoDB&lt;/strong&gt; &lt;sup id="fnref14"&gt;14&lt;/sup&gt;. Solo debemos ajustar &lt;code&gt;config/queue.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'failed'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'QUEUE_FAILED_DRIVER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'database-uuids'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// Al utilizar driver=dynamodb no es necesario especificar 'database'&lt;/span&gt;
    &lt;span class="c1"&gt;// 'database' =&amp;gt; env('DB_CONNECTION', 'sqlite'),&lt;/span&gt;
    &lt;span class="s1"&gt;'key'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_ACCESS_KEY_ID'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'secret'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_SECRET_ACCESS_KEY'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'token'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_SESSION_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'region'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_DEFAULT_REGION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'us-east-1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'table'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DYNAMODB_FAILED_JOBS_TABLE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'failed_jobs'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;QUEUE_FAILED_DRIVER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb&lt;/span&gt;
    &lt;span class="na"&gt;DYNAMODB_FAILED_JOBS_TABLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;FailedJobsTable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y declaramos la tabla dentro de &lt;code&gt;resources&lt;/code&gt; en &lt;code&gt;serverless.yml&lt;/code&gt;. Laravel usa &lt;code&gt;application&lt;/code&gt; como partition key &lt;em&gt;(el nombre de la app)&lt;/em&gt; y &lt;code&gt;uuid&lt;/code&gt; como sort key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;FailedJobsTable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::DynamoDB::Table&lt;/span&gt;
  &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failed_jobs&lt;/span&gt;
    &lt;span class="na"&gt;BillingMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;
    &lt;span class="na"&gt;AttributeDefinitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;        &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;KeySchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;HASH&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;        &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RANGE&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;TimeToLiveSpecification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;expires_at&lt;/span&gt;
      &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Así como lo hicimos en el &lt;a href="https://dev.to/hschimpf/donde-persisten-los-datos-de-mi-aplicacion-laravel-en-un-entorno-efimero-donde-ni-el-disco-te-2c5d"&gt;capítulo anterior&lt;/a&gt; con la tabla de caché y sesiones, utilizamos &lt;code&gt;TimeToLiveSpecification&lt;/code&gt;; Laravel guarda cada job fallido con fecha de vencimiento &lt;em&gt;(&lt;code&gt;expires_at&lt;/code&gt;, 7 días por defecto &lt;sup id="fnref14"&gt;14&lt;/sup&gt;)&lt;/em&gt;. Este driver ni siquiera implementa &lt;code&gt;queue:flush&lt;/code&gt;: DynamoDB se encarga de limpiar los registros caducados.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Por último, le damos permisos sobre la tabla al rol de ejecución:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;iam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;statements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
          &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;FailedJobsTable.Arn&lt;/span&gt;
          &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:PutItem&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:Query&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:GetItem&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:DeleteItem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para listar o reintentar los jobs fallidos, como en Lambda no hay SSH, usamos el puente &lt;code&gt;bref:cli&lt;/code&gt; que vimos en el &lt;a href="https://dev.to/hschimpf/como-logre-llevar-mi-aplicacion-laravel-al-mundo-serverless-y-como-casi-muero-en-el-intento-4b1h"&gt;segundo capítulo&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless bref:cli &lt;span class="nt"&gt;--args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"queue:failed"&lt;/span&gt;
serverless bref:cli &lt;span class="nt"&gt;--args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"queue:retry all"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Entonces, ¿para qué está la DLQ que creó Lift?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  d) Segunda capa: cuando falla la infraestructura
&lt;/h3&gt;

&lt;p&gt;Un mensaje solo llega a la &lt;em&gt;Dead Letter Queue&lt;/em&gt; cuando &lt;strong&gt;falla la invocación completa de Lambda&lt;/strong&gt;, sin darle oportunidad a Laravel de atrapar el error. Por ejemplo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un job superó el tiempo que le quedaba a la invocación (el &lt;code&gt;pcntl_alarm&lt;/code&gt; se activó y la ejecución salió con &lt;code&gt;exit(1)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Se quedó sin memoria &lt;em&gt;(OOM)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Un error fatal de PHP fuera del alcance del &lt;code&gt;Worker&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Un fallo de arranque: una extensión faltante, un error de configuración, un &lt;code&gt;composer install&lt;/code&gt; que dejó algo afuera.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En cualquiera de estos escenarios, Lambda muere antes de que Bref pueda borrar el mensaje, así que entra en acción el mecanismo nativo de SQS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Espera (&lt;em&gt;visibility timeout&lt;/em&gt;)&lt;/strong&gt;: el mensaje queda oculto hasta que vence su tiempo de visibilidad. Lift lo configura automáticamente en &lt;strong&gt;6 veces el &lt;code&gt;timeout&lt;/code&gt; del worker&lt;/strong&gt; siguiendo la recomendación de AWS &lt;sup id="fnref15"&gt;15&lt;/sup&gt; &lt;em&gt;(con los 60 segundos de nuestro ejemplo, reaparece a los 6 minutos)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reintento&lt;/strong&gt;: una vez visible de nuevo, SQS vuelve a entregárselo a Lambda.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rumbo a la DLQ&lt;/strong&gt;: si la invocación falla 3 veces seguidas &lt;em&gt;(&lt;code&gt;maxRetries&lt;/code&gt; por defecto en Lift)&lt;/em&gt;, SQS retira el mensaje de la cola principal y lo guarda en la &lt;strong&gt;Dead Letter Queue&lt;/strong&gt; hasta por &lt;strong&gt;14 días&lt;/strong&gt;, evitando que se pierda.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Para inspeccionar qué cayó en la DLQ o volver a encolarlo sin entrar a la consola de AWS, Lift nos da estos comandos &lt;sup id="fnref16"&gt;16&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless &lt;span class="nb"&gt;jobs&lt;/span&gt;:logs &lt;span class="nt"&gt;--tail&lt;/span&gt;      &lt;span class="c"&gt;# logs del worker, en vivo&lt;/span&gt;
serverless &lt;span class="nb"&gt;jobs&lt;/span&gt;:failed           &lt;span class="c"&gt;# qué hay en la DLQ&lt;/span&gt;
serverless &lt;span class="nb"&gt;jobs&lt;/span&gt;:failed:retry     &lt;span class="c"&gt;# devolver todo a la cola principal&lt;/span&gt;
serverless &lt;span class="nb"&gt;jobs&lt;/span&gt;:failed:purge     &lt;span class="c"&gt;# borrar todo y seguir con la vida&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claro que nadie quiere ejecutar &lt;code&gt;serverless jobs:failed&lt;/code&gt; todos los días por las dudas. Si agregan la propiedad &lt;code&gt;alarm&lt;/code&gt; al construct, Lift configura automáticamente una alarma en CloudWatch que les envía un email apenas cae un mensaje en la DLQ &lt;sup id="fnref4"&gt;4&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;constructs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;queue&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="na"&gt;alarm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alertas@mi-dominio.com&lt;/span&gt;  &lt;span class="c1"&gt;# alerta por email cuando la DLQ recibe mensajes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Luego del primer &lt;code&gt;serverless deploy&lt;/code&gt;, AWS les enviará un correo de confirmación al email configurado en &lt;code&gt;alarm&lt;/code&gt; &lt;em&gt;(no olviden confirmarlo para activar las alertas)&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  e) Las dos capas, lado a lado
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Primera capa: &lt;code&gt;failed_jobs&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Segunda capa: DLQ de SQS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;¿Qué falló?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nuestro código: excepción en &lt;code&gt;handle()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;La infraestructura: timeout, OOM, error fatal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;¿Quién reintenta?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Laravel (&lt;code&gt;#[Tries]&lt;/code&gt;, &lt;code&gt;#[Backoff]&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;SQS (&lt;em&gt;visibility timeout&lt;/em&gt;, &lt;code&gt;maxRetries&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;¿Qué se reintenta?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ese job, tras el &lt;code&gt;backoff&lt;/code&gt; configurado&lt;/td&gt;
&lt;td&gt;El job en curso y el resto del lote (6× timeout)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;¿Dónde termina?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tabla &lt;code&gt;failed_jobs&lt;/code&gt; (DynamoDB, 7 días)&lt;/td&gt;
&lt;td&gt;Dead Letter Queue (SQS, 14 días)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;¿Cómo lo reintento?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bref:cli --args="queue:retry ..."&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;serverless jobs:failed:retry&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;¿Cómo me entero?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;failed()&lt;/code&gt;, CloudWatch, &lt;code&gt;queue:failed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Alerta por email (&lt;code&gt;alarm:&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fíjense en la fila &lt;strong&gt;¿Qué se reintenta?&lt;/strong&gt; de la segunda capa. Cuando Lambda muere a mitad de un lote &lt;em&gt;(por timeout u OOM)&lt;/em&gt;, los jobs que ya habían terminado fueron eliminados uno a uno con &lt;code&gt;$job-&amp;gt;delete()&lt;/code&gt;, así que no se repiten. SQS solo vuelve a entregar &lt;strong&gt;lo que quedó sin borrar&lt;/strong&gt;: el job que estaba a mitad de ejecución y los que venían detrás en la fila.&lt;/p&gt;

&lt;p&gt;El peligro está en ese job que quedó a medias: cuando SQS lo reintente, &lt;strong&gt;va a correr de nuevo desde el principio&lt;/strong&gt;. Si ya había disparado un cobro o enviado un email un segundo antes de que muriera Lambda y no es &lt;strong&gt;idempotente&lt;/strong&gt;, lo hará por segunda vez &lt;em&gt;(y van a aprender la palabra "reembolso" antes de lo que quisieran)&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Regla de oro: diseñen siempre jobs idempotentes.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. Ajustando las Perillas del Worker para Producción
&lt;/h2&gt;

&lt;p&gt;Las 8 líneas de la sección 3 alcanzan para arrancar, pero ahora que conocemos qué pasa debajo del capó podemos ajustar las perillas para producción. Así se ve una configuración pensada para el mundo real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;constructs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;queue&lt;/span&gt;
    &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;       &lt;span class="c1"&gt;# maxReceiveCount de la cola: AWS recomienda &amp;gt;= 5&lt;/span&gt;
    &lt;span class="na"&gt;maxConcurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;  &lt;span class="c1"&gt;# el freno "limpio": Lambda deja de leer, los mensajes esperan&lt;/span&gt;
    &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bref\LaravelBridge\Queue\QueueHandler&lt;/span&gt;
      &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-85&lt;/span&gt;
      &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${bref-extra:gd-php-85}&lt;/span&gt;
      &lt;span class="na"&gt;reservedConcurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;25&lt;/span&gt;  &lt;span class="c1"&gt;# tope duro y reserva; siempre &amp;gt;= maxConcurrency&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
    &lt;span class="na"&gt;batchSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  a) &lt;code&gt;timeout&lt;/code&gt;: el techo del job más largo
&lt;/h3&gt;

&lt;p&gt;Define tanto el tiempo máximo que puede durar una invocación como el &lt;em&gt;visibility timeout&lt;/em&gt; de la cola en SQS &lt;em&gt;(que Lift multiplica ×6)&lt;/em&gt;. En mi caso lo llevé a 300 segundos porque el worker procesa imágenes pesadas, y redimensionar una foto de 20 MB lleva su tiempo. Si sus jobs solo envían emails o llaman a una API, con 30 o 60 segundos van sobrados.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) &lt;code&gt;batchSize&lt;/code&gt;: menos invocaciones, más responsabilidad
&lt;/h3&gt;

&lt;p&gt;Lift lo deja en 1 por defecto para simplificar el manejo de errores &lt;sup id="fnref17"&gt;17&lt;/sup&gt;. Subirlo a 10 significa que una ráfaga de 500 jobs se procesa en 50 invocaciones en lugar de 500, reduciendo &lt;em&gt;cold-starts&lt;/em&gt; y costos. A cambio, recuerden las dos condiciones que vimos antes: todos los jobs del lote &lt;strong&gt;comparten el mismo reloj&lt;/strong&gt; de Lambda, y nuestros jobs deben ser &lt;strong&gt;idempotentes&lt;/strong&gt; por si una invocación muere a mitad del lote.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Existe también &lt;code&gt;maxBatchingWindow&lt;/code&gt;: cuántos segundos espera SQS para juntar un lote completo antes de invocar. Cero por defecto. Si no les molesta que un job espere unos segundos a que lleguen compañeros, pueden subirlo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) &lt;code&gt;reservedConcurrency&lt;/code&gt; vs &lt;code&gt;maxConcurrency&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Si llegan 1.000 jobs de golpe, Lambda escala rápido hasta cientos de invocaciones simultáneas. Y eso suena hermoso hasta que esos 1.000 jobs pegan al mismo tiempo contra una API externa con &lt;em&gt;rate limit&lt;/em&gt;, contra nuestro servidor SMTP, o contra la misma DynamoDB.&lt;/p&gt;

&lt;p&gt;Hay dos formas de ponerle un techo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reservedConcurrency&lt;/code&gt;&lt;/strong&gt; &lt;em&gt;(propiedad de la función)&lt;/em&gt;: la función nunca ejecuta más de N instancias simultáneas. Pero además &lt;strong&gt;reserva&lt;/strong&gt; esas N de la cuota total de concurrencia de la cuenta, aunque estén sin uso &lt;sup id="fnref18"&gt;18&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;maxConcurrency&lt;/code&gt;&lt;/strong&gt; &lt;em&gt;(propiedad del construct de Lift)&lt;/em&gt;: limita cuántas invocaciones puede disparar el event source mapping de esta cola, sin reservar nada &lt;sup id="fnref19"&gt;19&lt;/sup&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo ideal es combinarlos: &lt;code&gt;maxConcurrency&lt;/code&gt; como freno limpio &lt;em&gt;(los mensajes esperan en la cola sin gastar intentos)&lt;/em&gt; y &lt;code&gt;reservedConcurrency&lt;/code&gt; apenas por encima, como red de seguridad por si un bug despacha jobs en loop.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ojo&lt;/strong&gt;: si usan solo &lt;code&gt;reservedConcurrency&lt;/code&gt;, todo lo que supere ese límite cae en &lt;em&gt;throttling&lt;/em&gt;, y cada rechazo cuenta como un intento en SQS. Con &lt;code&gt;maxRetries: 3&lt;/code&gt; &lt;em&gt;(el default de Lift)&lt;/em&gt;, un pico de tráfico puede mandar jobs directo a la DLQ sin haberse ejecutado ni una vez. Por eso AWS recomienda subir &lt;code&gt;maxRetries&lt;/code&gt; a por lo menos 5 &lt;sup id="fnref20"&gt;20&lt;/sup&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  d) &lt;code&gt;memorySize&lt;/code&gt;: más memoria también es más CPU
&lt;/h3&gt;

&lt;p&gt;Como vimos en el &lt;a href="https://dev.to/hschimpf/como-logre-llevar-mi-aplicacion-laravel-al-mundo-serverless-y-como-casi-muero-en-el-intento-4b1h"&gt;segundo capítulo&lt;/a&gt;, en Lambda la potencia de CPU escala de forma proporcional a la memoria asignada. Un job pesado que tarda 10 segundos con 512 MB puede resolverse en 3 segundos con 1.769 MB; y como AWS cobra por GB-segundo, darle más memoria muchas veces &lt;strong&gt;termina saliendo más barato&lt;/strong&gt;. Si su worker hace trabajo intensivo, pueden asignarle su propio &lt;code&gt;memorySize&lt;/code&gt; y medir el punto dulce con &lt;code&gt;aws-lambda-power-tuning&lt;/code&gt; &lt;sup id="fnref21"&gt;21&lt;/sup&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  e) Layers a medida: extensiones solo donde hacen falta
&lt;/h3&gt;

&lt;p&gt;¿Notaron el layer &lt;code&gt;${bref-extra:gd-php-85}&lt;/code&gt; en la configuración? Mi worker necesita la extensión GD para procesar imágenes, pero la función HTTP &lt;code&gt;web&lt;/code&gt; no. Al ser funciones Lambda separadas, cada una carga &lt;strong&gt;únicamente&lt;/strong&gt; las extensiones que usa: las peticiones web siguen livianas, y el peso extra solo lo carga el worker en segundo plano.&lt;/p&gt;

&lt;h3&gt;
  
  
  f) ¿Hace falta un warmer en el worker?
&lt;/h3&gt;

&lt;p&gt;En el &lt;a href="https://dev.to/hschimpf/como-logre-llevar-mi-aplicacion-laravel-al-mundo-serverless-y-como-casi-muero-en-el-intento-4b1h"&gt;segundo capítulo&lt;/a&gt; implementamos un &lt;em&gt;warmer&lt;/em&gt; cada 5 minutos para evitar el &lt;em&gt;cold-start&lt;/em&gt; en las peticiones web. ¿Necesita uno el worker? &lt;strong&gt;Para nada.&lt;/strong&gt; Un &lt;em&gt;cold-start&lt;/em&gt; de 200 ms en una ruta HTTP lo sufre un usuario mirando cómo carga la pantalla; en una cola en segundo plano, 200 ms extra no le quitan el sueño a nadie.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Lo que No Te Cuentan los Tutoriales (y Descubres en Producción)
&lt;/h2&gt;

&lt;p&gt;Cosas que no están en la documentación &lt;em&gt;(o que estaban en letra chica y aprendí por las malas)&lt;/em&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  a) &lt;code&gt;SerializesModels&lt;/code&gt; y las claves compuestas de DynamoDB
&lt;/h3&gt;

&lt;p&gt;¿Notaron que en &lt;code&gt;SendWelcomeEmail&lt;/code&gt; &lt;em&gt;(sección 5.a)&lt;/em&gt; pasé &lt;code&gt;$user_uuid&lt;/code&gt; como &lt;code&gt;string&lt;/code&gt; en lugar del modelo &lt;code&gt;User&lt;/code&gt;? No fue casualidad. &lt;code&gt;SerializesModels&lt;/code&gt; guarda solo &lt;code&gt;getKey()&lt;/code&gt; &lt;em&gt;(&lt;code&gt;PK&lt;/code&gt;)&lt;/em&gt; e intenta restaurar el modelo con una consulta SQL tradicional que no incluye la &lt;code&gt;SK&lt;/code&gt; ni soporta DynamoDB.&lt;/p&gt;

&lt;p&gt;Para sus propios jobs, lo más simple es pasar el &lt;code&gt;uuid&lt;/code&gt; al constructor y buscar el modelo dentro de &lt;code&gt;handle()&lt;/code&gt;. Ahora bien, si usan notificaciones encoladas &lt;em&gt;(&lt;code&gt;$user-&amp;gt;notify()&lt;/code&gt;)&lt;/em&gt;, Laravel serializa el modelo &lt;code&gt;User&lt;/code&gt; sí o sí: ahí no queda otra que sobreescribir &lt;code&gt;getQueueableId()&lt;/code&gt; y &lt;code&gt;newQueryForRestoration()&lt;/code&gt; en su modelo para codificar y reconstruir &lt;code&gt;PK + SK&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) &lt;code&gt;Bus::batch()&lt;/code&gt; también necesita dónde guardar su progreso
&lt;/h3&gt;

&lt;p&gt;Por defecto busca la tabla relacional &lt;code&gt;job_batches&lt;/code&gt;, pero Laravel incluye soporte nativo para DynamoDB: solo deben crear la tabla &lt;code&gt;job_batches&lt;/code&gt; &lt;em&gt;(partition key &lt;code&gt;application&lt;/code&gt;, sort key &lt;code&gt;id&lt;/code&gt;)&lt;/em&gt;, darle permisos IAM y configurar &lt;code&gt;queue.batching.driver&lt;/code&gt; en &lt;code&gt;dynamodb&lt;/code&gt; &lt;sup id="fnref22"&gt;22&lt;/sup&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) El entorno local no necesita tocar AWS
&lt;/h3&gt;

&lt;p&gt;Como el código de nuestros jobs no cambia según el driver, en su máquina pueden seguir usando &lt;code&gt;QUEUE_CONNECTION=database&lt;/code&gt; &lt;em&gt;(o &lt;code&gt;sync&lt;/code&gt;)&lt;/em&gt; con el &lt;code&gt;queue:work&lt;/code&gt; de toda la vida, o bien emular SQS localmente con herramientas como &lt;strong&gt;MiniStack&lt;/strong&gt; &lt;sup id="fnref23"&gt;23&lt;/sup&gt; o &lt;strong&gt;LocalStack&lt;/strong&gt; &lt;sup id="fnref24"&gt;24&lt;/sup&gt;. &lt;code&gt;QUEUE_CONNECTION=sqs&lt;/code&gt; apuntando a AWS solo hace falta en &lt;code&gt;serverless.yml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  d) El límite es de 1 MiB por mensaje
&lt;/h3&gt;

&lt;p&gt;Es el tope de SQS &lt;sup id="fnref25"&gt;25&lt;/sup&gt;. Los jobs deben transportar identificadores livianos, no archivos en base64 ni colecciones de 10.000 elementos. Si están cerca de ese límite, el diseño les está pidiendo a gritos que el job busque los datos cuando corre en vez de recibirlos en el payload.&lt;/p&gt;

&lt;h3&gt;
  
  
  e) La limpieza del SDK de AWS del segundo capítulo
&lt;/h3&gt;

&lt;p&gt;Si usaron &lt;code&gt;aws/aws-sdk-php&lt;/code&gt; con &lt;code&gt;removeUnusedServices&lt;/code&gt; en &lt;code&gt;composer.json&lt;/code&gt; para reducir el peso del deploy, asegúrense de agregar &lt;code&gt;Sqs&lt;/code&gt; a la lista de servicios permitidos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"extra"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"aws/aws-sdk-php"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"DynamoDb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"S3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Sqs"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Por qué es tan fácil olvidarlo? Porque los tests corren con &lt;code&gt;QUEUE_CONNECTION=sync&lt;/code&gt; o &lt;code&gt;Queue::fake()&lt;/code&gt;, así que el CI pasa todo en verde sin instanciar jamás &lt;code&gt;SqsClient&lt;/code&gt;... hasta que el primer &lt;code&gt;dispatch()&lt;/code&gt; real en producción explota porque la clase fue borrada.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Costos: El Worker que Solo Cobra Cuando Trabaja
&lt;/h2&gt;

&lt;p&gt;Actualicemos la cuenta, como en cada capítulo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Antes (EC2)&lt;/strong&gt;: el worker vivía en una de las instancias &lt;code&gt;t3.small&lt;/code&gt; del &lt;a href="https://dev.to/hschimpf/por-que-migre-mi-aplicacion-laravel-a-aws-serverless-y-por-que-podria-ahorrarte-tiempo-y-dinero-6hp"&gt;primer capítulo&lt;/a&gt; &lt;em&gt;(~$15/mes por instancia)&lt;/em&gt;. Un proceso &lt;code&gt;queue:work&lt;/code&gt; residente con RAM reservada y &lt;code&gt;supervisord&lt;/code&gt; encendido las 24 horas, aunque la cola pasara el 90% del día vacía.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ahora (SQS + Lambda)&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQS&lt;/strong&gt;: el primer millón de requests mensuales es gratis; después cuesta &lt;strong&gt;\$0.40 por millón&lt;/strong&gt; &lt;sup id="fnref26"&gt;26&lt;/sup&gt;. Sumando los ~630.000 requests del polling en reposo y 3 requests por cada job &lt;em&gt;(&lt;code&gt;SendMessage&lt;/code&gt;, &lt;code&gt;ReceiveMessage&lt;/code&gt; y &lt;code&gt;DeleteMessage&lt;/code&gt;)&lt;/em&gt;, pueden procesar hasta &lt;strong&gt;~120.000 jobs al mes por $0.00&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt;: la capa gratuita perpetua incluye &lt;strong&gt;1 millón de invocaciones&lt;/strong&gt; y &lt;strong&gt;400.000 GB-segundos&lt;/strong&gt; al mes. Sin jobs en la cola, el cómputo cuesta exactamente cero.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pongámoslo en números para un mes con &lt;strong&gt;100.000 jobs&lt;/strong&gt; &lt;em&gt;(de ~500 ms promedio con 512 MB de RAM)&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Concepto&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Dentro del Free Tier&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Tarifa Plena &lt;em&gt;(sin Free Tier)&lt;/em&gt;&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Polling SQS (~630k req/mes)&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;$0.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mensajes SQS (300k req/mes)&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;$0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda Worker (100k jobs)&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;$0.44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total mensual&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.81&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;¡Incluso sin la capa gratuita, procesar 100.000 jobs cuesta menos de un dólar al mes!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Y lo más importante: con DynamoDB en &lt;code&gt;PAY_PER_REQUEST&lt;/code&gt;, Lambda facturando por milisegundo y SQS por request, &lt;strong&gt;casi ningún componente del sistema de colas cobra tarifa fija por existir&lt;/strong&gt; &lt;em&gt;(a excepción del polling de SQS, que en mi caso cae dentro del free tier)&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. ¿Qué Sigue?
&lt;/h2&gt;

&lt;p&gt;Hoy jubilamos definitivamente a &lt;code&gt;supervisord&lt;/code&gt; y al proceso &lt;code&gt;queue:work&lt;/code&gt; que corría las 24 horas: los reemplazamos por una cola SQS y un worker en Lambda que solo despierta cuando hay trabajo. Vimos que Bref reutiliza el mismo &lt;code&gt;Worker&lt;/code&gt; de Laravel &lt;em&gt;(así que nuestros jobs no cambian)&lt;/em&gt;, cómo hace su truco en &lt;code&gt;release()&lt;/code&gt; para conservar los reintentos y el backoff, y cómo conviven &lt;code&gt;failed_jobs&lt;/code&gt; en DynamoDB con la &lt;em&gt;Dead Letter Queue&lt;/em&gt; de SQS.&lt;/p&gt;

&lt;p&gt;Pero quedó un cabo suelto entre el &lt;a href="https://dev.to/hschimpf/donde-persisten-los-datos-de-mi-aplicacion-laravel-en-un-entorno-efimero-donde-ni-el-disco-te-2c5d"&gt;capítulo anterior&lt;/a&gt; y este. En la sección 6 les conté que mi worker carga la extensión GD para redimensionar imágenes; ahora bien, si en el capítulo anterior vimos que los archivos viajan directo del navegador a S3 sin pasar por nuestra aplicación... &lt;strong&gt;¿quién le avisa a Laravel que terminó de subirse un archivo para encolar ese job?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;En el próximo capítulo vamos a cerrar ese círculo con &lt;strong&gt;S3 Event Notifications&lt;/strong&gt;, y veremos por qué la solución &lt;em&gt;no&lt;/em&gt; es conectar S3 directo a la cola que acabamos de crear.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Una pregunta para ustedes&lt;/strong&gt;: ¿con qué corren sus colas de Laravel hoy en producción, o cuál fue la peor jugada que les hizo un worker a las 3 AM? Los leo en los comentarios: las mejores historias de producción salen de ahí, no de los post-mortems.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/queues" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/queues&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/queue.md" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/queue.md&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/permissions.md" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/permissions.md&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-DelaySeconds" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-DelaySeconds&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/queues#multiple-queues" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/queues#multiple-queues&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-scaling.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-scaling.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/sqs/pricing/#How_are_Amazon_SQS_requests_priced.3F" rel="noopener noreferrer"&gt;https://aws.amazon.com/sqs/pricing/#How_are_Amazon_SQS_requests_priced.3F&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;&lt;a href="https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/QueueHandler.php#L53-L78" rel="noopener noreferrer"&gt;https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/QueueHandler.php#L53-L78&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;&lt;a href="https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/Worker.php#L10-L39" rel="noopener noreferrer"&gt;https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/Worker.php#L10-L39&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn12"&gt;
&lt;p&gt;&lt;a href="https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/SqsJob.php#L13-L38" rel="noopener noreferrer"&gt;https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/SqsJob.php#L13-L38&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn13"&gt;
&lt;p&gt;&lt;a href="https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/QueueHandler.php#L107-L117" rel="noopener noreferrer"&gt;https://github.com/brefphp/laravel-bridge/blob/a62196d5cb3efe95861fc6b78f7e5606bd2ad09d/src/Queue/QueueHandler.php#L107-L117&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn14"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/queues#storing-failed-jobs-in-dynamodb" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/queues#storing-failed-jobs-in-dynamodb&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn15"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/queue.md#retry-delay" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/queue.md#retry-delay&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn16"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/queue.md#commands" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/queue.md#commands&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn17"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/queue.md#batch-size" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/queue.md#batch-size&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn18"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn19"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/queue.md#max-concurrency" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/queue.md#max-concurrency&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn20"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-configure.html#events-sqs-queueconfig" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-configure.html#events-sqs-queueconfig&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn21"&gt;
&lt;p&gt;&lt;a href="https://github.com/alexcasalboni/aws-lambda-power-tuning" rel="noopener noreferrer"&gt;https://github.com/alexcasalboni/aws-lambda-power-tuning&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn22"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/queues#storing-batches-in-dynamodb" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/queues#storing-batches-in-dynamodb&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn23"&gt;
&lt;p&gt;&lt;a href="https://github.com/ministackorg/ministack" rel="noopener noreferrer"&gt;https://github.com/ministackorg/ministack&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn24"&gt;
&lt;p&gt;&lt;a href="https://github.com/localstack/localstack" rel="noopener noreferrer"&gt;https://github.com/localstack/localstack&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn25"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn26"&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/sqs/pricing/" rel="noopener noreferrer"&gt;https://aws.amazon.com/sqs/pricing/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>serverless</category>
      <category>laravel</category>
      <category>sqs</category>
      <category>lambda</category>
    </item>
    <item>
      <title>Where Does My Laravel App's Data Live in an Ephemeral Environment (Where Not Even the Disk Can Save You)</title>
      <dc:creator>Hermann D. Schimpf</dc:creator>
      <pubDate>Wed, 30 Sep 2026 16:06:47 +0000</pubDate>
      <link>https://dev.to/hschimpf/where-does-my-laravel-apps-data-live-in-an-ephemeral-environment-where-not-even-the-disk-can-save-4k18</link>
      <guid>https://dev.to/hschimpf/where-does-my-laravel-apps-data-live-in-an-ephemeral-environment-where-not-even-the-disk-can-save-4k18</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Versión en español &lt;a href="https://dev.to/hschimpf/donde-persisten-los-datos-de-mi-aplicacion-laravel-en-un-entorno-efimero-donde-ni-el-disco-te-2c5d"&gt;aquí&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmpxj7v9qup7bpjfbxl0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmpxj7v9qup7bpjfbxl0.png" alt="Lambda Ephemeral Environment" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Spoiler&lt;/strong&gt;: Lambda's disk exists. It's ephemeral, read-only, and self-destructs when AWS recycles the environment — and it doesn't tell you when.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;In the &lt;a href="https://dev.to/hschimpf/how-i-managed-to-take-my-laravel-application-to-the-serverless-world-and-how-i-almost-died-trying-3mhd"&gt;previous chapter&lt;/a&gt; we turned Laravel into a digital nomad: we deployed to Lambda, served assets through CloudFront, and won the &lt;em&gt;cold-start&lt;/em&gt; battle with Octane. But a promise was left hanging: where does your data end up when the disk is, literally, a temporary illusion?&lt;/p&gt;

&lt;p&gt;Let's be clear: Lambda's disk &lt;em&gt;exists&lt;/em&gt;. There's a writable &lt;code&gt;/tmp&lt;/code&gt; directory where you can drop temporary files &lt;sup id="fnref1"&gt;1&lt;/sup&gt;. But treating it as real storage is like keeping your life savings in a shared taxi: technically they're in there, but good luck finding them tomorrow. The rest of the filesystem is read-only, and every Lambda instance can vanish at any moment, taking &lt;code&gt;/tmp&lt;/code&gt; with it.&lt;/p&gt;

&lt;p&gt;In this chapter we tackle persistence in three acts: first files (S3, the short part), then our entry into the NoSQL world with cache and sessions (DynamoDB, easy mode), and finally the main course: DynamoDB as the primary database.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Serverless Treasure Map
&lt;/h2&gt;

&lt;p&gt;Before we dive into the code, let's figure out where we're standing. This is the simplified architecture of my application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ko96xyi6x8s6wv5xs9p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ko96xyi6x8s6wv5xs9p.png" alt="AWS Serverless architecture" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In previous chapters we already covered how we use &lt;strong&gt;Lambda&lt;/strong&gt; to run our Laravel app, and how &lt;strong&gt;CloudFront&lt;/strong&gt; splits traffic between &lt;strong&gt;S3&lt;/strong&gt; &lt;em&gt;(assets)&lt;/em&gt; and &lt;strong&gt;Lambda&lt;/strong&gt; &lt;em&gt;(the app)&lt;/em&gt;. Today we'll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send uploads to &lt;strong&gt;S3&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Store session and cache data in &lt;strong&gt;DynamoDB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;And use &lt;strong&gt;DynamoDB&lt;/strong&gt; as our primary database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other services are left for upcoming chapters. Patience, my young padawans.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Uploads to S3: The Short Part
&lt;/h2&gt;

&lt;p&gt;This one is so simple it's almost embarrassing to dedicate a section to it. Almost.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) The same old facade, a different destination
&lt;/h3&gt;

&lt;p&gt;Laravel already abstracts storage with the &lt;code&gt;Storage&lt;/code&gt; facade. On a traditional server it points to the local disk; in serverless, we simply define the bucket &lt;sup id="fnref2"&gt;2&lt;/sup&gt; and switch the driver to &lt;code&gt;s3&lt;/code&gt; &lt;sup id="fnref3"&gt;3&lt;/sup&gt; &lt;sup id="fnref4"&gt;4&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require league/flysystem-aws-s3-v3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;FILESYSTEM_DISK&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
    &lt;span class="na"&gt;AWS_BUCKET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${construct:storage.bucketName}&lt;/span&gt;

&lt;span class="na"&gt;constructs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;storage&lt;/span&gt;
    &lt;span class="c1"&gt;# Flysystem sends ACL headers on every operation and new buckets reject them by default [^12]&lt;/span&gt;
    &lt;span class="na"&gt;allowAcl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="c1"&gt;# CORS is required to upload files directly from the browser using pre-signed URLs&lt;/span&gt;
    &lt;span class="na"&gt;cors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${construct:website.url}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it. Every call to &lt;code&gt;Storage::put()&lt;/code&gt;, &lt;code&gt;Storage::get()&lt;/code&gt; now talks to S3 without touching a single line of business logic. Zero refactoring, pure abstraction, point for Laravel.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) Pre-signed URLs: so uploads don't even go through Lambda
&lt;/h3&gt;

&lt;p&gt;But here comes the clever trick. If the user uploads a file &lt;em&gt;through&lt;/em&gt; your application, you're paying Lambda time for bytes that are just passing by. Remember the $530 bill for idle time from the previous article? Exactly.&lt;/p&gt;

&lt;p&gt;The elegant alternative: &lt;strong&gt;pre-signed URLs&lt;/strong&gt; &lt;sup id="fnref5"&gt;5&lt;/sup&gt;. Laravel generates a temporary S3 URL, the frontend uses it to upload the file &lt;em&gt;directly&lt;/em&gt; to S3, and Lambda never even finds out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Storage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Laravel generates the URL, the browser uploads straight to S3&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;temporaryUploadUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"uploads/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file travels directly from the browser to S3 with no layovers. Generating the URL is a local signature, zero cost. Lambda keeps sleeping, and so does your wallet.&lt;/p&gt;

&lt;p&gt;Lambda &lt;em&gt;(and API Gateway)&lt;/em&gt; limit the payload to a maximum of 6 MB and 10 MB respectively &lt;sup id="fnref6"&gt;6&lt;/sup&gt;. Beyond that size, pre-signed URLs stop being an optimization and become mandatory &lt;sup id="fnref7"&gt;7&lt;/sup&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;But if Lambda keeps sleeping... how does it find out a file was uploaded?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's where the fun begins: S3 can automatically fire events &lt;sup id="fnref8"&gt;8&lt;/sup&gt; when a file is uploaded. That event can invoke a Lambda, broadcast a message through SNS, or emit an event to SQS or EventBridge. But we'll open that Pandora's box in another chapter.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. DynamoDB, Easy Mode: Cache and Sessions
&lt;/h2&gt;

&lt;p&gt;Before using DynamoDB as a database &lt;em&gt;(the final boss of this article)&lt;/em&gt;, let's start where it hurts the least: cache and sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Cache: the table that cleans itself
&lt;/h3&gt;

&lt;p&gt;The setup is insultingly simple &lt;sup id="fnref9"&gt;9&lt;/sup&gt;. We only need to switch the cache store to &lt;code&gt;dynamodb&lt;/code&gt;, and specify the DynamoDB table name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;CACHE_STORE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb&lt;/span&gt;
    &lt;span class="na"&gt;DYNAMODB_CACHE_TABLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;CacheTable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And add the table definition in &lt;code&gt;serverless.yml&lt;/code&gt; &lt;em&gt;(inside &lt;code&gt;resources&lt;/code&gt;, as CloudFormation)&lt;/em&gt; &lt;sup id="fnref10"&gt;10&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;CacheTable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::DynamoDB::Table&lt;/span&gt;
  &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app-cache&lt;/span&gt;
    &lt;span class="na"&gt;BillingMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;
    &lt;span class="na"&gt;AttributeDefinitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key&lt;/span&gt;
        &lt;span class="na"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;S&lt;/span&gt;
    &lt;span class="na"&gt;TimeToLiveSpecification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;expires_at&lt;/span&gt;
      &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;KeySchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key&lt;/span&gt;
        &lt;span class="na"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HASH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beautiful detail: &lt;code&gt;TimeToLiveSpecification&lt;/code&gt;. DynamoDB automatically deletes expired items based on the &lt;code&gt;expires_at&lt;/code&gt; attribute &lt;sup id="fnref11"&gt;11&lt;/sup&gt;. No cleanup cron jobs, no &lt;code&gt;cache:*&lt;/code&gt; commands, nothing. DynamoDB's Roomba does the cleaning while you sleep.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;key&lt;/code&gt; and &lt;code&gt;expires_at&lt;/code&gt; attributes are the default names Laravel uses &lt;sup id="fnref9"&gt;9&lt;/sup&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) A new home for sessions
&lt;/h3&gt;

&lt;p&gt;In the previous article we left sessions in cookies as a quick fix. It works, sure, but it has its limits: cookies travel with every request and are capped at ~4KB. For something more robust, Laravel ships with native DynamoDB support &lt;sup id="fnref12"&gt;12&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SESSION_DRIVER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Internally, Laravel reuses the same cache table &lt;sup id="fnref12"&gt;12&lt;/sup&gt; &lt;em&gt;(with the same automatic TTL &lt;sup id="fnref11"&gt;11&lt;/sup&gt;)&lt;/em&gt;. Sessions expire, DynamoDB sweeps them away, and you keep building features instead of maintaining infrastructure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The honest trade-off&lt;/strong&gt;: cookies were free; now every authenticated request is one read and one write in DynamoDB. With &lt;code&gt;PAY_PER_REQUEST&lt;/code&gt; and modest traffic we're talking cents per month, but it's worth knowing before it shows up on the bill.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With cache and sessions migrated, only one stateful component is left standing: the database.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Grab some coffee, because it's about to get interesting.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. DynamoDB as a Database: The Final Boss
&lt;/h2&gt;

&lt;p&gt;This is where most Laravel devs get nervous. Our whole careers we were raised on &lt;code&gt;CREATE TABLE&lt;/code&gt;, migrations, and JOINs. DynamoDB looks at all of that and says: &lt;em&gt;"that's cute, but we don't do that here"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bnmskobsajll9nfaep1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bnmskobsajll9nfaep1.png" alt="We don't do that here - Black Panther Meme" width="320" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  a) DynamoDB basics for Laravel devs
&lt;/h3&gt;

&lt;p&gt;Let's translate the concepts into our language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Table&lt;/strong&gt;: same as always, but with no migrations and no columns: each item can have whatever attributes it wants. The only exception is the attributes you use as keys or indexes &lt;sup id="fnref13"&gt;13&lt;/sup&gt;: those you do have to declare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Item&lt;/strong&gt;: the equivalent of a row. Basically a JSON with its own identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PK&lt;/strong&gt; &lt;em&gt;(Partition Key)&lt;/em&gt;: defines which physical partition the item lives in. It's the mandatory &lt;code&gt;WHERE&lt;/code&gt; of every query. The only one that gets a pass is &lt;code&gt;Scan&lt;/code&gt;, but in section 4.d you'll see why you won't want to use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SK&lt;/strong&gt; &lt;em&gt;(Sort Key)&lt;/em&gt;: sorts items within the same partition. PK + SK form the composite primary key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GSI&lt;/strong&gt; &lt;em&gt;(Global Secondary Index)&lt;/em&gt; &lt;sup id="fnref14"&gt;14&lt;/sup&gt;: an alternative way to organize items so you can query them by other attributes. The closest thing to an index you'll find.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The golden rule that took me a while to internalize: &lt;strong&gt;in DynamoDB you don't model data, you model access patterns&lt;/strong&gt;. First you think about which queries you'll run, then you design the keys. Unlike SQL, where you model normalized and then fight with the indexes.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) Eloquent on DynamoDB: kitar/laravel-dynamodb
&lt;/h3&gt;

&lt;p&gt;To simplify implementing Eloquent on top of DynamoDB I used the &lt;a href="https://github.com/kitar/laravel-dynamodb" rel="noopener noreferrer"&gt;&lt;code&gt;kitar/laravel-dynamodb&lt;/code&gt;&lt;/a&gt; package, which provides a connection driver and a query builder that translates Eloquent calls into DynamoDB operations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require kitar/laravel-dynamodb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The connection in &lt;code&gt;config/database.php&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'dynamodb'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'driver'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'dynamodb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'key'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_ACCESS_KEY_ID'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'secret'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_SECRET_ACCESS_KEY'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'region'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_DEFAULT_REGION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'us-east-1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'token'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_SESSION_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'endpoint'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DYNAMODB_ENDPOINT'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// useful for local/testing&lt;/span&gt;
    &lt;span class="s1"&gt;'prefix'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'my-app-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// optional prefix for table names&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The values of &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;, and &lt;code&gt;AWS_SESSION_TOKEN&lt;/code&gt; are automatically injected into the environment from the function's execution role. They're temporary credentials that rotate on their own, and the permissions they carry are exactly the ones we declare in IAM &lt;em&gt;(section 4.e)&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DB_CONNECTION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the models... are still Eloquent models, with a couple of extra attributes for the table keys &lt;sup id="fnref15"&gt;15&lt;/sup&gt;, and a bit of glue for the GSI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Casts\Attribute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Kitar\Dynamodb\Model\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$primaryKey&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PK'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// partition key → USER#&amp;lt;uuid&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$sortKey&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SK'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// sort key&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$sortKeyDefault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PROFILE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$fillable&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$appends&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  &lt;span class="c1"&gt;// so it shows up when serializing&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;booted&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;creating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;self&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;PK&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USER#'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;GSI1PK&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USER'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// groups all users together&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Attribute&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// the email lives in GSI1SK: the same index serves both listing users and looking up by email&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Attribute&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;GSI1SK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'GSI1SK'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Careful with the "still Eloquent" part: they inherit mutators, casts, events, serialization and authentication, but not relationships &lt;em&gt;(&lt;code&gt;hasMany()&lt;/code&gt;, &lt;code&gt;belongsTo()&lt;/code&gt;, &lt;code&gt;with()&lt;/code&gt;)&lt;/em&gt;, nor a generic &lt;code&gt;where()&lt;/code&gt; &lt;em&gt;(here it's &lt;code&gt;keyCondition()&lt;/code&gt;, &lt;code&gt;filter()&lt;/code&gt; or &lt;code&gt;condition()&lt;/code&gt;)&lt;/em&gt;, nor arbitrary &lt;code&gt;orderBy()&lt;/code&gt; &lt;em&gt;(only by Sort Key)&lt;/em&gt;, nor page-number &lt;code&gt;paginate()&lt;/code&gt; &lt;em&gt;(DynamoDB paginates by cursor)&lt;/em&gt;, nor &lt;code&gt;count()&lt;/code&gt;/aggregates. It's not a package limitation: DynamoDB simply doesn't know how to do that, and the package is honest enough not to pretend otherwise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why a &lt;code&gt;UUID&lt;/code&gt; and not the &lt;code&gt;email&lt;/code&gt; as the Partition Key, which is what the package's README does? Because in DynamoDB the primary key &lt;strong&gt;is&lt;/strong&gt; the item's identity: it can't be modified, only deleted and recreated. The day a user wants to change their email, you'd have to recreate the item and everything hanging off that PK. An immutable identifier as PK and the email in a GSI lets you look up by email today and change it tomorrow without touching the item's identity.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sortKeyDefault&lt;/code&gt; covers the most common case: &lt;code&gt;User::find('USER#2aab…11a2')&lt;/code&gt; automatically uses &lt;code&gt;PROFILE&lt;/code&gt; as the sort key. And the GSI solves the other two access patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// all users&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GSI1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyCondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GSI1PK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USER'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// the user with this email&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GSI1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyCondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GSI1PK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USER'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyCondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GSI1SK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'foo@bar.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;A detail that bites: the GSI lets you look up by email, but it does not guarantee it's unique. Two users with the same email would happily coexist in the index. If you need uniqueness, you validate it before creating.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;See those weird keys? &lt;code&gt;USER#&amp;lt;uuid&amp;gt;&lt;/code&gt; as the primary key, &lt;code&gt;USER&lt;/code&gt; stuffed into a &lt;code&gt;GSI1&lt;/code&gt; index... that has a name of its own...&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Single-Table Design: the concept (and nothing more)
&lt;/h3&gt;

&lt;p&gt;Those weird keys are the gateway to &lt;strong&gt;Single-Table Design&lt;/strong&gt;: instead of one table per entity (users, posts, comments...), several entities &lt;strong&gt;share a single table&lt;/strong&gt;, told apart by their keys. The &lt;code&gt;PK&lt;/code&gt; can identify the data's "owner" (&lt;code&gt;USER#123&lt;/code&gt;), the &lt;code&gt;SK&lt;/code&gt; what kind of thing it is (&lt;code&gt;PROFILE&lt;/code&gt;, &lt;code&gt;POST#456&lt;/code&gt;), and GSIs give you alternative views for other access patterns.&lt;/p&gt;

&lt;p&gt;Sounds like black magic? It isn't, but it is an art: properly designing a single-table layout requires mapping &lt;em&gt;all&lt;/em&gt; your access patterns before writing a single line of code, something you can improvise on the fly in SQL.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I won't go deeper here because the topic deserves its own article (and it will get one). Today you only need to know it exists, and that it's the reason DynamoDB can replace a full relational database.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  d) When DynamoDB beats SQL, and when it doesn't
&lt;/h3&gt;

&lt;p&gt;Let's be honest, no smoke and mirrors. DynamoDB is &lt;strong&gt;brutal&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You know the access patterns&lt;/strong&gt; upfront &lt;em&gt;(get by id, list by owner, filter by status)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need predictable latency&lt;/strong&gt;: single-digit millisecond reads, at any scale &lt;sup id="fnref16"&gt;16&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic is unpredictable&lt;/strong&gt;: huge spikes or no traffic at all. &lt;code&gt;PAY_PER_REQUEST&lt;/code&gt;: you pay per request, so with almost no traffic the bill is almost zero; with sustained high traffic, consider provisioned mode &lt;sup id="fnref17"&gt;17&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to scale without drama&lt;/strong&gt;: there's no "master" node dying at 4 AM, no read replicas to configure to bypass &lt;code&gt;max_connections&lt;/code&gt;. Scalability stops being your problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB Streams&lt;/strong&gt; &lt;sup id="fnref18"&gt;18&lt;/sup&gt;: every write can fire events. The equivalent of MySQL's &lt;code&gt;binlog&lt;/code&gt;, but managed and capable of running code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero management&lt;/strong&gt;: no patching, no &lt;code&gt;VACUUM&lt;/code&gt;, no scheduled maintenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it's &lt;strong&gt;not&lt;/strong&gt; for you if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You need to sort or filter by any attribute&lt;/strong&gt;: in DynamoDB you only sort by Sort Key and only search efficiently by keys (or GSIs). Everything else is &lt;code&gt;filter()&lt;/code&gt; over what's already been read: you pay to read and then discard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need JOINs and ad-hoc queries&lt;/strong&gt;: "show me all users who bought X last month grouped by state" → do it in SQL or export to a data warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You depend on &lt;code&gt;UNIQUE&lt;/code&gt;, foreign keys or &lt;code&gt;ON DELETE CASCADE&lt;/code&gt;&lt;/strong&gt;: they don't exist. Uniqueness is enforced by hand with condition expressions (&lt;code&gt;attribute_not_exists(PK)&lt;/code&gt;), and referential integrity is your code's responsibility, not the database's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your data models change every week&lt;/strong&gt;: redesigning keys in DynamoDB hurts more than a migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You depend on reporting/analytics&lt;/strong&gt;: dashboards, aggregated charts, exploratory queries → you need SQL. DynamoDB is not the place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to store giant items&lt;/strong&gt;: DynamoDB has a maximum of 400KB per item &lt;sup id="fnref19"&gt;19&lt;/sup&gt;. Large documents go in S3, and only the reference goes in DynamoDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Scan&lt;/code&gt; as a strategy&lt;/strong&gt; &lt;sup id="fnref20"&gt;20&lt;/sup&gt;: you read the whole table and pay for all of it. If your query is solved with a scan, the design failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Access patterns like "posts by user", "comments by post", or direct reads by ID fit perfectly in the first group. Use cases tailor-made for DynamoDB.&lt;/p&gt;

&lt;h3&gt;
  
  
  e) Infrastructure is code too
&lt;/h3&gt;

&lt;p&gt;Tables are declared as CloudFormation resources &lt;sup id="fnref21"&gt;21&lt;/sup&gt;. The table for the example &lt;code&gt;User&lt;/code&gt; model would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;UsersTable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::DynamoDB::Table&lt;/span&gt;
  &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app-users&lt;/span&gt;
    &lt;span class="na"&gt;BillingMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;
    &lt;span class="na"&gt;AttributeDefinitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;     &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;SK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;     &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;GSI1PK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;GSI1SK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;KeySchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;HASH&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;SK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RANGE&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;GlobalSecondaryIndexes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;IndexName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GSI1&lt;/span&gt;
        &lt;span class="na"&gt;KeySchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;GSI1PK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;HASH&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;GSI1SK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RANGE&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;Projection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;ProjectionType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ALL&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the minimum IAM permissions in the main &lt;code&gt;serverless.yml&lt;/code&gt; &lt;sup id="fnref22"&gt;22&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;iam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;statements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
          &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CacheTable.Arn&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;UsersTable.Arn&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${Resource}/index/*'&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="nv"&gt;UsersTable.Arn&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:Query&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:GetItem&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:PutItem&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:UpdateItem&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:DeleteItem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;: We must never use &lt;code&gt;Action: dynamodb:*&lt;/code&gt; or &lt;code&gt;Resource: *&lt;/code&gt;. Least privilege or nothing &lt;sup id="fnref23"&gt;23&lt;/sup&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  f) Costs: the last fixed bill goes away
&lt;/h3&gt;

&lt;p&gt;Let's update the tally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The debt from the first chapter&lt;/strong&gt;: Back there we left an RDS MySQL &lt;code&gt;db.t4g.small&lt;/code&gt; running, which costs &lt;strong&gt;~$26/month&lt;/strong&gt; (≈ $0.032/h × 730 h + storage &lt;sup id="fnref24"&gt;24&lt;/sup&gt;) for a database that charges just for existing. Today we retire it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With DynamoDB&lt;/strong&gt;: We can use &lt;code&gt;BillingMode: PAY_PER_REQUEST&lt;/code&gt; and pay only for processed requests and stored GB. With modest traffic, the monthly bill is measured in cents: the database that charged for breathing is gone, and no &lt;em&gt;major&lt;/em&gt; component charges a flat fee for resources nobody is using anymore.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. What's Next?
&lt;/h2&gt;

&lt;p&gt;Today we figured out where everything persists: files in S3 (with uploads that never even touch Lambda), cache and sessions in DynamoDB with automatic expiration, and a NoSQL database that speaks Eloquent.&lt;/p&gt;

&lt;p&gt;But the map still has unexplored areas. In the next article: &lt;strong&gt;queues&lt;/strong&gt;. We'll see how SQS + a Lambda worker replace the &lt;code&gt;queue:work&lt;/code&gt; that ran forever under a supervisor, and why that's better than praying to &lt;code&gt;supervisord&lt;/code&gt; at 3 AM.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A question for you&lt;/strong&gt;: have you already used DynamoDB with Laravel, or are you still on Team MySQL/PostgreSQL? Tell me in the comments what holds you back (or what made you fall in love) about the NoSQL world.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/environment/storage" rel="noopener noreferrer"&gt;https://bref.sh/docs/environment/storage&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/storage.md" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/storage.md&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/filesystem#s3-driver-configuration" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/filesystem#s3-driver-configuration&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/file-storage" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/file-storage&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/filesystem#temporary-upload-urls" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/filesystem#temporary-upload-urls&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/file-storage#uploading-files" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/file-storage#uploading-files&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/cache#dynamodb" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/cache#dynamodb&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/environment/storage#deploying-dynamodb-tables" rel="noopener noreferrer"&gt;https://bref.sh/docs/environment/storage#deploying-dynamodb-tables&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn12"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/session#configuration" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/session#configuration&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn13"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn14"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn15"&gt;
&lt;p&gt;&lt;a href="https://github.com/kitar/laravel-dynamodb#extending-the-base-model" rel="noopener noreferrer"&gt;https://github.com/kitar/laravel-dynamodb#extending-the-base-model&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn16"&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/database/understanding-amazon-dynamodb-latency/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/database/understanding-amazon-dynamodb-latency/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn17"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/capacity.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/capacity.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn18"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn19"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-use-s3-too.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-use-s3-too.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn20"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-query-scan.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-query-scan.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn21"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-dynamodb-table.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-dynamodb-table.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn22"&gt;
&lt;p&gt;&lt;a href="https://www.serverless.com/framework/docs/providers/aws/guide/iam" rel="noopener noreferrer"&gt;https://www.serverless.com/framework/docs/providers/aws/guide/iam&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn23"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn24"&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/rds/mysql/pricing/" rel="noopener noreferrer"&gt;https://aws.amazon.com/rds/mysql/pricing/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>serverless</category>
      <category>laravel</category>
      <category>dynamodb</category>
      <category>s3</category>
    </item>
    <item>
      <title>Dónde Persisten los Datos de Mi Aplicación Laravel en un Entorno Efímero (Donde Ni el Disco Te Salva)</title>
      <dc:creator>Hermann D. Schimpf</dc:creator>
      <pubDate>Wed, 30 Sep 2026 04:56:00 +0000</pubDate>
      <link>https://dev.to/hschimpf/donde-persisten-los-datos-de-mi-aplicacion-laravel-en-un-entorno-efimero-donde-ni-el-disco-te-2c5d</link>
      <guid>https://dev.to/hschimpf/donde-persisten-los-datos-de-mi-aplicacion-laravel-en-un-entorno-efimero-donde-ni-el-disco-te-2c5d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;English version &lt;a href="https://dev.to/hschimpf/where-does-my-laravel-apps-data-live-in-an-ephemeral-environment-where-not-even-the-disk-can-save-4k18"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmpxj7v9qup7bpjfbxl0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmpxj7v9qup7bpjfbxl0.png" alt="Lambda Entorno Efímero" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Spoiler&lt;/strong&gt;: El disco de Lambda existe. Es efímero, de solo lectura y se autodestruye cuando AWS recicla el entorno — y no avisa cuándo.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;En el &lt;a href="https://dev.to/hschimpf/como-logre-llevar-mi-aplicacion-laravel-al-mundo-serverless-y-como-casi-muero-en-el-intento-4b1h"&gt;capítulo anterior&lt;/a&gt; convertimos a Laravel en un nómada digital: desplegamos en Lambda, servimos los assets con CloudFront y ganamos la batalla del &lt;em&gt;cold-start&lt;/em&gt; con Octane. Pero quedó una promesa pendiente: ¿dónde van a parar los datos cuando el disco es, literalmente, una ilusión temporal?&lt;/p&gt;

&lt;p&gt;Hablemos claro: el disco en Lambda &lt;em&gt;existe&lt;/em&gt;. Hay un directorio &lt;code&gt;/tmp&lt;/code&gt; escribible donde pueden tirar archivos temporales &lt;sup id="fnref1"&gt;1&lt;/sup&gt;. Pero tratarlo como almacenamiento real es como guardar tus ahorros en un taxi compartido: técnicamente están ahí, pero buena suerte encontrándolos mañana. El resto del filesystem es de solo lectura, y cada instancia de Lambda puede desaparecer en cualquier momento llevándose &lt;code&gt;/tmp&lt;/code&gt; con ella.&lt;/p&gt;

&lt;p&gt;En este capítulo atacamos la persistencia en tres actos: primero los archivos (S3, la parte corta), después nuestra entrada al mundo NoSQL con caché y sesiones (DynamoDB, modo fácil), y finalmente el plato fuerte: DynamoDB como base de datos principal.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. El Mapa del Tesoro Serverless
&lt;/h2&gt;

&lt;p&gt;Antes de meternos en el código, ubiquemos dónde estamos parados. Esta es la arquitectura simplificada de mi aplicación:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ko96xyi6x8s6wv5xs9p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ko96xyi6x8s6wv5xs9p.png" alt="AWS Serverless architecture" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En los capítulos previos ya hablamos sobre cómo utilizamos &lt;strong&gt;Lambda&lt;/strong&gt; para ejecutar nuestra App Laravel, y cómo &lt;strong&gt;CloudFront&lt;/strong&gt; reparte el tráfico entre &lt;strong&gt;S3&lt;/strong&gt; &lt;em&gt;(assets)&lt;/em&gt; y &lt;strong&gt;Lambda&lt;/strong&gt; &lt;em&gt;(la app)&lt;/em&gt;. Hoy toca:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enviar las subidas a &lt;strong&gt;S3&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Almacenar los datos de sesión y caché en &lt;strong&gt;DynamoDB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;y utilizar &lt;strong&gt;DynamoDB&lt;/strong&gt; como nuestra base de datos principal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los demás servicios quedan para los próximos capítulos. Paciencia, mis jóvenes padawans.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Uploads a S3: La Parte Corta
&lt;/h2&gt;

&lt;p&gt;Este punto es tan simple que casi da vergüenza dedicarle una sección. Casi.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) El mismo facade de siempre, otro destino
&lt;/h3&gt;

&lt;p&gt;Laravel ya abstrae el almacenamiento con el facade &lt;code&gt;Storage&lt;/code&gt;. En un servidor tradicional apunta al disco local; en serverless, simplemente definimos el bucket &lt;sup id="fnref2"&gt;2&lt;/sup&gt; y cambiamos el driver a &lt;code&gt;s3&lt;/code&gt; &lt;sup id="fnref3"&gt;3&lt;/sup&gt; &lt;sup id="fnref4"&gt;4&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require league/flysystem-aws-s3-v3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;FILESYSTEM_DISK&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
    &lt;span class="na"&gt;AWS_BUCKET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${construct:storage.bucketName}&lt;/span&gt;

&lt;span class="na"&gt;constructs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;storage&lt;/span&gt;
    &lt;span class="c1"&gt;# Flysystem envía headers ACL en cada operación y los buckets nuevos los rechazan por defecto [^12]&lt;/span&gt;
    &lt;span class="na"&gt;allowAcl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="c1"&gt;# CORS es requerido para subir ficheros directamente desde el navegador utilizando URLs pre-firmadas&lt;/span&gt;
    &lt;span class="na"&gt;cors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${construct:website.url}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y listo. Toda llamada a &lt;code&gt;Storage::put()&lt;/code&gt;, &lt;code&gt;Storage::get()&lt;/code&gt; ahora habla con S3 sin tocar una línea de lógica de negocio. Cero refactor, pura abstracción, punto para Laravel.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) URLs pre-firmadas: para que los uploads ni siquiera pasen por Lambda
&lt;/h3&gt;

&lt;p&gt;Pero acá viene el truco fino. Si el usuario sube un archivo &lt;em&gt;a través&lt;/em&gt; de la aplicación, están pagando tiempo de Lambda por bytes que solo están de paso. ¿Recuerdan la factura de $530 por tiempo inactivo del artículo anterior? Exacto.&lt;/p&gt;

&lt;p&gt;La alternativa elegante: &lt;strong&gt;URLs pre-firmadas&lt;/strong&gt; &lt;sup id="fnref5"&gt;5&lt;/sup&gt;. Laravel genera una URL temporal de S3, el frontend la usa para subir el archivo &lt;em&gt;directamente&lt;/em&gt; a S3, y Lambda ni se entera:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Storage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Laravel genera la URL, el navegador sube directo a S3&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;temporaryUploadUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"uploads/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El archivo viaja directo desde el navegador a S3 sin escalas. Generar la URL es una firma local, costo cero. Lambda sigue durmiendo, y sus billeteras también.&lt;/p&gt;

&lt;p&gt;Lambda &lt;em&gt;(y API Gateway)&lt;/em&gt; limitan el payload a un máximo de 6 MB y 10 MB respectivamente &lt;sup id="fnref6"&gt;6&lt;/sup&gt;. Pasado ese tamaño, las URLs pre-firmadas dejan de ser una optimización y se vuelven obligatorias &lt;sup id="fnref7"&gt;7&lt;/sup&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Pero, si Lambda sigue durmiendo... ¿Cómo se entera de que se subió un archivo?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ahí empieza lo divertido: S3 puede disparar eventos automáticamente &lt;sup id="fnref8"&gt;8&lt;/sup&gt; cuando se sube un archivo. Ese evento puede invocar una Lambda, transmitir un mensaje por SNS, emitir un evento a SQS o EventBridge. Pero esa caja de Pandora la abriremos en otro capítulo.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. DynamoDB, Modo Fácil: Caché y Sesiones
&lt;/h2&gt;

&lt;p&gt;Antes de usar DynamoDB como base de datos &lt;em&gt;(el boss final de este artículo)&lt;/em&gt;, empecemos por donde duele menos: el caché y las sesiones.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Caché: la tabla que se limpia sola
&lt;/h3&gt;

&lt;p&gt;La configuración es insultantemente simple &lt;sup id="fnref9"&gt;9&lt;/sup&gt;. Solo debemos cambiar el store de caché a &lt;code&gt;dynamodb&lt;/code&gt;, y especificar el nombre de la tabla DynamoDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;CACHE_STORE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb&lt;/span&gt;
    &lt;span class="na"&gt;DYNAMODB_CACHE_TABLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;CacheTable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y agregar la definición de tabla en &lt;code&gt;serverless.yml&lt;/code&gt; &lt;em&gt;(dentro de &lt;code&gt;resources&lt;/code&gt;, como CloudFormation)&lt;/em&gt; &lt;sup id="fnref10"&gt;10&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;CacheTable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::DynamoDB::Table&lt;/span&gt;
  &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app-cache&lt;/span&gt;
    &lt;span class="na"&gt;BillingMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;
    &lt;span class="na"&gt;AttributeDefinitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key&lt;/span&gt;
        &lt;span class="na"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;S&lt;/span&gt;
      &lt;span class="na"&gt;TimeToLiveSpecification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;expires_at&lt;/span&gt;
        &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;KeySchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key&lt;/span&gt;
          &lt;span class="na"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HASH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El detalle hermoso: &lt;code&gt;TimeToLiveSpecification&lt;/code&gt;. DynamoDB borra automáticamente los items expirados según el atributo &lt;code&gt;expires_at&lt;/code&gt; &lt;sup id="fnref11"&gt;11&lt;/sup&gt;. Sin cron jobs de limpieza, sin comandos &lt;code&gt;cache:*&lt;/code&gt;, sin nada. El Roomba de DynamoDB hace el aseo mientras ustedes duermen.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Los atributos &lt;code&gt;key&lt;/code&gt; y &lt;code&gt;expires_at&lt;/code&gt; son los nombres por defecto que utiliza Laravel &lt;sup id="fnref9"&gt;9&lt;/sup&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) Un nuevo hogar para las sesiones
&lt;/h3&gt;

&lt;p&gt;En el artículo anterior dejamos las sesiones en cookies como solución rápida. Funciona sí, pero tiene sus límites: las cookies viajan en cada request y tienen tope de ~4KB. Para algo más robusto, Laravel trae soporte nativo para DynamoDB &lt;sup id="fnref12"&gt;12&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SESSION_DRIVER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Internamente, Laravel reutiliza la misma tabla de caché &lt;sup id="fnref12"&gt;12&lt;/sup&gt; &lt;em&gt;(con el mismo TTL automático &lt;sup id="fnref11"&gt;11&lt;/sup&gt;)&lt;/em&gt;. Las sesiones expiran, DynamoDB las barre, y ustedes siguen programando features en vez de mantener infraestructura.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;El trade-off honesto&lt;/strong&gt;: las cookies eran gratis; ahora cada request autenticado es una lectura y una escritura en DynamoDB. Con &lt;code&gt;PAY_PER_REQUEST&lt;/code&gt; y tráfico modesto hablamos de centavos al mes, pero conviene saberlo antes de que aparezca en la factura.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Con caché y sesiones ya migrados, solo queda un componente stateful en pie: la base de datos.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Preparen café, que aquí se pone interesante.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. DynamoDB como Base de Datos: El Boss Final
&lt;/h2&gt;

&lt;p&gt;Acá es donde la mayoría de los devs Laravel se ponen nerviosos. Durante toda nuestra carrera fuimos criados a base de &lt;code&gt;CREATE TABLE&lt;/code&gt;, migraciones y JOINs. DynamoDB mira todo eso y dice: &lt;em&gt;"qué lindo, pero acá no hacemos nada de eso"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bnmskobsajll9nfaep1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bnmskobsajll9nfaep1.png" alt="We don't do that here - Black Panther Meme" width="320" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Básicos de DynamoDB para devs Laravel
&lt;/h3&gt;

&lt;p&gt;Traduzcamos los conceptos a nuestro idioma:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tabla&lt;/strong&gt;: igual que siempre, pero sin migraciones ni columnas: cada item puede tener los atributos que quiera. La única excepción son los atributos que usen como clave o índice &lt;sup id="fnref13"&gt;13&lt;/sup&gt;: esos sí hay que declararlos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Item&lt;/strong&gt;: el equivalente a una fila. Básicamente un JSON con identidad propia.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PK&lt;/strong&gt; &lt;em&gt;(Partition Key)&lt;/em&gt;: define en qué partición física vive el item. Es el &lt;code&gt;WHERE&lt;/code&gt; obligatorio de toda consulta. La única que se salva es &lt;code&gt;Scan&lt;/code&gt;, pero en la sección 4.d van a ver por qué no querrán usarla.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SK&lt;/strong&gt; &lt;em&gt;(Sort Key)&lt;/em&gt;: ordena los items dentro de una misma partición. PK + SK forman la clave primaria compuesta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GSI&lt;/strong&gt; &lt;em&gt;(Global Secondary Index)&lt;/em&gt; &lt;sup id="fnref14"&gt;14&lt;/sup&gt;: una forma alternativa de organizar los items para consultarlos por otros atributos. Lo más parecido a un índice que van a encontrar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La regla de oro que me costó interiorizar: &lt;strong&gt;en DynamoDB no modelan datos, modelan los patrones de acceso&lt;/strong&gt;. Primero piensan qué queries van a hacer, después diseñan las claves. A diferencia de SQL, donde modelan normalizado y después se pelean con los índices.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) Eloquent sobre DynamoDB: kitar/laravel-dynamodb
&lt;/h3&gt;

&lt;p&gt;Para simplificar la implementación de Eloquent con DynamoDB utilicé el paquete &lt;a href="https://github.com/kitar/laravel-dynamodb" rel="noopener noreferrer"&gt;&lt;code&gt;kitar/laravel-dynamodb&lt;/code&gt;&lt;/a&gt;, que provee un driver de conexión y un query builder que traduce las llamadas de Eloquent a operaciones de DynamoDB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require kitar/laravel-dynamodb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La conexión en &lt;code&gt;config/database.php&lt;/code&gt; se ve así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'dynamodb'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'driver'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'dynamodb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'key'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_ACCESS_KEY_ID'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'secret'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_SECRET_ACCESS_KEY'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'region'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_DEFAULT_REGION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'us-east-1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'token'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AWS_SESSION_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'endpoint'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DYNAMODB_ENDPOINT'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// útil para local/testing&lt;/span&gt;
    &lt;span class="s1"&gt;'prefix'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'my-app-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// prefijo opcional para nombres de tabla&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Los valores de &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;, y &lt;code&gt;AWS_SESSION_TOKEN&lt;/code&gt; son inyectados automáticamente en el entorno a partir del rol de ejecución de la función. Son credenciales temporales que rotan solas, y los permisos que tienen son exactamente los que declaremos en IAM &lt;em&gt;(sección 4.e)&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DB_CONNECTION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y los modelos... siguen siendo modelos Eloquent, con un par de atributos extra para las claves de la tabla &lt;sup id="fnref15"&gt;15&lt;/sup&gt;, y un poco de pegamento para el GSI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Casts\Attribute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Kitar\Dynamodb\Model\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$primaryKey&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PK'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// partition key → USER#&amp;lt;uuid&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$sortKey&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SK'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// sort key&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$sortKeyDefault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PROFILE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$fillable&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$appends&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  &lt;span class="c1"&gt;// para que aparezca al serializar&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;booted&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;creating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;self&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;PK&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USER#'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;GSI1PK&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USER'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// agrupa a todos los usuarios&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Attribute&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// el email vive en GSI1SK: el mismo índice sirve para listar usuarios y para buscar por email&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Attribute&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;GSI1SK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'GSI1SK'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Ojo con el "siguen siendo Eloquent": heredan mutators, casts, eventos, serialización y autenticación, pero no relaciones &lt;em&gt;(&lt;code&gt;hasMany()&lt;/code&gt;, &lt;code&gt;belongsTo()&lt;/code&gt;, &lt;code&gt;with()&lt;/code&gt;)&lt;/em&gt;, ni &lt;code&gt;where()&lt;/code&gt; genérico &lt;em&gt;(acá es &lt;code&gt;keyCondition()&lt;/code&gt;, &lt;code&gt;filter()&lt;/code&gt; o &lt;code&gt;condition()&lt;/code&gt;)&lt;/em&gt;, ni &lt;code&gt;orderBy()&lt;/code&gt; arbitrario &lt;em&gt;(solo por Sort Key)&lt;/em&gt;, ni &lt;code&gt;paginate()&lt;/code&gt; por número de página &lt;em&gt;(DynamoDB pagina por cursor)&lt;/em&gt;, ni &lt;code&gt;count()&lt;/code&gt;/agregados. No es limitación del paquete: es que DynamoDB no sabe hacer eso, y el paquete tiene la honestidad de no fingirlo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;¿Por qué un &lt;code&gt;UUID&lt;/code&gt; y no el &lt;code&gt;email&lt;/code&gt; como Partition Key, que es lo que hace el README del paquete? Porque en DynamoDB la clave primaria &lt;strong&gt;es&lt;/strong&gt; la identidad del item: no se modifica, solo se borra y se vuelve a crear. El día que un usuario quiera cambiar su email, tendrían que recrear el item y todo lo que cuelgue de esa PK. Un identificador inmutable como PK y el email en un GSI les permite buscar por email hoy y cambiarlo mañana sin tocar la identidad del item.&lt;/p&gt;

&lt;p&gt;El &lt;code&gt;sortKeyDefault&lt;/code&gt; cubre el caso más común: &lt;code&gt;User::find('USER#2aab…11a2')&lt;/code&gt; usa &lt;code&gt;PROFILE&lt;/code&gt; como sort key automáticamente. Y el GSI resuelve los otros dos patrones de acceso:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// todos los usuarios&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GSI1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyCondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GSI1PK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USER'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// el usuario con este email&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GSI1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyCondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GSI1PK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USER'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyCondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GSI1SK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'foo@bar.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Un detalle que muerde: el GSI permite buscar por email, pero no garantiza que sea único. Dos usuarios con el mismo email convivirían felices en el índice. Si necesitan unicidad, se valida antes de crear.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;¿Ven esas claves raras? &lt;code&gt;USER#&amp;lt;uuid&amp;gt;&lt;/code&gt; como clave primaria, &lt;code&gt;USER&lt;/code&gt; metido en un índice &lt;code&gt;GSI1&lt;/code&gt;... eso tiene nombre propio...&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Single-Table Design: el concepto (y nada más)
&lt;/h3&gt;

&lt;p&gt;Esas claves raras son la puerta de entrada al &lt;strong&gt;Single-Table Design&lt;/strong&gt;: en lugar de una tabla por entidad (users, posts, comments...), varias entidades &lt;strong&gt;comparten una sola tabla&lt;/strong&gt; diferenciándose por sus claves. La &lt;code&gt;PK&lt;/code&gt; puede identificar el "dueño" del dato (&lt;code&gt;USER#123&lt;/code&gt;), la &lt;code&gt;SK&lt;/code&gt; qué cosa es (&lt;code&gt;PROFILE&lt;/code&gt;, &lt;code&gt;POST#456&lt;/code&gt;), y los GSIs les dan vistas alternativas para otros patrones de acceso.&lt;/p&gt;

&lt;p&gt;¿Suena a magia negra? No lo es, pero sí es un arte: diseñar bien una tabla "single-table" requiere mapear &lt;em&gt;todos&lt;/em&gt; los patrones de acceso antes de escribir una línea de código, algo que en SQL pueden improvisar sobre la marcha.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;No voy a profundizar acá porque el tema merece su propio artículo (y lo tendrá). Hoy solo necesitan saber que existe, y que es la razón por la que DynamoDB puede reemplazar a una base relacional completa.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  d) Cuándo DynamoDB le gana a SQL, y cuándo no
&lt;/h3&gt;

&lt;p&gt;Seamos honestos, sin vender humo. DynamoDB es &lt;strong&gt;brutal&lt;/strong&gt; cuando:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conocen los patrones de acceso&lt;/strong&gt; de antemano &lt;em&gt;(get by id, list by owner, filter by status)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Necesitan latencia predecible&lt;/strong&gt;: lecturas de un dígito de milisegundos, a cualquier escala &lt;sup id="fnref16"&gt;16&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El tráfico es impredecible&lt;/strong&gt;: picos gigantes o nada de tráfico. &lt;code&gt;PAY_PER_REQUEST&lt;/code&gt;: pagan por request, así que con tráfico casi nulo la factura es casi nula; con tráfico sostenido y alto, evalúen el modo provisionado &lt;sup id="fnref17"&gt;17&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quieren escalar sin dramas&lt;/strong&gt;: no hay nodo "master" que muera a las 4 AM, no hay réplicas de solo lectura que configurar para bypassear &lt;code&gt;max_connections&lt;/code&gt;. La escalabilidad deja de ser un problema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB Streams&lt;/strong&gt; &lt;sup id="fnref18"&gt;18&lt;/sup&gt;: cada escritura puede disparar eventos. El equivalente al &lt;code&gt;binlog&lt;/code&gt; de MySQL, pero administrado y capaz de ejecutar código.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gestión cero&lt;/strong&gt;: sin parches, sin &lt;code&gt;VACUUM&lt;/code&gt;, sin mantenimiento programado.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y &lt;strong&gt;no&lt;/strong&gt; es para ustedes si:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Necesitan ordenar o filtrar por cualquier atributo&lt;/strong&gt;: en DynamoDB solo se ordena por Sort Key y solo se busca eficientemente por claves (o GSIs). Todo lo demás es &lt;code&gt;filter()&lt;/code&gt; sobre lo ya leído: pagan por leer y después descartan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Necesitan JOINs y queries ad-hoc&lt;/strong&gt;: "mostrame todos los usuarios que compraron X en el último mes agrupado por provincia" → háganlo en SQL o exporten a un data warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependen de &lt;code&gt;UNIQUE&lt;/code&gt;, foreign keys o &lt;code&gt;ON DELETE CASCADE&lt;/code&gt;&lt;/strong&gt;: no existen. La unicidad se garantiza a mano con condition expressions (&lt;code&gt;attribute_not_exists(PK)&lt;/code&gt;), y la integridad referencial es responsabilidad de su código, no de la base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sus modelos de datos cambian cada semana&lt;/strong&gt;: rediseñar claves en DynamoDB duele más que una migración.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependen de reporting/analytics&lt;/strong&gt;: dashboards, gráficos agregados, queries exploratorias → necesitan SQL. DynamoDB no es el lugar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Necesitan almacenar ítems gigantes&lt;/strong&gt;: DynamoDB tiene un máximo de 400KB por item &lt;sup id="fnref19"&gt;19&lt;/sup&gt;. Los documentos grandes van en S3, y en DynamoDB solo la referencia.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Scan&lt;/code&gt; como estrategia&lt;/strong&gt; &lt;sup id="fnref20"&gt;20&lt;/sup&gt;: leen toda la tabla y pagan todo. Si la consulta se resuelve con scan, el diseño falló.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Patrones de acceso tipo «posts por usuario», «comentarios por post» o lecturas directas por ID encajan perfecto en el primer grupo. Casos de uso hechos a medida para DynamoDB.&lt;/p&gt;

&lt;h3&gt;
  
  
  e) La infraestructura también es código
&lt;/h3&gt;

&lt;p&gt;Las tablas viven declaradas como recursos CloudFormation &lt;sup id="fnref21"&gt;21&lt;/sup&gt;. La tabla del modelo &lt;code&gt;User&lt;/code&gt; del ejemplo se vería así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;UsersTable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::DynamoDB::Table&lt;/span&gt;
  &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app-users&lt;/span&gt;
    &lt;span class="na"&gt;BillingMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;
    &lt;span class="na"&gt;AttributeDefinitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;     &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;SK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;     &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;GSI1PK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;GSI1SK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;S&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;KeySchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;HASH&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;SK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RANGE&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;GlobalSecondaryIndexes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;IndexName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GSI1&lt;/span&gt;
        &lt;span class="na"&gt;KeySchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;GSI1PK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;HASH&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;GSI1SK&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RANGE&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;Projection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;ProjectionType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ALL&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y los permisos IAM mínimos en el &lt;code&gt;serverless.yml&lt;/code&gt; principal &lt;sup id="fnref22"&gt;22&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;iam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;statements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
          &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CacheTable.Arn&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;UsersTable.Arn&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${Resource}/index/*'&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="nv"&gt;UsersTable.Arn&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:Query&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:GetItem&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:PutItem&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:UpdateItem&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:DeleteItem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Si usan &lt;code&gt;Cache::many()&lt;/code&gt; o &lt;code&gt;Cache::getMultiple()&lt;/code&gt;, agreguen &lt;code&gt;dynamodb:BatchGetItem&lt;/code&gt;: Laravel los resuelve en una sola llamada batch, y sin ese permiso van a recibir un &lt;code&gt;AccessDeniedException&lt;/code&gt; que no menciona la palabra &lt;em&gt;"batch"&lt;/em&gt; por ningún lado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sugerencia&lt;/strong&gt;: Nunca debemos utilizar &lt;code&gt;Action: dynamodb:*&lt;/code&gt; o &lt;code&gt;Resource: *&lt;/code&gt;. Privilegio mínimo o nada &lt;sup id="fnref23"&gt;23&lt;/sup&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  f) Costos: la última factura fija se va
&lt;/h3&gt;

&lt;p&gt;Actualicemos la cuenta:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La deuda del primer capítulo&lt;/strong&gt;: Ahí quedó un RDS MySQL &lt;code&gt;db.t4g.small&lt;/code&gt; que genera &lt;strong&gt;~$26/mes&lt;/strong&gt; (≈ $0.032/h × 730 h + almacenamiento &lt;sup id="fnref24"&gt;24&lt;/sup&gt;) por una base de datos que cobra solo por existir. Hoy la retiramos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Con DynamoDB&lt;/strong&gt;: Podemos utilizar el modo &lt;code&gt;BillingMode: PAY_PER_REQUEST&lt;/code&gt; y así solo pagar por request procesado y GB almacenado. Con tráfico modesto, la factura del mes se mide en centavos: la base de datos que cobraba por respirar dejó de existir, y ya ningún componente &lt;em&gt;grande&lt;/em&gt; cobra tarifa plana por recursos que nadie usa.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. ¿Qué Sigue?
&lt;/h2&gt;

&lt;p&gt;Hoy resolvimos dónde persiste todo: archivos en S3 (con uploads que ni tocan Lambda), caché y sesiones en DynamoDB con expiración automática, y una base de datos NoSQL que habla Eloquent.&lt;/p&gt;

&lt;p&gt;Pero el mapa todavía tiene zonas inexploradas. En el próximo artículo: &lt;strong&gt;las colas&lt;/strong&gt;. Veremos cómo SQS + un worker Lambda reemplazan al &lt;code&gt;queue:work&lt;/code&gt; que corría eternamente en un supervisor, y por qué eso es mejor que rezarle a &lt;code&gt;supervisord&lt;/code&gt; a las 3 AM.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Una pregunta para ustedes&lt;/strong&gt;: ¿ya usaron DynamoDB con Laravel, o siguen en el equipo MySQL/PostgreSQL? Cuéntenme en los comentarios qué los frena (o qué los enamoró) del mundo NoSQL.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/environment/storage" rel="noopener noreferrer"&gt;https://bref.sh/docs/environment/storage&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/storage.md" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/storage.md&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/filesystem#s3-driver-configuration" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/filesystem#s3-driver-configuration&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/file-storage" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/file-storage&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/filesystem#temporary-upload-urls" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/filesystem#temporary-upload-urls&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/file-storage#uploading-files" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/file-storage#uploading-files&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/cache#dynamodb" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/cache#dynamodb&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/environment/storage#deploying-dynamodb-tables" rel="noopener noreferrer"&gt;https://bref.sh/docs/environment/storage#deploying-dynamodb-tables&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn12"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/framework/docs/13.x/session#configuration" rel="noopener noreferrer"&gt;https://laravel.com/framework/docs/13.x/session#configuration&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn13"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn14"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn15"&gt;
&lt;p&gt;&lt;a href="https://github.com/kitar/laravel-dynamodb#extending-the-base-model" rel="noopener noreferrer"&gt;https://github.com/kitar/laravel-dynamodb#extending-the-base-model&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn16"&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/database/understanding-amazon-dynamodb-latency/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/database/understanding-amazon-dynamodb-latency/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn17"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/capacity.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/capacity.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn18"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn19"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-use-s3-too.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-use-s3-too.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn20"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-query-scan.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-query-scan.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn21"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-dynamodb-table.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-dynamodb-table.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn22"&gt;
&lt;p&gt;&lt;a href="https://www.serverless.com/framework/docs/providers/aws/guide/iam" rel="noopener noreferrer"&gt;https://www.serverless.com/framework/docs/providers/aws/guide/iam&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn23"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn24"&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/rds/mysql/pricing/" rel="noopener noreferrer"&gt;https://aws.amazon.com/rds/mysql/pricing/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>serverless</category>
      <category>laravel</category>
      <category>dynamodb</category>
      <category>s3</category>
    </item>
    <item>
      <title>Cómo Mi Configuración de Docker Me Salvó de un Ataque de Supply Chain (Y Por Qué la Tuya Debería Hacerlo También)</title>
      <dc:creator>Hermann D. Schimpf</dc:creator>
      <pubDate>Mon, 25 May 2026 23:50:55 +0000</pubDate>
      <link>https://dev.to/hschimpf/como-mi-configuracion-de-docker-me-salvo-de-un-ataque-de-supply-chain-y-por-que-la-tuya-deberia-51n5</link>
      <guid>https://dev.to/hschimpf/como-mi-configuracion-de-docker-me-salvo-de-un-ataque-de-supply-chain-y-por-que-la-tuya-deberia-51n5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxslko8b8u7fzi9medzbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxslko8b8u7fzi9medzbi.png" alt="Docker security shield" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;English version &lt;a href="https://dev.to/hschimpf/how-my-docker-setup-saved-me-from-a-supply-chain-attack-and-why-yours-should-too-59lk"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;¡Por fin es viernes! Sales del trabajo y vas a casa a trabajar en tu proyecto personal &lt;em&gt;(sí, ese soy yo)&lt;/em&gt;. Abres tu computadora y empiezas a trabajar ejecutando &lt;code&gt;composer update&lt;/code&gt; porque quieres mantener tus dependencias actualizadas.&lt;/p&gt;

&lt;p&gt;A la mañana siguiente, te despiertas con la noticia de que un paquete popular de Laravel que usas ha sido comprometido. Los atacantes inyectaron un robo de credenciales en 233 versiones, listo para exfiltrar tus claves SSH, credenciales de la nube, datos del navegador, billeteras de criptomonedas—toda la ensalada digital &lt;sup id="fnref1"&gt;1&lt;/sup&gt;. El pánico se instala. Revisas tu &lt;code&gt;composer.lock&lt;/code&gt;. Tu corazón se detiene un segundo. Tienes la versión &lt;code&gt;15.29.3&lt;/code&gt; de &lt;code&gt;laravel-lang/lang&lt;/code&gt;, una de las versiones comprometidas &lt;sup id="fnref2"&gt;2&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;¿Te ha pasado? A mí me pasó el 22 de mayo de 2026.&lt;/p&gt;

&lt;p&gt;Pero aquí está el giro inesperado: cuando profundicé más, me di cuenta de que mi configuración de PHP en Docker acababa de convertirse en mi guardaespaldas inesperado. Déjame contarte cómo ejecutar PHP a través de contenedores Docker añadió una capa de seguridad que nunca planeé, y por qué tú también deberías considerarlo.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. El Ataque: Cuando &lt;code&gt;composer update&lt;/code&gt; Se Vuelve Rebelde
&lt;/h2&gt;

&lt;p&gt;El 22 de mayo de 2026, los atacantes comprometieron la organización Laravel-Lang en GitHub. No solo publicaron una versión maliciosa—reescribieron &lt;strong&gt;cada&lt;/strong&gt; etiqueta git en cuatro paquetes populares para apuntar a sus propios commits. ¿El payload? Un robo de credenciales disfrazado como un archivo de ayuda inofensivo que se ejecuta automáticamente a través del autoloader de Composer.&lt;/p&gt;

&lt;p&gt;Lo que roba es una pesadilla:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credenciales de la nube &lt;em&gt;(AWS, GCP, Azure)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Claves privadas SSH&lt;/li&gt;
&lt;li&gt;Contraseñas y cookies del navegador&lt;/li&gt;
&lt;li&gt;Bóvedas de gestores de contraseñas&lt;/li&gt;
&lt;li&gt;Billeteras de criptomonedas&lt;/li&gt;
&lt;li&gt;Archivos &lt;code&gt;.env&lt;/code&gt; con secretos&lt;/li&gt;
&lt;li&gt;Y básicamente cualquier otra cosa que parezca valiosa&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El ataque golpeó durante una ventana de 15 minutos y comprometió 233 versiones en cuatro paquetes. Si ejecutaste&lt;code&gt;composer update&lt;/code&gt; entre las 22:32 UTC y la medianoche del 22 de mayo, podrías haber invitado a un ladrón digital a tu máquina.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Mi Configuración: PHP en una Caja
&lt;/h2&gt;

&lt;p&gt;Así es como ejecuto PHP y Composer en mi máquina local:&lt;/p&gt;

&lt;p&gt;El script &lt;code&gt;php-docker&lt;/code&gt; inicia un contenedor Docker y ejecuta PHP dentro de él:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Obtiene el &lt;strong&gt;directorio de trabajo actual&lt;/strong&gt; desde donde se está ejecutando el comando &lt;em&gt;(ej: ~/Projects/my-awesome-project)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Monta el &lt;strong&gt;directorio de trabajo actual&lt;/strong&gt; como un volumen en el contenedor y cambia a él&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NO monta ningún otro directorio&lt;/strong&gt;, como &lt;code&gt;/tmp&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt;, &lt;code&gt;/root&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;/usr/local/bin/php-docker&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# obtener la versión especificada y eliminarla de los argumentos&lt;/span&gt;
&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nb"&gt;shift&lt;/span&gt;

&lt;span class="c"&gt;# obtener el directorio de trabajo actual desde donde se ejecuta el comando&lt;/span&gt;
&lt;span class="nv"&gt;ORIGIN_PWD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# moverse al directorio del proyecto php dockerizado&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"/opt/docker/php-fpm/"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;

&lt;span class="c"&gt;# ejecutar el contenedor de la versión php especificada&lt;/span&gt;
&lt;span class="nb"&gt;command &lt;/span&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt; &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="nt"&gt;--quiet-build&lt;/span&gt; &lt;span class="nt"&gt;--quiet-pull&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="c"&gt;# montar el directorio de ejecución original y cd a él&lt;/span&gt;
  &lt;span class="nt"&gt;--volume&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ORIGIN_PWD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ORIGIN_PWD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workdir&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ORIGIN_PWD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  php-fpm-&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PHP_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; php &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# volver al directorio original&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ORIGIN_PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En lugar del binario &lt;code&gt;php&lt;/code&gt;, tengo un pequeño script que ejecuta el script &lt;code&gt;php-docker&lt;/code&gt; con la versión de PHP especificada &lt;em&gt;(obtenida del nombre del archivo)&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;/usr/local/bin/php&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# obtener la versión del nombre del archivo&lt;/span&gt;
&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="p"&gt;##*-&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;# validar el formato de la versión&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 3 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# fallback a la última versión estable&lt;/span&gt;
  &lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.5
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# ejecutar php-docker especificando la versión y pasando los argumentos&lt;/span&gt;
&lt;span class="nb"&gt;command &lt;/span&gt;php-docker &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PHP_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y múltiples enlaces al mismo script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /usr/local/bin/php&lt;span class="k"&gt;*&lt;/span&gt;
/usr/local/bin/php
/usr/local/bin/php-5.6 -&amp;gt; php
/usr/local/bin/php-7.4 -&amp;gt; php
/usr/local/bin/php-8.0 -&amp;gt; php
...
/usr/local/bin/php-8.5 -&amp;gt; php
/usr/local/bin/php-docker -&amp;gt; /opt/docker/php-fpm/bin/php-docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;De esa manera, cualquier ejecución cli &lt;code&gt;php-*&lt;/code&gt; se ejecutará dentro del contenedor Docker de la versión de PHP especificada.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;php &lt;span class="nt"&gt;-v&lt;/span&gt;
PHP 8.5.5 &lt;span class="o"&gt;(&lt;/span&gt;cli&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;built: Apr 18 2026 10:39:21&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;ZTS&lt;span class="o"&gt;)&lt;/span&gt;
Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; The PHP Group
Built by HDS Solutions
Zend Engine v4.5.5, Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; Zend Technologies
    with Xdebug v3.5.1, Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; 2002-2026, by Derick Rethans
    with Zend OPcache v8.5.5, Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt;, by Zend Technologies

&lt;span class="nv"&gt;$ &lt;/span&gt;php-7.4 &lt;span class="nt"&gt;-v&lt;/span&gt;
PHP 7.4.33 &lt;span class="o"&gt;(&lt;/span&gt;cli&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;built: Apr 18 2026 10:47:16&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; ZTS &lt;span class="o"&gt;)&lt;/span&gt;
Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; The PHP Group
Zend Engine v3.4.0, Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; Zend Technologies
    with Xdebug v3.1.6, Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; 2002-2022, by Derick Rethans
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mi comando &lt;code&gt;composer&lt;/code&gt; sigue el mismo patrón, todo se ejecuta dentro de Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;/usr/local/bin/composer&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="p"&gt;##*-&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 3 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.5
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# ejecutar php-docker especificando la versión y pasando los argumentos al cli de composer&lt;/span&gt;
&lt;span class="nb"&gt;command &lt;/span&gt;php-docker &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PHP_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /usr/bin/composer &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Por qué configuré esto? Originalmente para soportar múltiples versiones de PHP, tener builds ZTS, y migrar fácilmente cuando compras una nueva laptop. Pero como lo descubriría más adelante, esta elección arquitectónica tuvo un beneficio de seguridad que no fue intencional.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Qué Me Protegió Docker
&lt;/h2&gt;

&lt;p&gt;Cuando el payload de Laravel-Lang se ejecuta, busca credenciales en ubicaciones específicas. Así es como mi configuración de Docker lo bloqueó:&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Secretos del Directorio Home: Estilo Fort Knox
&lt;/h3&gt;

&lt;p&gt;El robo apunta a:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.ssh/&lt;/code&gt; - Claves privadas SSH&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.aws/credentials&lt;/code&gt; - Credenciales de AWS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.config/gcloud/&lt;/code&gt; - Credenciales de GCP&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.azure/&lt;/code&gt; - Credenciales de Azure&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.git-credentials&lt;/code&gt;, &lt;code&gt;.netrc&lt;/code&gt;, &lt;code&gt;.npmrc&lt;/code&gt;, &lt;code&gt;.pypirc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Datos del navegador &lt;em&gt;(perfiles Chrome, Firefox)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Bóvedas de gestores de contraseñas &lt;em&gt;(KeePass, 1Password)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Billeteras de criptomonedas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Resultado&lt;/strong&gt;: Ninguno de estos existe en el contenedor. El directorio home del contenedor está &lt;em&gt;"vacío"&lt;/em&gt;. El atacante no encuentra nada.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;/opt/docker/php-fpm/docker-compose.yml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;php-fpm-8.5&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-fpm-zts-8.5&lt;/span&gt;
        &lt;span class="na"&gt;domainname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-fpm-8.5.vanilla&lt;/span&gt;
        &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${USER:-http}&lt;/span&gt;
                &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;33&lt;/span&gt;
                &lt;span class="na"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8.5&lt;/span&gt;
            &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./docker&lt;/span&gt;
            &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;common/Dockerfile&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./docker/common/php/00-php.ini:/usr/local/etc/php/conf.d/00-php.ini:ro&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./docker/common/php-fpm/www.conf:/usr/local/etc/php-fpm.d/www-docker.conf:ro&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./docker/8.5/home/.composer/cache:/home/http/.composer/cache:rw&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./docker/8.5/home/.composer/vendor:/home/http/.composer/vendor:rw&lt;/span&gt;

    &lt;span class="na"&gt;php-fpm-8.4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-fpm-zts-8.4&lt;/span&gt;
        &lt;span class="na"&gt;domainname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-fpm-8.4.vanilla&lt;/span&gt;
        &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${USER:-http}&lt;/span&gt;
                &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;33&lt;/span&gt;
                &lt;span class="na"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8.4&lt;/span&gt;
            &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./docker&lt;/span&gt;
            &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;common/Dockerfile&lt;/span&gt;
        &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;php-fpm-8.3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-fpm-zts-8.3&lt;/span&gt;
        &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  b) Secretos a Nivel de Sistema: Aislados
&lt;/h3&gt;

&lt;p&gt;El payload también escanea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/etc/kubernetes/admin.conf&lt;/code&gt; - Configuraciones de Kubernetes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/var/run/secrets/&lt;/code&gt; - Secretos de CI/CD&lt;/li&gt;
&lt;li&gt;Gestores de credenciales del sistema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Resultado&lt;/strong&gt;: El contenedor no tiene acceso a los directorios del sistema host. Estos están aislados.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Datos del Navegador y Contraseñas: Inexistentes
&lt;/h3&gt;

&lt;p&gt;El robo busca bases de datos del navegador, bóvedas de gestores de contraseñas, y billeteras de criptomonedas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resultado&lt;/strong&gt;: No hay navegador, no hay gestor de contraseñas, no hay billeteras en el contenedor. Solo PHP y tu código.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Qué No Me Protegió Docker
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;O por qué no estoy lanzando un desfile de victoria todavía&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mi configuración de Docker añadió una capa de seguridad significativa, pero no es perfecta. Así es lo que sigue siendo vulnerable:&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Secretos del Directorio del Proyecto: La Puerta Abierta
&lt;/h3&gt;

&lt;p&gt;El contenedor monta tu directorio del proyecto. Si tu archivo &lt;code&gt;.env&lt;/code&gt; contiene secretos, el robo puede leerlo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;.env&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS_ACCESS_KEY_ID=1U0o...pzdZ
AWS_SECRET_ACCESS_KEY=KKaY...aKEd
REDIS_PASSWORD=XSFK...y4XV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mitigación&lt;/strong&gt;: Usa servicios solo locales &lt;em&gt;(explicaré en un momento)&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) Claves de API Externas: Todavía en Riesgo
&lt;/h3&gt;

&lt;p&gt;Tu archivo &lt;code&gt;.env&lt;/code&gt; podría contener claves de API externas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claves de API de Google Maps&lt;/li&gt;
&lt;li&gt;Claves de reCAPTCHA&lt;/li&gt;
&lt;li&gt;Tokens de servicios de terceros&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estas se exfiltran si el payload se ejecuta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigación&lt;/strong&gt;: Rota estas claves si ejecutaste composer durante la ventana del ataque.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Mi Estrategia de Defensa: Todo Local
&lt;/h2&gt;

&lt;p&gt;Para minimizar lo que el robo podría robar, uso servicios solo locales para desarrollo &lt;em&gt;(todos ellos también dockerizados)&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# configuración .env&lt;/span&gt;
&lt;span class="nv"&gt;DB_CONNECTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dynamodb
&lt;span class="nv"&gt;DYNAMODB_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000  &lt;span class="c"&gt;# DynamoDB Local&lt;/span&gt;

&lt;span class="nv"&gt;BROADCAST_CONNECTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pusher
&lt;span class="nv"&gt;PUSHER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;soketi.vanilla  &lt;span class="c"&gt;# Instancia Soketi Local&lt;/span&gt;

&lt;span class="nv"&gt;REDIS_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis-8.6.vanilla  &lt;span class="c"&gt;# Redis Local&lt;/span&gt;
&lt;span class="nv"&gt;REDIS_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;a3BM...6nys

&lt;span class="nv"&gt;AWS_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://minio.vanilla  &lt;span class="c"&gt;# MinIO Local&lt;/span&gt;
&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0onp...ZHjn
&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fzfK...Aear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Por qué funciona&lt;/strong&gt;: Incluso si estas credenciales se roban, son inútiles para un atacante. Solo funcionan contra&lt;code&gt;localhost&lt;/code&gt; o mis dominios de red local (&lt;code&gt;*.vanilla&lt;/code&gt;). El atacante no puede alcanzar mi infraestructura de desarrollo local.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qué mantengo externo&lt;/strong&gt;: Clave de API de Google Maps, claves de reCAPTCHA. Estos son los puntos débiles, pero son de bajo riesgo comparado con las credenciales de proveedores de la nube.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. El Veredicto: Docker como Capa de Seguridad
&lt;/h2&gt;

&lt;p&gt;Mi configuración de Docker no fue diseñada para seguridad, fue diseñada para consistencia entre versiones de PHP y cambiarme fácilmente de laptop. Pero esa elección arquitectónica proporcionó protección significativa contra este ataque, completamente por accidente.&lt;/p&gt;

&lt;p&gt;Cuando descubrí por primera vez que mi versión estaba comprometida, sentí ese sudor frío. Verifiqué mis claves SSH, mis credenciales de AWS, mis datos del navegador, todo estaba ahí. Mi ritmo cardíaco volvió lentamente a la normalidad mientras me daba cuenta: el payload había corrido, pero no tenía dónde buscar.&lt;/p&gt;

&lt;p&gt;Si hubiera estado ejecutando PHP directamente en mi máquina host, la historia habría sido muy diferente. El robo habría encontrado mis claves SSH, mis credenciales de la nube, mis contraseñas del navegador, quizás incluso mi billetera de criptomonedas. Eso no es solo una brecha de seguridad, es robo de identidad esperando a suceder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo que Docker me dio&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aislamiento de secretos del directorio home—mis claves SSH, credenciales de AWS, y datos del navegador viven fuera del contenedor&lt;/li&gt;
&lt;li&gt;Sin acceso a credenciales a nivel de sistema—configuraciones de Kubernetes, secretos de CI/CD, y gestores de credenciales del sistema son inalcanzables&lt;/li&gt;
&lt;li&gt;Sin datos de navegador o gestor de contraseñas—no hay Chrome, Firefox, o KeePass dentro del contenedor&lt;/li&gt;
&lt;li&gt;Ejecución en contenedor que limita lo que el payload puede ver—el atacante obtiene un ambiente estéril con nada valioso&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lo que Docker no me dio&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protección contra secretos de archivo &lt;code&gt;.env&lt;/code&gt; en el directorio del proyecto—si tuviera credenciales de producción ahí, se habrían ido&lt;/li&gt;
&lt;li&gt;Protección contra fugas de variables de entorno—los secretos pasados al contenedor siguen siendo accesibles&lt;/li&gt;
&lt;li&gt;Protección contra robo de claves de API externas—las claves de Google Maps y reCAPTCHA todavía se habrían exfiltrado&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;En resumen&lt;/strong&gt;: Docker redujo mi superficie de ataque significativamente. En lugar de perder claves SSH, credenciales de la nube, datos del navegador, y billeteras de criptomonedas, el peor escenario sería perder credenciales de desarrollo local y algunas claves de API. Esa es la diferencia entre &lt;em&gt;"mi fin de semana está arruinado"&lt;/em&gt; y &lt;em&gt;"mi vida está arruinada"&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Cómo Dormir Mejor por la Noche: Endurece Tu Configuración
&lt;/h2&gt;

&lt;p&gt;Si no estás ejecutando PHP/Composer en Docker, considéralo. Los beneficios de consistencia por sí solos valen la pena, y el beneficio de seguridad es un bono agradable. Si ya estás usando Docker, así es cómo hacerlo aún más seguro:&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Nunca Montes el Directorio Home
&lt;/h3&gt;

&lt;p&gt;Obvio, pero crítico. Tu directorio home contiene las joyas de la corona: claves SSH, credenciales de la nube, datos del navegador, gestores de contraseñas. Si lo montas, estás invitando al atacante a tu casa.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) Usa Servicios Solo Locales para Desarrollo
&lt;/h3&gt;

&lt;p&gt;No uses credenciales de la nube de producción en desarrollo. Ejecuta instancias locales de todo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bases de datos &lt;em&gt;(MySQL, PostgreSQL, DynamoDB Local)&lt;/em&gt;—Docker Compose hace esto trivial&lt;/li&gt;
&lt;li&gt;Redis—inicia un contenedor, nunca toques una instancia real de Redis&lt;/li&gt;
&lt;li&gt;Almacenamiento de objetos &lt;em&gt;(MinIO compatible con S3)&lt;/em&gt;—simula el comportamiento de AWS S3 localmente&lt;/li&gt;
&lt;li&gt;Colas de mensajes—RabbitMQ local o equivalente&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El objetivo: si las credenciales se filtran, son inútiles porque solo funcionan contra localhost.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Separa Archivos &lt;code&gt;.env&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Mantén &lt;code&gt;.env&lt;/code&gt; fuera del control de versiones &lt;em&gt;(obviamente)&lt;/em&gt;. Usa diferentes archivos &lt;code&gt;.env&lt;/code&gt; para local, staging, y producción. Nunca, nunca lleves secretos en tus commits. Si lo haces por accidente, rótalos inmediatamente.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;El historial de Git es para siempre.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  d) No Pases Secretos Variables de Entorno
&lt;/h3&gt;

&lt;p&gt;Para desarrollo, usa servicios locales. Para producción, usa sistemas de gestión de secretos &lt;em&gt;(ej: AWS Secrets Manager)&lt;/em&gt;. Pasar secretos vía variables de entorno es conveniente, pero también es filtrable—listas de procesos, herramientas de depuración, y frameworks de logging pueden exponerlos.&lt;/p&gt;

&lt;h3&gt;
  
  
  e) Monitorea IOCs
&lt;/h3&gt;

&lt;p&gt;Si quieres saber si fuiste afectado, busca:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tráfico de red a &lt;code&gt;flipboxstudio[.]info&lt;/code&gt;—si tienes logging de tráfico &lt;em&gt;(ej: uso Pi-Hole &lt;sup id="fnref3"&gt;3&lt;/sup&gt;)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Archivos en &lt;code&gt;/tmp/.laravel_locale/&lt;/code&gt;—el dropper escribe marcadores de infección aquí&lt;/li&gt;
&lt;li&gt;Procesos PHP sospechosos—procesos PHP huérfanos sin padre son una bandera roja&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  f) Bloquea Tus Dependencias
&lt;/h3&gt;

&lt;p&gt;Usa &lt;code&gt;composer.lock&lt;/code&gt; y verifica los SHAs de commit. El ataque reescribió etiquetas, los SHAs de commit bloqueados de antes de la ventana del ataque son seguros. Cuando descargas dependencias, verifica que el SHA de commit coincida con lo que esperas.&lt;/p&gt;

&lt;p&gt;También, una buena práctica es ejecutar &lt;code&gt;composer outdated&lt;/code&gt;, revisar manualmente el CHANGELOG de cada paquete, y después de revisar los cambios, actualizar el paquete a la última versión. Si los cambios no añaden ninguna característica necesaria, o corrigen vulnerabilidades de seguridad, puedes ignorar el update de forma segura.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. ¿Qué Sigue?
&lt;/h2&gt;

&lt;p&gt;Docker no es una bala de plata, pero es una capa poderosa en tu estrategia de seguridad. El ataque de Laravel-Lang nos mostró que los ataques Supply Chain son reales, sofisticados, y cada vez más comunes.&lt;/p&gt;

&lt;p&gt;En mi caso, una decisión tomada por consistencia de desarrollo se convirtió en una característica de seguridad. Configuré Docker para gestionar múltiples versiones de PHP y builds ZTS, pero accidentalmente construí un búnker de seguridad.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A veces la mejor seguridad es la que no sabías que tenías.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pero aquí está el tema: Docker es solo una pieza del rompecabezas. También necesitas bloqueo de dependencias, gestión de secretos, monitoreo de red, y una dosis saludable de paranoia. La seguridad no es un destino, es un viaje, y los ataques Supply Chain son solo un peligro en el camino.&lt;/p&gt;

&lt;p&gt;La próxima vez que configures un ambiente de desarrollo, piensa en las implicaciones de seguridad de tus elecciones. Docker, servicios locales, redes aisladas, estos no son solo conveniencias. Son armadura.&lt;/p&gt;

&lt;p&gt;¿Y si todavía estás ejecutando PHP directamente en tu máquina host? Bueno, quizás sea hora de ponerlo en una caja. Tu futuro yo podría agradecértelo.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Pregunta para ti&lt;/strong&gt;: ¿Ejecutas tus herramientas de desarrollo en Docker o directamente en tu host? ¿Alguna vez has considerado las implicaciones de seguridad de esa elección? Comenta abajo, tengo curiosidad sobre cómo otros manejan esto.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://www.aikido.dev/blog/supply-chain-attack-targets-laravel-lang-packages-with-credential-stealer" rel="noopener noreferrer"&gt;https://www.aikido.dev/blog/supply-chain-attack-targets-laravel-lang-packages-with-credential-stealer&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://x.com/AikidoSecurity/status/2057959626692526098" rel="noopener noreferrer"&gt;https://x.com/AikidoSecurity/status/2057959626692526098&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://pi-hole.net/" rel="noopener noreferrer"&gt;https://pi-hole.net/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>How My Docker Setup Saved Me From a Supply Chain Attack (And Why Yours Should Too)</title>
      <dc:creator>Hermann D. Schimpf</dc:creator>
      <pubDate>Mon, 25 May 2026 23:49:56 +0000</pubDate>
      <link>https://dev.to/hschimpf/how-my-docker-setup-saved-me-from-a-supply-chain-attack-and-why-yours-should-too-59lk</link>
      <guid>https://dev.to/hschimpf/how-my-docker-setup-saved-me-from-a-supply-chain-attack-and-why-yours-should-too-59lk</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxslko8b8u7fzi9medzbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxslko8b8u7fzi9medzbi.png" alt="Docker security shield" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Versión en español &lt;a href="https://dev.to/hschimpf/como-mi-configuracion-de-docker-me-salvo-de-un-ataque-de-supply-chain-y-por-que-la-tuya-deberia-51n5"&gt;aquí&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;It's finally Friday! You leave work and go home to work on your side project &lt;em&gt;(yep, that's me)&lt;/em&gt;. You open your computer and start working on it by running &lt;code&gt;composer update&lt;/code&gt; because you want to keep your dependencies up to date.&lt;/p&gt;

&lt;p&gt;Next morning, you wake up to the news that a popular Laravel package you use has been compromised. Attackers injected a credential stealer into 233 versions, ready to exfiltrate your SSH keys, cloud credentials, browser data, crypto wallets—the whole digital enchilada &lt;sup id="fnref1"&gt;1&lt;/sup&gt;. Panic sets in. You check your &lt;code&gt;composer.lock&lt;/code&gt;. Your heart skips a beat. You have version &lt;code&gt;15.29.3&lt;/code&gt; of &lt;code&gt;laravel-lang/lang&lt;/code&gt;, one of the compromised versions &lt;sup id="fnref2"&gt;2&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;Been there? I was, on May 22, 2026.&lt;/p&gt;

&lt;p&gt;But here's the plot twist: When I dug deeper, I realized my Dockerized PHP setup had just become my unexpected bodyguard. Let me tell you how running PHP through Docker containers added a security layer I never planned for, and why you should consider it too.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Attack: When &lt;code&gt;composer update&lt;/code&gt; Goes Rogue
&lt;/h2&gt;

&lt;p&gt;On May 22, 2026, attackers compromised the Laravel-Lang organization on GitHub. They didn't just publish a malicious version—they rewrote &lt;strong&gt;every&lt;/strong&gt; git tag across four popular packages to point to their own commits. The payload? A credential stealer disguised as a harmless helper file that runs automatically via Composer's autoloader.&lt;/p&gt;

&lt;p&gt;What it stole is nightmare fuel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud credentials &lt;em&gt;(AWS, GCP, Azure)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;SSH private keys&lt;/li&gt;
&lt;li&gt;Browser passwords and cookies&lt;/li&gt;
&lt;li&gt;Password manager vaults&lt;/li&gt;
&lt;li&gt;Crypto wallets&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; files with secrets&lt;/li&gt;
&lt;li&gt;And basically anything else that looks valuable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The attack hit during a 15-minute window and compromised 233 versions across four packages. If you ran &lt;code&gt;composer update&lt;/code&gt; between 22:32 UTC and midnight on May 22, you might have invited a digital burglar into your machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. My Setup: PHP in a Box
&lt;/h2&gt;

&lt;p&gt;Here's how I run PHP and Composer on my local machine:&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;php-docker&lt;/code&gt; script spins up a Docker container and runs PHP inside it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gets the &lt;strong&gt;current working directory&lt;/strong&gt; from where the command is being executed &lt;em&gt;(ex: ~/Projects/my-awesome-project)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Mounts the &lt;strong&gt;current working directory&lt;/strong&gt; as a volume in the container and changes to it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does NOT mount any other directory&lt;/strong&gt;, like &lt;code&gt;/tmp&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt;, &lt;code&gt;/root&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;/usr/local/bin/php-docker&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# get specified version and remove it from arguments&lt;/span&gt;
&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nb"&gt;shift&lt;/span&gt;

&lt;span class="c"&gt;# get the current working directory from where the command is being executed&lt;/span&gt;
&lt;span class="nv"&gt;ORIGIN_PWD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# move into the dockerized php project directory&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"/opt/docker/php-fpm/"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;

&lt;span class="c"&gt;# execute the specified php version container&lt;/span&gt;
&lt;span class="nb"&gt;command &lt;/span&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt; &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="nt"&gt;--quiet-build&lt;/span&gt; &lt;span class="nt"&gt;--quiet-pull&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="c"&gt;# mount the origin execution directory and cd to it&lt;/span&gt;
  &lt;span class="nt"&gt;--volume&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ORIGIN_PWD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ORIGIN_PWD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workdir&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ORIGIN_PWD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  php-fpm-&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PHP_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; php &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# go back to original directory&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ORIGIN_PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of the &lt;code&gt;php&lt;/code&gt; binary, I have a small script that runs the &lt;code&gt;php-docker&lt;/code&gt; script with the specified PHP version &lt;em&gt;(obtained from the filename)&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;/usr/local/bin/php&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# get the version from the filename&lt;/span&gt;
&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="p"&gt;##*-&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;# validate version format&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 3 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# fallback to latest stable version&lt;/span&gt;
  &lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.5
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# execute php-docker specifying version and passing through arguments&lt;/span&gt;
&lt;span class="nb"&gt;command &lt;/span&gt;php-docker &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PHP_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And multiple links to the same script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /usr/local/bin/php&lt;span class="k"&gt;*&lt;/span&gt;
/usr/local/bin/php
/usr/local/bin/php-5.6 -&amp;gt; php
/usr/local/bin/php-7.4 -&amp;gt; php
/usr/local/bin/php-8.0 -&amp;gt; php
...
/usr/local/bin/php-8.5 -&amp;gt; php
/usr/local/bin/php-docker -&amp;gt; /opt/docker/php-fpm/bin/php-docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That way, any &lt;code&gt;php-*&lt;/code&gt; cli execution will run inside the Docker container of the specified PHP version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;php &lt;span class="nt"&gt;-v&lt;/span&gt;
PHP 8.5.5 &lt;span class="o"&gt;(&lt;/span&gt;cli&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;built: Apr 18 2026 10:39:21&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;ZTS&lt;span class="o"&gt;)&lt;/span&gt;
Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; The PHP Group
Built by HDS Solutions
Zend Engine v4.5.5, Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; Zend Technologies
    with Xdebug v3.5.1, Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; 2002-2026, by Derick Rethans
    with Zend OPcache v8.5.5, Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt;, by Zend Technologies

&lt;span class="nv"&gt;$ &lt;/span&gt;php-7.4 &lt;span class="nt"&gt;-v&lt;/span&gt;
PHP 7.4.33 &lt;span class="o"&gt;(&lt;/span&gt;cli&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;built: Apr 18 2026 10:47:16&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; ZTS &lt;span class="o"&gt;)&lt;/span&gt;
Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; The PHP Group
Zend Engine v3.4.0, Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; Zend Technologies
    with Xdebug v3.1.6, Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; 2002-2022, by Derick Rethans
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My &lt;code&gt;composer&lt;/code&gt; command follows the same pattern, everything runs inside Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;/usr/local/bin/composer&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="p"&gt;##*-&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 3 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.5
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# execute php-docker specifying version and passing through arguments to composer cli&lt;/span&gt;
&lt;span class="nb"&gt;command &lt;/span&gt;php-docker &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PHP_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /usr/bin/composer &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why did I set this up? Originally to support multiple PHP versions, have ZTS builds, and easily migrate when you buy a new laptop. But as I would discover, this architectural choice had an unintended security benefit.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. What Docker Protected Me From
&lt;/h2&gt;

&lt;p&gt;When the Laravel-Lang payload executes, it hunts for credentials in specific locations. Here's what my Docker setup blocked:&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Home Directory Secrets: Fort Knox Style
&lt;/h3&gt;

&lt;p&gt;The stealer targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.ssh/&lt;/code&gt; - SSH private keys&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.aws/credentials&lt;/code&gt; - AWS credentials&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.config/gcloud/&lt;/code&gt; - GCP credentials&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.azure/&lt;/code&gt; - Azure credentials&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.git-credentials&lt;/code&gt;, &lt;code&gt;.netrc&lt;/code&gt;, &lt;code&gt;.npmrc&lt;/code&gt;, &lt;code&gt;.pypirc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Browser data &lt;em&gt;(Chrome, Firefox profiles)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Password manager vaults &lt;em&gt;(KeePass, 1Password)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Crypto wallets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: None of these exist in the container. The container's home directory is &lt;em&gt;"empty"&lt;/em&gt;. The attacker finds nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;/opt/docker/php-fpm/docker-compose.yml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;php-fpm-8.5&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-fpm-zts-8.5&lt;/span&gt;
        &lt;span class="na"&gt;domainname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-fpm-8.5.vanilla&lt;/span&gt;
        &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${USER:-http}&lt;/span&gt;
                &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;33&lt;/span&gt;
                &lt;span class="na"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8.5&lt;/span&gt;
            &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./docker&lt;/span&gt;
            &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;common/Dockerfile&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./docker/common/php/00-php.ini:/usr/local/etc/php/conf.d/00-php.ini:ro&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./docker/common/php-fpm/www.conf:/usr/local/etc/php-fpm.d/www-docker.conf:ro&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./docker/8.5/home/.composer/cache:/home/http/.composer/cache:rw&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./docker/8.5/home/.composer/vendor:/home/http/.composer/vendor:rw&lt;/span&gt;

    &lt;span class="na"&gt;php-fpm-8.4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-fpm-zts-8.4&lt;/span&gt;
        &lt;span class="na"&gt;domainname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-fpm-8.4.vanilla&lt;/span&gt;
        &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${USER:-http}&lt;/span&gt;
                &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;33&lt;/span&gt;
                &lt;span class="na"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8.4&lt;/span&gt;
            &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./docker&lt;/span&gt;
            &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;common/Dockerfile&lt;/span&gt;
        &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;php-fpm-8.3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-fpm-zts-8.3&lt;/span&gt;
        &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  b) System-Level Secrets: Isolated
&lt;/h3&gt;

&lt;p&gt;The payload also scans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/etc/kubernetes/admin.conf&lt;/code&gt; - Kubernetes configs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/var/run/secrets/&lt;/code&gt; - CI/CD secrets&lt;/li&gt;
&lt;li&gt;System credential managers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: The container doesn't have access to host system directories. These are isolated.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Browser and Password Data: Nonexistent
&lt;/h3&gt;

&lt;p&gt;The stealer looks for browser databases, password manager vaults, and crypto wallets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: No browser, no password manager, no wallets in the container. Just PHP and your code.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. What Docker Didn't Protect
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Or why I'm not throwing a victory parade just yet&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My Docker setup added a significant security layer, but it's not perfect. Here's what remains vulnerable:&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Project Directory Secrets: The Open Door
&lt;/h3&gt;

&lt;p&gt;The container mounts your project directory. If your &lt;code&gt;.env&lt;/code&gt; file contains secrets, the stealer can read it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;.env&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS_ACCESS_KEY_ID=1U0o...pzdZ
AWS_SECRET_ACCESS_KEY=KKaY...aKEd
REDIS_PASSWORD=XSFK...y4XV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;: Use local-only services &lt;em&gt;(I'll explain in a moment)&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) External API Keys: Still at Risk
&lt;/h3&gt;

&lt;p&gt;Your &lt;code&gt;.env&lt;/code&gt; might contain external API keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Maps API keys&lt;/li&gt;
&lt;li&gt;reCAPTCHA keys&lt;/li&gt;
&lt;li&gt;Third-party service tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These get exfiltrated if the payload runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;: Rotate these keys if you ran composer during the attack window.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. My Defense Strategy: Local-Everything
&lt;/h2&gt;

&lt;p&gt;To minimize what the stealer could steal, I use local-only services for development &lt;em&gt;(all of them also dockerized)&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env configuration&lt;/span&gt;
&lt;span class="nv"&gt;DB_CONNECTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dynamodb
&lt;span class="nv"&gt;DYNAMODB_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000  &lt;span class="c"&gt;# Local DynamoDB&lt;/span&gt;

&lt;span class="nv"&gt;BROADCAST_CONNECTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pusher
&lt;span class="nv"&gt;PUSHER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;soketi.vanilla  &lt;span class="c"&gt;# Local Soketi instance&lt;/span&gt;

&lt;span class="nv"&gt;REDIS_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis-8.6.vanilla  &lt;span class="c"&gt;# Local Redis&lt;/span&gt;
&lt;span class="nv"&gt;REDIS_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;a3BM...6nys

&lt;span class="nv"&gt;AWS_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://minio.vanilla  &lt;span class="c"&gt;# Local MinIO&lt;/span&gt;
&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0onp...ZHjn
&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fzfK...Aear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt;: Even if these credentials get stolen, they're useless to an attacker. They only work against &lt;code&gt;localhost&lt;/code&gt; or my local network domains (&lt;code&gt;*.vanilla&lt;/code&gt;). The attacker can't reach my local development infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I keep external&lt;/strong&gt;: Google Maps API key, reCAPTCHA keys. These are the weak points, but they're low-risk compared to cloud provider credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Verdict: Docker as Security Layer
&lt;/h2&gt;

&lt;p&gt;My Docker setup wasn't designed for security, it was designed for consistency across PHP versions and easy laptop migration. But that architectural choice provided significant protection against this supply chain attack, completely by accident.&lt;/p&gt;

&lt;p&gt;When I first discovered my version was compromised, I felt that familiar cold sweat. I checked my SSH keys, my AWS credentials, my browser data—everything was there. My heart rate slowly returned to normal as I realized: the payload had run, but it had nowhere to look.&lt;/p&gt;

&lt;p&gt;If I had been running PHP directly on my host machine, the story would have been very different. The stealer would have found my SSH keys, my cloud credentials, my browser passwords, maybe even my crypto wallet. That's not just a security breach—that's identity theft waiting to happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Docker gave me&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Isolation from home directory secrets—my SSH keys, AWS credentials, and browser data live outside the container&lt;/li&gt;
&lt;li&gt;No access to system-level credentials—Kubernetes configs, CI/CD secrets, and system credential managers are unreachable&lt;/li&gt;
&lt;li&gt;No browser or password manager data—there's no Chrome, Firefox, or KeePass inside the container&lt;/li&gt;
&lt;li&gt;Containerized execution that limits what the payload can see—the attacker gets a sterile environment with nothing valuable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What Docker didn't give me&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protection against &lt;code&gt;.env&lt;/code&gt; file secrets in project directory—if I had production credentials there, they'd be gone&lt;/li&gt;
&lt;li&gt;Protection against environment variable leaks—secrets passed to the container are still accessible&lt;/li&gt;
&lt;li&gt;Protection against external API key theft—Google Maps and reCAPTCHA keys would still be exfiltrated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The bottom line&lt;/strong&gt;: Docker reduced my attack surface significantly. Instead of losing SSH keys, cloud credentials, browser data, and crypto wallets, the worst-case scenario would be losing local development credentials and a few API keys. That's the difference between &lt;em&gt;"my weekend is ruined"&lt;/em&gt; and &lt;em&gt;"my life is ruined"&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. How to sleep better at night: Harden Your Setup
&lt;/h2&gt;

&lt;p&gt;If you're not running PHP/Composer in Docker, consider it. The consistency benefits alone are worth it, and the security upside is a nice bonus. If you are already using Docker, here's how to make it even more secure:&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Never Mount Home Directory
&lt;/h3&gt;

&lt;p&gt;Obvious, but critical. Your home directory contains the crown jewels: SSH keys, cloud credentials, browser data, password managers. If you mount it, you're inviting the attacker into your house.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) Use Local-Only Services for Development
&lt;/h3&gt;

&lt;p&gt;Don't use production cloud credentials in development. Run local instances of everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Databases &lt;em&gt;(MySQL, PostgreSQL, DynamoDB Local)&lt;/em&gt;—Docker Compose makes this trivial&lt;/li&gt;
&lt;li&gt;Redis—spin up a container, never touch a real Redis instance&lt;/li&gt;
&lt;li&gt;Object storage &lt;em&gt;(MinIO for S3-compatible)&lt;/em&gt;—mocks AWS S3 behavior locally&lt;/li&gt;
&lt;li&gt;Message queues—local RabbitMQ or equivalent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal: if credentials leak, they're useless because they only work against localhost.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Separate &lt;code&gt;.env&lt;/code&gt; Files
&lt;/h3&gt;

&lt;p&gt;Keep &lt;code&gt;.env&lt;/code&gt; out of version control &lt;em&gt;(obviously)&lt;/em&gt;. Use different &lt;code&gt;.env&lt;/code&gt; files for local, staging, and production. Never, ever commit your secrets. If you do by accident, rotate them immediately.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Git history is forever.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  d) Don't Pass Secrets via Environment Variables
&lt;/h3&gt;

&lt;p&gt;For development, use local services. For production, use secret management systems &lt;em&gt;(e.g., AWS Secrets Manager)&lt;/em&gt;. Passing secrets via environment variables is convenient, but it's also leaky-process lists, debugging tools, and logging frameworks can expose them.&lt;/p&gt;

&lt;h3&gt;
  
  
  e) Monitor for IOCs
&lt;/h3&gt;

&lt;p&gt;If you want to know if you were affected, look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network traffic to &lt;code&gt;flipboxstudio[.]info&lt;/code&gt;—if you have traffic logging &lt;em&gt;(e.g., I use Pi-Hole &lt;sup id="fnref3"&gt;3&lt;/sup&gt;)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Files in &lt;code&gt;/tmp/.laravel_locale/&lt;/code&gt;—the dropper writes infection markers here&lt;/li&gt;
&lt;li&gt;Suspicious PHP processes—orphaned PHP processes with no parent are a red flag&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  f) Lock Your Dependencies
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;composer.lock&lt;/code&gt; and verify commit SHAs. The attack rewrote tags, locked commit SHAs from before the attack window are safe. When you pull dependencies, check the commit SHA matches what you expect.&lt;/p&gt;

&lt;p&gt;Also, a good practice is to run &lt;code&gt;composer outdated&lt;/code&gt;, manually go through the CHANGELOG of each package, and after reviewing the changes, update the package to the latest version. If the changes don't add any needed features, or fix any security vulnerabilities, you can safely ignore the update.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. What's Next?
&lt;/h2&gt;

&lt;p&gt;Docker isn't a silver bullet, but it's a powerful layer in your security strategy. The Laravel-Lang attack showed us that supply chain attacks are real, sophisticated, and increasingly common.&lt;/p&gt;

&lt;p&gt;In my case, a decision made for development consistency became a security feature. I set up Docker to manage multiple PHP versions and ZTS builds, but I accidentally built a security bunker.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sometimes the best security is the kind you didn't know you had.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But here's the thing: Docker is just one piece of the puzzle. You also need dependency locking, secret management, network monitoring, and a healthy dose of paranoia. Security isn't a destination, it's a journey, and supply chain attacks are just one hazard along the way.&lt;/p&gt;

&lt;p&gt;The next time you're setting up a development environment, think about the security implications of your choices. Docker, local services, isolated networks, these aren't just conveniences. They're armor.&lt;/p&gt;

&lt;p&gt;And if you're still running PHP directly on your host machine? Well, maybe it's time to put it in a box. Your future self might thank you.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Question for you&lt;/strong&gt;: Do you run your development tools in Docker or directly on your host? Have you ever considered the security implications of that choice? Share below, I'm curious how others handle this.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://www.aikido.dev/blog/supply-chain-attack-targets-laravel-lang-packages-with-credential-stealer" rel="noopener noreferrer"&gt;https://www.aikido.dev/blog/supply-chain-attack-targets-laravel-lang-packages-with-credential-stealer&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://x.com/AikidoSecurity/status/2057959626692526098" rel="noopener noreferrer"&gt;https://x.com/AikidoSecurity/status/2057959626692526098&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://pi-hole.net/" rel="noopener noreferrer"&gt;https://pi-hole.net/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>security</category>
      <category>docker</category>
      <category>php</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>How I Managed to Take My Laravel Application to the Serverless World (And How I Almost Died Trying)</title>
      <dc:creator>Hermann D. Schimpf</dc:creator>
      <pubDate>Tue, 01 Apr 2025 18:18:57 +0000</pubDate>
      <link>https://dev.to/hschimpf/how-i-managed-to-take-my-laravel-application-to-the-serverless-world-and-how-i-almost-died-trying-3mhd</link>
      <guid>https://dev.to/hschimpf/how-i-managed-to-take-my-laravel-application-to-the-serverless-world-and-how-i-almost-died-trying-3mhd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Versión en español &lt;a href="https://dev.to/hschimpf/como-logre-llevar-mi-aplicacion-laravel-al-mundo-serverless-y-como-casi-muero-en-el-intento-4b1h"&gt;aquí&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;In the &lt;a href="https://dev.to/hschimpf/why-i-migrated-my-laravel-app-to-aws-serverless-and-why-it-could-save-you-time-and-money-dem"&gt;first chapter&lt;/a&gt; of this journey, we explored why migrating to AWS Serverless was the best decision for my Laravel application. We talked about automatic scalability, cost savings, and freedom from traditional maintenance. But like in every great adventure, the path has its obstacles.&lt;/p&gt;

&lt;p&gt;One of the concerns shared by most readers is that since &lt;em&gt;serverless&lt;/em&gt; does all the magic for you and scales automatically, your wallet would also need to do so.&lt;/p&gt;

&lt;p&gt;And I must be honest with you: I fell into the trap of the &lt;em&gt;magical serverless&lt;/em&gt;. While I celebrated the absence of servers, my code wandered through the cloud like a tourist with an unlimited credit card: enthusiastic, but wasting resources at every step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;But hey! That's why I'm here today—to reveal what no tutorial warned me about: the art of writing PHP conscious of Lambda.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's dive into how to turn our Laravel application into a true &lt;em&gt;serverless&lt;/em&gt; warrior. Yes, we will tackle the necessary optimizations and the precautions we must take.&lt;/p&gt;

&lt;p&gt;But let's start lightly with some new concepts.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Serverless Magic
&lt;/h2&gt;

&lt;h3&gt;
  
  
  a) Lambda: The Execution Chef
&lt;/h3&gt;

&lt;p&gt;AWS Lambda is, in short, the chef that cooks your code just when needed. Its event-driven model means it only activates upon requests (HTTP, SQS events, cron jobs, etc.), and like an a la carte kitchen, you only pay for the time the stove is used. Additionally, Lambda's free tier allows you to start without worries, as long as you keep an eye on usage limits.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;But&lt;/strong&gt;: Did you know that AWS Lambda, among all the languages it &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html#runtimes-supported" rel="noopener noreferrer"&gt;natively supports&lt;/a&gt;, PHP is not one of them?&lt;/em&gt; 😤&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For our peace of mind, Lambda allows us to define our own &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html" rel="noopener noreferrer"&gt;custom runtimes&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) Bref: The Bridge that Connects PHP and Lambda
&lt;/h3&gt;

&lt;p&gt;Although I'd love to have created my own solution to run PHP on Lambda like some suggested &lt;em&gt;(and thanks in advance for trusting my abilities so much)&lt;/em&gt;, sometimes it's not necessary to reinvent the wheel.&lt;/p&gt;

&lt;p&gt;That's where Bref comes in, an essential tool that allows us to run PHP applications on Lambda without rewriting all our code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Great! But, how does it work?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Personally, I love knowing the &lt;a href="https://bref.sh/docs/how-it-works" rel="noopener noreferrer"&gt;inner workings&lt;/a&gt; of things, but to keep it simple, Bref operates as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg61e9thmubexsvtbgq63.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg61e9thmubexsvtbgq63.png" alt="API Gateway → Lambda → Bref → Laravel" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a request enters your application, it is captured by API Gateway and sent to the Lambda function, where Bref's runtime starts PHP-FPM in the background and redirects the request via FastCGI. Once the result is obtained, it is returned to API Gateway, which in turn sends it as a response to the end user.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Simple, right?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) Serverless Framework: The Glue That Binds All Components
&lt;/h3&gt;

&lt;p&gt;Can you imagine the amount of configurations needed to set up a Lambda function with a custom runtime that runs Bref (and with it, PHP-FPM) so that your Laravel code can work?&lt;/p&gt;

&lt;p&gt;Well, you can relax: the Serverless framework does all that for you. You just need to define the necessary components in a &lt;code&gt;serverless.yml&lt;/code&gt; configuration file and, with a simple &lt;code&gt;serverless deploy&lt;/code&gt;, you're in the cloud! 🚀&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Preparing Laravel for the Serverless Jungle
&lt;/h2&gt;

&lt;p&gt;First, you need to understand that migrating to a &lt;em&gt;serverless&lt;/em&gt; environment means that Laravel must go into &lt;em&gt;"nomadic mode"&lt;/em&gt;. This means adapting it to be &lt;strong&gt;stateless&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Do you remember when I mentioned that Lambda is ephemeral? The simplest way to imagine Lambda is like talking to someone with a very short memory: every time you start the conversation, you have to reintroduce yourself &lt;em&gt;(e.g., session data)&lt;/em&gt;. Do you give him a notebook to write something down and then return it to you &lt;em&gt;(file uploads)&lt;/em&gt;? Forget about it, you won't even find it in the &lt;em&gt;limbo&lt;/em&gt; of Inception.&lt;/p&gt;

&lt;p&gt;So, some essential adjustments are necessary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logs to stderr:&lt;/strong&gt; You don't have SSH access to Lambda, so you need to store logs in CloudWatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only storage:&lt;/strong&gt; The file system in Lambda is read-only, except for the &lt;code&gt;/tmp&lt;/code&gt; directory. So, file uploads need to be stored in S3 &lt;em&gt;(or similar)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessions:&lt;/strong&gt; You need to move session data to &lt;em&gt;cookies&lt;/em&gt; or, better yet, use DynamoDB for a more robust solution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache:&lt;/strong&gt; For simplicity, we'll opt for DynamoDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables:&lt;/strong&gt; Forget about the &lt;code&gt;.env&lt;/code&gt; file in production; you'll need to use variables defined in the &lt;code&gt;serverless.yml&lt;/code&gt; configuration file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bref's Survival Kit
&lt;/h3&gt;

&lt;p&gt;To make these adjustments easier, the Bref community has created a package called &lt;code&gt;bref/laravel-bridge&lt;/code&gt; that automates most of the changes mentioned above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Our Hands Dirty: Transforming Laravel into a Cloud Nomad
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;It's time to get our hands dirty.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to follow along step by step, the requirements from this point on are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP &lt;em&gt;(obviously!)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Composer &lt;sup id="fnref1"&gt;1&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;Node.js &lt;sup id="fnref2"&gt;2&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;serverless &lt;sup id="fnref3"&gt;3&lt;/sup&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's start by creating a new Laravel project &lt;sup id="fnref4"&gt;4&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Installing Laravel&lt;/span&gt;
laravel new laravel-above-the-clouds
&lt;span class="nb"&gt;cd &lt;/span&gt;laravel-above-the-clouds

&lt;span class="c"&gt;# Create and migrate the database (sqlite by default)&lt;/span&gt;
php artisan migrate

&lt;span class="c"&gt;# Install Node.js packages (Vite.js, Tailwind CSS, etc.) and compile assets&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the &lt;code&gt;bref/bref&lt;/code&gt; and &lt;code&gt;bref/laravel-bridge&lt;/code&gt; packages &lt;sup id="fnref5"&gt;5&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require bref/bref bref/laravel-bridge &lt;span class="nt"&gt;--update-with-dependencies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;bref/laravel-bridge&lt;/code&gt; package, as I mentioned earlier, resolves the necessary adjustments for Laravel to work in a &lt;em&gt;serverless&lt;/em&gt; environment.&lt;/p&gt;

&lt;p&gt;Bref already includes a preconfigured &lt;code&gt;serverless.yml&lt;/code&gt; file that we can use as a starting point for our first deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;serverless-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of the &lt;code&gt;serverless.yml&lt;/code&gt; file as a set of LEGO instructions for AWS: it's the guide that allows you to set up your application flawlessly.&lt;/p&gt;

&lt;p&gt;Deploy the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7bikbq8q3jytzzmd98x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7bikbq8q3jytzzmd98x.png" alt="serverless deploy output" width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;When finishes, in the console you will get the API Gateway URL to access your application on AWS!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. But, what happened with my CSS?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F58ahujvwwlydz8v4j3d5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F58ahujvwwlydz8v4j3d5.png" alt="first request, 404 on assets" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first thing you'll notice is that the assets (CSS and JS) don't load correctly. This happens because, in a &lt;em&gt;serverless&lt;/em&gt; environment, we don't have Apache or Nginx to serve static assets; and here I want to make a brief pause.&lt;/p&gt;

&lt;p&gt;Taking our application to a &lt;em&gt;serverless&lt;/em&gt; environment implies changing our mind-set that a single server is responsible for performing all the tasks to make our application work. In a &lt;em&gt;serverless&lt;/em&gt; environment, each component must be managed by a specialized service.&lt;/p&gt;

&lt;p&gt;In our case, some of the main components in a Laravel application are as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Servidor Dedicated Server&lt;/th&gt;
&lt;th&gt;AWS Serverless&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Routes, Controllers, ...&lt;/td&gt;
&lt;td&gt;PHP-FPM&lt;/td&gt;
&lt;td&gt;Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static Assets &lt;em&gt;(images, javascript, css)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Apache / Nginx&lt;/td&gt;
&lt;td&gt;S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session Data&lt;/td&gt;
&lt;td&gt;Database / Cookies&lt;/td&gt;
&lt;td&gt;RDS / DynamoDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache&lt;/td&gt;
&lt;td&gt;Local Disk&lt;/td&gt;
&lt;td&gt;DynamoDB / Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File Upload&lt;/td&gt;
&lt;td&gt;Local Disk&lt;/td&gt;
&lt;td&gt;S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled Tasks &lt;em&gt;(Schedule)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;crontab&lt;/td&gt;
&lt;td&gt;EventBridge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event Queue&lt;/td&gt;
&lt;td&gt;SSH $ &lt;code&gt;php artisan queue:work&lt;/code&gt; / maybe supervisor&lt;/td&gt;
&lt;td&gt;SQS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;artisan&lt;/code&gt; Commands&lt;/td&gt;
&lt;td&gt;SSH $ &lt;code&gt;php artisan ...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application Log&lt;/td&gt;
&lt;td&gt;SSH $ &lt;code&gt;tail -f storage/logs/*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;CloudWatch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;But let's take it easy, solving one point at a time.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  From Complexity to Simplicity: Serverless Orchestration
&lt;/h3&gt;

&lt;p&gt;From the points mentioned, the &lt;code&gt;bref/laravel-bridge&lt;/code&gt; package already solves application logs and session data. A log group is created in CloudWatch for application logs, and session data is stored in cookies &lt;em&gt;(we will later see how to move session data to DynamoDB)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;To serve static assets, we have two alternatives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The hard way:&lt;/strong&gt; Add the creation of a S3 Bucket to our &lt;code&gt;serverless.yml&lt;/code&gt; configuration, obtain the bucket name, build the public URL, and assign it to the &lt;code&gt;ASSET_URL&lt;/code&gt; environment variable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The easy way:&lt;/strong&gt; Use the &lt;code&gt;serverless-lift&lt;/code&gt; plugin &lt;sup id="fnref6"&gt;6&lt;/sup&gt;, and let the magic flow.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Although I learned a lot the hard way &lt;em&gt;(I've been following Bref's growth since 2020 &lt;sup id="fnref7"&gt;7&lt;/sup&gt;)&lt;/em&gt;, you can take advantage of community-developed tools that simplify our lives.&lt;/p&gt;

&lt;p&gt;We install the &lt;code&gt;serverless-lift&lt;/code&gt; plugin and add a new section &lt;sup id="fnref8"&gt;8&lt;/sup&gt; within the &lt;code&gt;serverless.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless plugin &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; serverless-lift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;service: laravel
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;functions:
&lt;/span&gt;  web:
    handler: public/index.php
    runtime: php-82-fpm
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  artisan:
    handler: artisan
    runtime: php-82-console
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ constructs:
+   website:
+     type: server-side-website
+     assets:
+       '/build/*': public/build
+       # Add here any file or directory that needs to be served from S3
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;plugins:
&lt;/span&gt;  - ./vendor/bref/bref
&lt;span class="gi"&gt;+   - serverless-lift
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within the &lt;code&gt;constructs&lt;/code&gt; section, we have defined a &lt;code&gt;website&lt;/code&gt; component of type &lt;code&gt;server-side-website&lt;/code&gt;. This tells the &lt;code&gt;serverless-lift&lt;/code&gt; plugin to create a &lt;strong&gt;CloudFront&lt;/strong&gt; distribution, which will act as a CDN to serve static files from an S3 Bucket, and also as a reverse proxy, redirecting requests to your PHP application's routes through API Gateway =&amp;gt; Lambda.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmp3emi5unpl6e42nzrpt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmp3emi5unpl6e42nzrpt.png" alt="Serverless with CloudFront" width="694" height="417"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This deployment will take an additional 5-7 minutes only once, as CloudFront distributions are created globally. Subsequent deployments will be faster, as long as you don't modify configurations that affect the CloudFront distribution.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the console output, you will see that you now have two URLs, one from the API Gateway &lt;em&gt;(which we will no longer use)&lt;/em&gt;, and another from CloudFront, which will serve static assets from the S3 Bucket, and the rest of the routes will be processed by Lambda.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8cujunv9ntlahiwtv3ie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8cujunv9ntlahiwtv3ie.png" alt="serverless deploy output with CloudFront" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And now the CSS loads correctly! Congratulations, you have deployed your first Laravel application in a &lt;em&gt;serverless&lt;/em&gt; environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkjiyh2926gopiitc9jwt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkjiyh2926gopiitc9jwt.png" alt="Laravel on AWS Serverless" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Optimizing for the Real World: The Battle Against Cold-Start
&lt;/h2&gt;

&lt;p&gt;You may have noticed that the first time you accessed your application, it took a few extra seconds to respond.&lt;/p&gt;

&lt;p&gt;As I mentioned in the previous article, this is called a &lt;em&gt;cold-start&lt;/em&gt; and occurs when Lambda initializes a new instance to run your application. This initialization can take 250ms or more, especially if your application is large.&lt;/p&gt;

&lt;p&gt;Lambda keeps an instance alive for up to 10 minutes after processing a request, then it is automatically destroyed.&lt;/p&gt;

&lt;p&gt;We can address this from several fronts.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Keep a Lambda instance always alive &lt;sup id="fnref9"&gt;9&lt;/sup&gt;
&lt;/h3&gt;

&lt;p&gt;In low-traffic applications, it is normal to have periods of inactivity greater than 10 minutes, so we may have a higher percentage of requests processed in a &lt;em&gt;cold-start&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Bref provides a special event that we can use to keep a Lambda instance alive. We simply need to add a &lt;code&gt;schedule&lt;/code&gt; event in our &lt;code&gt;serverless.yml&lt;/code&gt; configuration file with the payload &lt;code&gt;{warmer: true}&lt;/code&gt;. Bref will recognize this special event and respond instantly with status code 100 &lt;sup id="fnref10"&gt;10&lt;/sup&gt; without executing your code, thus keeping the instance alive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;service: laravel
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;functions:
&lt;/span&gt;  web:
    handler: public/index.php
    runtime: php-82-fpm
    events:
      - httpApi: '*'
&lt;span class="gi"&gt;+       - schedule:
+         rate: rate(5 minutes)
+         input:
+           warmer: true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  b) Reducing the size of your application
&lt;/h3&gt;

&lt;p&gt;During a cold-start, AWS Lambda downloads the application package, decompresses it in a temporary environment, and loads the runtime with all the necessary dependencies and configurations. This process, although optimized, adds latency to the first request. Therefore, reducing the package size (for example, by removing development dependencies or unnecessary modules) can significantly shorten this time.&lt;/p&gt;

&lt;p&gt;The default &lt;code&gt;serverless.yml&lt;/code&gt; file from Bref already ignores certain directories that are not necessary in PHP, such as &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Development packages
&lt;/h4&gt;

&lt;p&gt;Before deploying, we can uninstall development packages. This will significantly reduce the size of the &lt;code&gt;vendor&lt;/code&gt; directory, and consequently, the total size of the code sent to Lambda.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  AWS Services dependencies
&lt;/h4&gt;

&lt;p&gt;In most of my applications, the composer package that takes up the most space is &lt;code&gt;aws/aws-sdk-php&lt;/code&gt; &lt;em&gt;(required by &lt;code&gt;bref/laravel-bridge&lt;/code&gt;)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The good news is that the community has developed a composer script to clean up unused AWS service packages in our application &lt;sup id="fnref11"&gt;11&lt;/sup&gt;. We just need to specify the services we use in the &lt;code&gt;composer.json&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;{
  "name": "laravel/laravel",
  "type": "project",
  ...
  "require": {
    "php": "^8.2",
    "bref/bref": "^2.3",
    "bref/laravel-bridge": "^2.5",
    "laravel/framework": "^11.31",
    "laravel/octane": "^2.8",
    ...
  },
  "scripts": {
&lt;span class="gi"&gt;+     "pre-autoload-dump": "Aws\\Script\\Composer\\Composer::removeUnusedServices",
&lt;/span&gt;    "post-autoload-dump": [
      "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
      "@php artisan package:discover --ansi"
    ],
    ...
  },
  "extra": {
    "laravel": {
      "dont-discover": []
    },
&lt;span class="gi"&gt;+     "aws/aws-sdk-php": [
+       "DynamoDb",
+       "S3",
+       "Sqs"
+     ]
&lt;/span&gt;  },
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cleanup script will run every time you run &lt;code&gt;composer install&lt;/code&gt; or &lt;code&gt;composer update&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Boosting Performance with Laravel Octane
&lt;/h3&gt;

&lt;p&gt;Even when &lt;em&gt;cold-starts&lt;/em&gt; occur, Laravel Octane kicks in to keep the application in memory and significantly reduce response times. By avoiding a full Laravel boot on each request, the user experience is greatly improved.&lt;/p&gt;

&lt;p&gt;Laravel Octane acts as an accelerator that "keeps the application warm". Once Lambda initializes the application, Octane takes care of keeping it in memory, so that subsequent requests can be served almost instantly. It's like starting a car engine and leaving it ready to go at any moment, rather than starting it from scratch each time.&lt;/p&gt;

&lt;p&gt;Steps to implement Laravel Octane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/octane
php artisan octane:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, update the &lt;code&gt;serverless.yml&lt;/code&gt; file to use Laravel Octane as the handler &lt;sup id="fnref12"&gt;12&lt;/sup&gt;. This will ensure that your application runs with the improved performance that Laravel Octane offers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;service: laravel
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;functions:
&lt;/span&gt;  web:
    handler: Bref\LaravelBridge\Http\OctaneHandler
    runtime: php-82
&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, deploy your application with the changes made:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  d) Metrics that Validate Progress
&lt;/h3&gt;

&lt;p&gt;Applying all the improvements mentioned, we have reduced the initialization time &lt;em&gt;(cold-start)&lt;/em&gt; of our application. And with Laravel Octane, we have not only improved the time of the &lt;em&gt;cold-start&lt;/em&gt;, but also improved the overall response time of our application.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;We can visualize the response times in the CloudWatch logs that were already configured automatically for us.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cold-start &lt;strong&gt;without optimizations&lt;/strong&gt; &lt;em&gt;(499ms)&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fquieaec9z2cepjz0dazz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fquieaec9z2cepjz0dazz.png" alt="php-fpm coldstart" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cold-start with &lt;strong&gt;Laravel Octane and optimizations&lt;/strong&gt; &lt;em&gt;(190ms)&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfh1wtwucl0unkfq6q4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfh1wtwucl0unkfq6q4k.png" alt="Laravel Octane coldstart" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note that when using Laravel Octane, Laravel execution is kept in memory, so be careful of memory leaks &lt;sup id="fnref13"&gt;13&lt;/sup&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. Falls and Lessons Learned: Errors Every Serverless Migrant Should Know
&lt;/h2&gt;

&lt;p&gt;You may have noticed that there is a function called &lt;code&gt;artisan&lt;/code&gt; in the &lt;code&gt;serverless.yml&lt;/code&gt; file. This function uses the &lt;em&gt;"console"&lt;/em&gt; version of the Bref runtime &lt;sup id="fnref14"&gt;14&lt;/sup&gt;, which allows us to run Laravel Artisan commands in AWS Lambda.&lt;/p&gt;

&lt;p&gt;Since we don't have SSH access to the Lambda instances, Bref provides a bridge to execute &lt;code&gt;cli&lt;/code&gt; commands using the &lt;code&gt;bref:cli&lt;/code&gt; command from &lt;em&gt;serverless&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless bref:cli &lt;span class="nt"&gt;--args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;artisan command and its options&amp;gt;"&lt;/span&gt;
serverless bref:cli &lt;span class="nt"&gt;--args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"route:list"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Optimizations
&lt;/h3&gt;

&lt;p&gt;A very important point I want to reinforce is that we must change our mentality when developing applications for &lt;em&gt;serverless&lt;/em&gt; environments. In this new paradigm, it is crucial to design our processes and workflows considering the unique characteristics of &lt;em&gt;serverless&lt;/em&gt; computing, such as the ephemeral nature of functions and the billing model based on usage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Serverless is not expensive... but poorly adapted code is.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my first month migrating one of my applications to a &lt;em&gt;serverless&lt;/em&gt; environment, I made the mistake of not thoroughly reviewing certain processes. One of them was responsible for synchronizing the products of the online store with an external billing system, including updating product images. And as I mentioned in the previous article, some processes are not ideal for execution in a &lt;em&gt;serverless&lt;/em&gt; environment. In this case, I was paying for idle time, during the image download, my code in Lambda was not doing anything more than waiting 5-8 seconds per image. With 65,000 items in the store and two daily synchronizations, this resulted in a bill of &lt;strong&gt;~$530&lt;/strong&gt; just for the idle time during image download.&lt;/p&gt;

&lt;p&gt;This incident led me to redesign the synchronization process. In a first attempt at optimization, I implemented the download of multiple images in parallel using PHP ZTS, which led me to develop the &lt;code&gt;hds-solutions/parallel-sdk&lt;/code&gt; library &lt;sup id="fnref15"&gt;15&lt;/sup&gt;. This version reduced costs to &lt;strong&gt;~160$&lt;/strong&gt;, but it was still not enough; my goal was to have costs below what I was spending originally on EC2.&lt;/p&gt;

&lt;p&gt;In a second attempt, I moved the image download process to a dedicated EC2 instance. Although this significantly reduced costs to &lt;strong&gt;~50$&lt;/strong&gt;, the application was divided into two separate components.&lt;/p&gt;

&lt;p&gt;Finally, in the third version of the synchronization process, after talking to the client, I implemented a small script on the billing system server. Instead of my application downloading the images, this script sent the images directly from the billing system server to S3 using pre-signed URLs &lt;sup id="fnref16"&gt;16&lt;/sup&gt;. This eliminated the costs associated with image download, leaving only the cost of S3 storage.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. What's Next?
&lt;/h2&gt;

&lt;p&gt;With this, we close another chapter in our journey to the &lt;em&gt;serverless&lt;/em&gt; world! Today, we saw how to transform Laravel into a true cloud nomad, adapting it to live without the weight of a traditional server. But this is just the beginning.&lt;/p&gt;

&lt;p&gt;In the next article, we'll dive into the heart of integration: we'll see how to transfer session and cache data to DynamoDB, how to use S3 for file management, configure SQS for queuing and use EventBridge to orchestrate scheduled tasks. See you in the next part!&lt;/p&gt;




&lt;p&gt;In the meantime, have you migrated your Laravel application to &lt;em&gt;serverless&lt;/em&gt;? Share your experiences in the comments! And for those who haven't, I encourage you to experiment with what we've implemented so far and share your doubts and experiences.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://getcomposer.org/download" rel="noopener noreferrer"&gt;https://getcomposer.org/download&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://github.com/nvm-sh/nvm#installation-and-update" rel="noopener noreferrer"&gt;https://github.com/nvm-sh/nvm#installation-and-update&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://www.serverless.com/framework/docs/getting-started" rel="noopener noreferrer"&gt;https://www.serverless.com/framework/docs/getting-started&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/docs/installation" rel="noopener noreferrer"&gt;https://laravel.com/docs/installation&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/getting-started" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/getting-started&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/use-cases/websites" rel="noopener noreferrer"&gt;https://bref.sh/docs/use-cases/websites&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;&lt;a href="https://github.com/brefphp/bref/releases/tag/0.5.20" rel="noopener noreferrer"&gt;https://github.com/brefphp/bref/releases/tag/0.5.20&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/server-side-website.md" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/server-side-website.md&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/use-cases/http/advanced-use-cases#cold-starts" rel="noopener noreferrer"&gt;https://bref.sh/docs/use-cases/http/advanced-use-cases#cold-starts&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;&lt;a href="https://github.com/aws/aws-sdk-php/tree/master/src/Script/Composer" rel="noopener noreferrer"&gt;https://github.com/aws/aws-sdk-php/tree/master/src/Script/Composer&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn12"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/octane" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/octane&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn13"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/docs/11.x/octane#managing-memory-leaks" rel="noopener noreferrer"&gt;https://laravel.com/docs/11.x/octane#managing-memory-leaks&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn14"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/runtimes/console" rel="noopener noreferrer"&gt;https://bref.sh/docs/runtimes/console&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn15"&gt;
&lt;p&gt;&lt;a href="https://github.com/hschimpf/parallel-sdk" rel="noopener noreferrer"&gt;https://github.com/hschimpf/parallel-sdk&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn16"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/es_es/AmazonS3/latest/userguide/PresignedUrlUploadObject.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/es_es/AmazonS3/latest/userguide/PresignedUrlUploadObject.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>serverless</category>
      <category>aws</category>
    </item>
    <item>
      <title>Como Logré Llevar Mi Aplicación Laravel al Mundo Serverless (Y como casi muero en el intento)</title>
      <dc:creator>Hermann D. Schimpf</dc:creator>
      <pubDate>Tue, 11 Mar 2025 22:36:10 +0000</pubDate>
      <link>https://dev.to/hschimpf/como-logre-llevar-mi-aplicacion-laravel-al-mundo-serverless-y-como-casi-muero-en-el-intento-4b1h</link>
      <guid>https://dev.to/hschimpf/como-logre-llevar-mi-aplicacion-laravel-al-mundo-serverless-y-como-casi-muero-en-el-intento-4b1h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;English version &lt;a href="https://dev.to/hschimpf/how-i-managed-to-take-my-laravel-application-to-the-serverless-world-and-how-i-almost-died-trying-3mhd"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;En el &lt;a href="https://dev.to/hschimpf/por-que-migre-mi-aplicacion-laravel-a-aws-serverless-y-por-que-podria-ahorrarte-tiempo-y-dinero-6hp"&gt;primer capítulo&lt;/a&gt; de esta travesía, exploramos por qué migrar a AWS Serverless fue la mejor decisión para mi aplicación Laravel. Hablamos de escalabilidad automática, ahorro de costos y libertad del mantenimiento tradicional. Pero como en toda gran aventura, el camino tiene sus obstáculos.&lt;/p&gt;

&lt;p&gt;Una de las preocupaciones que compartieron la mayoría de los lectores es que, dado que &lt;em&gt;serverless&lt;/em&gt; hace toda la magia por ustedes y escala automáticamente, su billetera también tendría que hacerlo.&lt;/p&gt;

&lt;p&gt;Y debo ser sincero con ustedes: caí en la trampa del &lt;em&gt;serverless&lt;/em&gt; mágico. Mientras yo celebraba la ausencia de servidores, mi código se paseaba por la nube como un turista con tarjeta de crédito ilimitada: entusiasta, pero derrochando recursos a cada paso.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Pero hey! Para eso estoy hoy aquí—para revelarles lo que ningún tutorial me advirtió: el arte de escribir PHP consciente de Lambda.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vamos a meternos de lleno en el cómo convertir nuestra aplicación Laravel en un verdadero guerrero &lt;em&gt;serverless&lt;/em&gt;. Sí, atacaremos las optimizaciones necesarias y las precauciones que debemos tener.&lt;/p&gt;

&lt;p&gt;Pero empecemos liviano con algunos conceptos nuevos.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. La Magia Serverless
&lt;/h2&gt;

&lt;h3&gt;
  
  
  a) Lambda: El Chef de la Ejecución
&lt;/h3&gt;

&lt;p&gt;AWS Lambda es, en pocas palabras, el chef que cocina el código justo cuando se le necesita. Su modelo basado en eventos significa que solo se activa ante solicitudes (HTTP, eventos de SQS, cron jobs, etc.) y, como en una cocina a la carta, solo pagan por el tiempo que se utiliza la estufa. Además, el nivel gratuito de Lambda les permite arrancar sin preocupaciones, siempre y cuando mantengan un ojo en los límites de uso.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Pero&lt;/strong&gt;: ¿sabían que AWS Lambda, entre todos los lenguajes que &lt;a href="https://docs.aws.amazon.com/es_es/lambda/latest/dg/lambda-runtimes.html#runtimes-supported" rel="noopener noreferrer"&gt;soporta nativamente&lt;/a&gt; nativamente, PHP no es uno de ellos?&lt;/em&gt; 😤&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Para nuestra tranquilidad, Lambda permite definir nuestros propios &lt;a href="https://docs.aws.amazon.com/es_es/lambda/latest/dg/runtimes-custom.html" rel="noopener noreferrer"&gt;runtimes personalizados&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) Bref: El Puente que une PHP y Lambda
&lt;/h3&gt;

&lt;p&gt;Aunque me encantaría haber creado mi propia solución para ejecutar PHP en Lambda como sugirieron algunos &lt;em&gt;(y gracias de antemano por confiar tanto en mis capacidades)&lt;/em&gt;, a veces no es necesario reinventar la rueda.&lt;/p&gt;

&lt;p&gt;Aquí es donde entra Bref, una herramienta esencial que nos permite ejecutar aplicaciones PHP en Lambda sin reescribir todo nuestro código.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;¡Buenísimo! Pero, ¿cómo funciona?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Personalmente, me encanta conocer a fondo el &lt;a href="https://bref.sh/docs/how-it-works" rel="noopener noreferrer"&gt;funcionamiento interno&lt;/a&gt; de las cosas, pero para mantener la simplicidad, Bref opera de la siguiente manera:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg61e9thmubexsvtbgq63.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg61e9thmubexsvtbgq63.png" alt="API Gateway → Lambda → Bref → Laravel" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cuando una solicitud ingresa a tu aplicación, esta es capturada por API Gateway y enviada a ejecución en la función Lambda, donde el runtime de Bref inicia PHP-FPM en background y la redigire la solicitud vía FastCGI. Una vez obtenido el resultado, lo devuelve al API Gateway, para que a su vez sea enviado finalmente como respuesta al usuario final.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;¿Simple, cierto?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) Serverless Framework: El Pegamento que Une Todos los Componentes
&lt;/h3&gt;

&lt;p&gt;¿Se imaginan la cantidad de configuraciones que necesitan realizar para poner en marcha una función Lambda, con un runtime personalizado, que ejecute Bref (y con éste, PHP-FPM) para que tu código Laravel pueda funcionar?&lt;/p&gt;

&lt;p&gt;Pues ya puedes relajarte: el framework Serverless hace todo eso por ustedes. Solo tienen que definir los componentes necesarios en un fichero de configuración &lt;code&gt;serverless.yml&lt;/code&gt; y, con un simple &lt;code&gt;serverless deploy&lt;/code&gt;, ¡ya están en la nube! 🚀&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Preparando Laravel para la Jungla Serverless
&lt;/h2&gt;

&lt;p&gt;Primero deben entender que migrar a un entorno &lt;em&gt;serverless&lt;/em&gt; implica que Laravel debe ponerse en &lt;em&gt;"modo nómada"&lt;/em&gt;. Esto significa adaptarlo para que sea &lt;strong&gt;stateless&lt;/strong&gt; &lt;em&gt;(sin estado)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;¿Recuerdan que mencioné que Lambda es efímero? La forma más simple de imaginar Lambda es como conversar con alguien que tiene una memoria muy corta: cada vez que inician la conversación, tienen que volver a presentarte &lt;em&gt;(por ej, los datos de sesión)&lt;/em&gt;. ¿Le dan un cuaderno para que les anote algo y luego se lo devuelva &lt;em&gt;(upload de archivos)&lt;/em&gt;? Olvídenlo, ni en el &lt;em&gt;limbo&lt;/em&gt; de Inception lo encontrarán.&lt;/p&gt;

&lt;p&gt;Por lo tanto, es necesario realizar algunos ajustes esenciales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logs a stderr:&lt;/strong&gt; No tienen acceso SSH a Lambda, por lo que deben hacer que los logs se almacenen en CloudWatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Almacenamiento de solo lectura:&lt;/strong&gt; El sistema de archivos en Lambda es de solo lectura, a excepción del directorio &lt;code&gt;/tmp&lt;/code&gt;. Por lo que la subida de archivos debe ser almacenada en S3 &lt;em&gt;(o similar)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sesiones:&lt;/strong&gt; Deben mover los datos de sesión a &lt;em&gt;cookies&lt;/em&gt; o, mejor aún, usar DynamoDB para una solución más robusta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caché:&lt;/strong&gt; Por simplicidad, optamos por DynamoDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variables de entorno:&lt;/strong&gt; Olvídense del fichero &lt;code&gt;.env&lt;/code&gt; en producción; deberán utilizar variables definidas en el fichero de configuración &lt;code&gt;serverless.yml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  El Kit de Supervivencia de Bref
&lt;/h3&gt;

&lt;p&gt;Para facilitar estos ajustes, la comunidad de Bref ha creado un paquete llamado &lt;code&gt;bref/laravel-bridge&lt;/code&gt; que automatiza la mayoría de los cambios mencionados anteriormente.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manos a la Obra: Transformando Laravel en un Nómada Cloud
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Es hora de ensuciarnos las manos.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Si quieren ir siguiendo el paso a paso, los requisitos a partir de este punto son los siguientes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP &lt;em&gt;(¡obviamente!)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Composer &lt;sup id="fnref1"&gt;1&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;Node.js &lt;sup id="fnref2"&gt;2&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;serverless &lt;sup id="fnref3"&gt;3&lt;/sup&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Empecemos creando un proyecto de Laravel nuevo &lt;sup id="fnref4"&gt;4&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Instalación de Laravel&lt;/span&gt;
laravel new laravel-above-the-clouds
&lt;span class="nb"&gt;cd &lt;/span&gt;laravel-above-the-clouds

&lt;span class="c"&gt;# Creamos y migramos la base de datos (sqlite por defecto)&lt;/span&gt;
php artisan migrate

&lt;span class="c"&gt;# Instalamos los paquetes de Node.js (Vite.js, Tailwind CSS, etc.) y compilamos los assets&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instalamos los paquetes &lt;code&gt;bref/bref&lt;/code&gt; y &lt;code&gt;bref/laravel-bridge&lt;/code&gt; &lt;sup id="fnref5"&gt;5&lt;/sup&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require bref/bref bref/laravel-bridge &lt;span class="nt"&gt;--update-with-dependencies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El paquete &lt;code&gt;bref/laravel-bridge&lt;/code&gt;, como mencioné anteriormente, resuelve los ajustes necesarios para que Laravel funcione en un ambiente &lt;em&gt;serverless&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Bref ya incluye un fichero de configuración &lt;code&gt;serverless.yml&lt;/code&gt; preconfigurado que podemos utilizar como base para nuestro primer deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;serverless-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Piensen en el fichero &lt;code&gt;serverless.yml&lt;/code&gt; como las instrucciones de un set de LEGO para AWS: es la guía que le permite montar su aplicación de forma impecable.&lt;/p&gt;

&lt;p&gt;Realicen el deploy del proyecto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7bikbq8q3jytzzmd98x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7bikbq8q3jytzzmd98x.png" alt="serverless deploy output" width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Al finalizar, en la consola obtendrán la URL del API Gateway para acceder a su aplicación en AWS!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Pero, ¿qué ha pasado con mi CSS?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F58ahujvwwlydz8v4j3d5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F58ahujvwwlydz8v4j3d5.png" alt="first request, 404 on assets" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lo primero que notarán es que los assets (CSS y JS) no cargan correctamente. Esto sucede porque, en un entorno &lt;em&gt;serverless&lt;/em&gt;, no disponemos de Apache o Nginx para servir los assets estáticos; y aquí quiero hacer una breve pausa.&lt;/p&gt;

&lt;p&gt;Llevar nuestra aplicación a un entorno &lt;em&gt;serverless&lt;/em&gt; implica cambiar nuestro mind-set de que un único servidor es responsable de realizar todas las tareas para hacer funcionar nuestra aplicación. En un entorno &lt;em&gt;serverless&lt;/em&gt;, cada componente debe ser gestionado por un servicio especializado.&lt;/p&gt;

&lt;p&gt;En nuestro caso, algunos de los componentes principales en una aplicación Laravel son los siguientes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Componente&lt;/th&gt;
&lt;th&gt;Servidor Dedicado&lt;/th&gt;
&lt;th&gt;AWS Serverless&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rutas, Controladores, ...&lt;/td&gt;
&lt;td&gt;PHP-FPM&lt;/td&gt;
&lt;td&gt;Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assets estáticos &lt;em&gt;(imágenes, javascript, css)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Apache / Nginx&lt;/td&gt;
&lt;td&gt;S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Datos de sesión&lt;/td&gt;
&lt;td&gt;Base de Datos / Cookies&lt;/td&gt;
&lt;td&gt;RDS / DynamoDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caché&lt;/td&gt;
&lt;td&gt;Disco local&lt;/td&gt;
&lt;td&gt;DynamoDB / Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upload de archivos&lt;/td&gt;
&lt;td&gt;Disco local&lt;/td&gt;
&lt;td&gt;S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tareas programadas &lt;em&gt;(Schedule)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;crontab&lt;/td&gt;
&lt;td&gt;EventBridge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cola de Eventos&lt;/td&gt;
&lt;td&gt;SSH $ &lt;code&gt;php artisan queue:work&lt;/code&gt; / quizás supervisor&lt;/td&gt;
&lt;td&gt;SQS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comandos &lt;code&gt;artisan&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;SSH $ &lt;code&gt;php artisan ...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log de Aplicación&lt;/td&gt;
&lt;td&gt;SSH $ &lt;code&gt;tail -f storage/logs/*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;CloudWatch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Pero vayamos con calma, resolviendo un punto a la vez.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  De la Complejidad a la Simplicidad: La Orquestación Serverless
&lt;/h3&gt;

&lt;p&gt;De los puntos mencionados, el paquete &lt;code&gt;bref/laravel-bridge&lt;/code&gt; ya resuelve el log de la aplicación y los datos de sesión. Se crea un grupo de logs en CloudWatch para los logs de aplicación, y los datos de sesión son almacenados en cookies &lt;em&gt;(más adelante veremos cómo llevar los datos de sesión a DynamoDB)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Para poder servir los assets estáticos, tenemos dos alternativas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La difícil:&lt;/strong&gt; Agregar la creación de un Bucket S3 a nuestra configuración &lt;code&gt;serverless.yml&lt;/code&gt;, obtener el nombre del bucket, armar la URL pública y asignarla en la variable de entorno &lt;code&gt;ASSET_URL&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La simple:&lt;/strong&gt; Utilizar el plugin &lt;code&gt;serverless-lift&lt;/code&gt; &lt;sup id="fnref6"&gt;6&lt;/sup&gt;, y dejar que la magia fluya.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Aunque aprendí mucho por el camino difícil &lt;em&gt;(vengo acompañando el crecimiento de Bref desde 2020 &lt;sup id="fnref7"&gt;7&lt;/sup&gt;)&lt;/em&gt;, ustedes pueden aprovechar que la comunidad ha desarrollado herramientas que nos simplifican la vida.&lt;/p&gt;

&lt;p&gt;Instalamos el plugin &lt;code&gt;serverless-lift&lt;/code&gt; y agregamos una nueva sección &lt;sup id="fnref8"&gt;8&lt;/sup&gt; dentro del fichero &lt;code&gt;serverless.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless plugin &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; serverless-lift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;service: laravel
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;functions:
&lt;/span&gt;  web:
    handler: public/index.php
    runtime: php-82-fpm
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  artisan:
    handler: artisan
    runtime: php-82-console
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ constructs:
+   website:
+     type: server-side-website
+     assets:
+       '/build/*': public/build
+       # Agreguen aquí cualquier archivo o directorio que necesite ser servido desde S3
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;plugins:
&lt;/span&gt;  - ./vendor/bref/bref
&lt;span class="gi"&gt;+   - serverless-lift
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dentro de la sección &lt;code&gt;constructs&lt;/code&gt; hemos definido un componente &lt;code&gt;website&lt;/code&gt; del tipo &lt;code&gt;server-side-website&lt;/code&gt;. Esto le dice al plugin &lt;code&gt;serverless-lift&lt;/code&gt; que cree una distribución de &lt;strong&gt;CloudFront&lt;/strong&gt;, la cual actuará como CDN para servir los archivos estáticos desde un Bucket S3, y a su vez como proxy reverso, redirigiendo las solicitudes a las rutas de tu aplicación PHP a través de API Gateway =&amp;gt; Lambda.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmp3emi5unpl6e42nzrpt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmp3emi5unpl6e42nzrpt.png" alt="Serverless with CloudFront" width="694" height="417"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Este deploy demorará unos 5-7 minutos adicionales por única vez, ya que las distribuciones de CloudFront son creadas globalmente. Los siguientes deploys serán más rápidos, siempre que no modifiquen configuraciones que afecten a la distribución de CloudFront.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;En el resultado de la consola verán que ahora disponen de dos URLs, una del API Gateway &lt;em&gt;(que ya no utilizaremos)&lt;/em&gt;, y otra de CloudFront, el cual servirá los assets estáticos desde el Bucket S3, y el resto de las rutas serán procesadas por Lambda.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8cujunv9ntlahiwtv3ie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8cujunv9ntlahiwtv3ie.png" alt="serverless deploy output with CloudFront" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;¡Y ahora el CSS se carga correctamente! Felicidades, han desplegado su primera aplicación Laravel en ambiente &lt;em&gt;serverless&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkjiyh2926gopiitc9jwt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkjiyh2926gopiitc9jwt.png" alt="Laravel on AWS Serverless" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Optimizando para el Mundo Real: La Batalla Contra el Cold-Start
&lt;/h2&gt;

&lt;p&gt;Seguramente habrán notado que la primera vez que ingresaron a su aplicación, esta demoró unos segundos adicionales en responder.&lt;/p&gt;

&lt;p&gt;Así como mencioné en el artículo anterior, esto es llamado &lt;em&gt;cold-start&lt;/em&gt; y ocurre cuando Lambda inicializa una nueva instancia para ejecutar su aplicación. Esta inicialización puede llevar 250ms o más, especialmente si tu aplicación es grande.&lt;/p&gt;

&lt;p&gt;Lambda mantiene viva una instancia hasta 10 minutos después de procesar una solicitud, luego es destruida automáticamente.&lt;/p&gt;

&lt;p&gt;Podemos abordar esto desde varios frentes.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Mantener siempre viva una instancia Lambda &lt;sup id="fnref9"&gt;9&lt;/sup&gt;
&lt;/h3&gt;

&lt;p&gt;En aplicaciones con poco tráfico, es normal que hayan periodos mayores a 10 minutos de inactividad, por lo que podremos tener un porcentaje mayor de solicitudes procesadas en un &lt;em&gt;cold-start&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Bref dispone de un evento especial que podemos utilizar para mantener viva una instancia de Lambda. Simplemente debemos agregar un evento &lt;code&gt;schedule&lt;/code&gt; en nuestro fichero de configuración &lt;code&gt;serverless.yml&lt;/code&gt; con el payload &lt;code&gt;{warmer: true}&lt;/code&gt;. Bref reconocerá este evento especial y responderá instantáneamente con el código de estado 100 &lt;sup id="fnref10"&gt;10&lt;/sup&gt; sin ejecutar tu código, manteniendo así viva la instancia.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;service: laravel
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;functions:
&lt;/span&gt;  web:
    handler: public/index.php
    runtime: php-82-fpm
    events:
      - httpApi: '*'
&lt;span class="gi"&gt;+       - schedule:
+         rate: rate(5 minutes)
+         input:
+           warmer: true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  b) Reducir el tamaño de su aplicación
&lt;/h3&gt;

&lt;p&gt;Durante un cold-start, AWS Lambda descarga el paquete de la aplicación, lo descomprime en un entorno temporal y carga el runtime junto con todas las dependencias y configuraciones necesarias. Este proceso, aunque está optimizado, añade latencia a la primera solicitud. Por ello, reducir el tamaño del paquete &lt;em&gt;(por ejemplo, eliminando dependencias de desarrollo o módulos innecesarios)&lt;/em&gt; puede acortar considerablemente este tiempo de inicialización.&lt;/p&gt;

&lt;p&gt;El fichero &lt;code&gt;serverless.yml&lt;/code&gt; por defecto de Bref ya ignora ciertos directorios que no son necesarios en PHP, como por ejemplo &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Paquetes de desarrollo
&lt;/h4&gt;

&lt;p&gt;Antes de realizar el deploy, podemos desinstalar los paquetes de desarrollo. Esto reducirá en gran manera el tamaño del directorio &lt;code&gt;vendor&lt;/code&gt;, y en consecuencia, el tamaño total del código enviado a Lambda.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Dependencias de servicios AWS
&lt;/h4&gt;

&lt;p&gt;En la mayoría de mis aplicaciones, el paquete de composer que más ocupa espacio es &lt;code&gt;aws/aws-sdk-php&lt;/code&gt; &lt;em&gt;(requerido por &lt;code&gt;bref/laravel-bridge&lt;/code&gt;)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Lo bueno es que la comunidad ha desarrollado un script de composer para limpiar los paquetes de servicios AWS no utilizados en nuestra aplicación &lt;sup id="fnref11"&gt;11&lt;/sup&gt;. Solo debemos especificar en el fichero &lt;code&gt;composer.json&lt;/code&gt; los servicios que sí utilizamos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;{
  "name": "laravel/laravel",
  "type": "project",
  ...
  "require": {
    "php": "^8.2",
    "bref/bref": "^2.3",
    "bref/laravel-bridge": "^2.5",
    "laravel/framework": "^11.31",
    "laravel/octane": "^2.8",
    ...
  },
  "scripts": {
&lt;span class="gi"&gt;+     "pre-autoload-dump": "Aws\\Script\\Composer\\Composer::removeUnusedServices",
&lt;/span&gt;    "post-autoload-dump": [
      "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
      "@php artisan package:discover --ansi"
    ],
    ...
  },
  "extra": {
    "laravel": {
      "dont-discover": []
    },
&lt;span class="gi"&gt;+     "aws/aws-sdk-php": [
+       "DynamoDb",
+       "S3",
+       "Sqs"
+     ]
&lt;/span&gt;  },
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El script de limpieza correrá cada vez que ejecutemos &lt;code&gt;composer install&lt;/code&gt; o &lt;code&gt;composer update&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Impulsando el Rendimiento con Laravel Octane
&lt;/h3&gt;

&lt;p&gt;Incluso cuando se producen &lt;em&gt;cold-starts&lt;/em&gt;, Laravel Octane entra en acción para mantener la aplicación en memoria y reducir significativamente los tiempos de respuesta. Al evitar un arranque completo de Laravel en cada solicitud, se mejora notablemente la experiencia del usuario final.&lt;/p&gt;

&lt;p&gt;Laravel Octane actúa como un acelerador que "mantiene caliente" la aplicación. Una vez que Lambda inicia la aplicación, Octane se encarga de conservarla en memoria, de modo que las siguientes solicitudes se atiendan casi de inmediato. Es como encender el motor de un coche y dejarlo listo para arrancar en cualquier momento, en lugar de encenderlo desde cero cada vez.&lt;/p&gt;

&lt;p&gt;Pasos para implementar Laravel Octane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/octane
php artisan octane:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una vez instalado, actualicen el fichero &lt;code&gt;serverless.yml&lt;/code&gt; para utilizar Laravel Octane como handler &lt;sup id="fnref12"&gt;12&lt;/sup&gt;. Esto asegurará que tu aplicación se ejecute con el rendimiento mejorado que ofrece Laravel Octane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;service: laravel
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;functions:
&lt;/span&gt;  web:
    handler: Bref\LaravelBridge\Http\OctaneHandler
    runtime: php-82
&lt;span class="gd"&gt;- # ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finalmente, desplieguen su aplicación con los cambios realizados:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  d) Métricas que Validan el Progreso
&lt;/h3&gt;

&lt;p&gt;Aplicando todas las mejoras mencionadas, hemos reducido el tiempo de inicialización &lt;em&gt;(cold-start)&lt;/em&gt; de nuestra aplicación. Y con Laravel Octane, no solo hemos mejorado el tiempo del &lt;em&gt;cold-start&lt;/em&gt;, sino que mejoramos el tiempo de respuesta general de nuestra aplicación.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Podemos visualizar los tiempos de respuesta en los logs de CloudWatch que ya se configuraron automáticamente por nosotros.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cold-start &lt;strong&gt;sin optimizaciones&lt;/strong&gt; &lt;em&gt;(499ms)&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fquieaec9z2cepjz0dazz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fquieaec9z2cepjz0dazz.png" alt="php-fpm coldstart" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cold-start con &lt;strong&gt;Laravel Octane y optimizaciones&lt;/strong&gt; &lt;em&gt;(190ms)&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfh1wtwucl0unkfq6q4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfh1wtwucl0unkfq6q4k.png" alt="Laravel Octane coldstart" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Tengan en cuenta que al utilizar Laravel Octane, se mantiene la ejecución de Laravel en memoria, por lo que deben cuidarse de los memory leaks &lt;sup id="fnref13"&gt;13&lt;/sup&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. Caídas y Aprendizajes: Los Errores que Todo Migrante Serverless Debe Conocer
&lt;/h2&gt;

&lt;p&gt;Seguramente se dieron cuenta que hay una función llamada &lt;code&gt;artisan&lt;/code&gt; en el fichero &lt;code&gt;serverless.yml&lt;/code&gt;. Esa función utiliza la versión &lt;em&gt;"consola"&lt;/em&gt; del runtime de Bref &lt;sup id="fnref14"&gt;14&lt;/sup&gt;, la cual nos permite ejecutar comandos de Laravel Artisan en AWS Lambda.&lt;/p&gt;

&lt;p&gt;Como no tenemos acceso SSH a las instancias de Lambda, Bref nos disponibiliza un puente para poder ejecutar comandos &lt;code&gt;cli&lt;/code&gt; mediante el comando &lt;code&gt;bref:cli&lt;/code&gt; de &lt;em&gt;serverless&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverless bref:cli &lt;span class="nt"&gt;--args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;comando artisan y sus opciones&amp;gt;"&lt;/span&gt;
serverless bref:cli &lt;span class="nt"&gt;--args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"route:list"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Optimizaciones
&lt;/h3&gt;

&lt;p&gt;Un punto muy importante que quiero reforzar es que debemos cambiar nuestra mentalidad al desarrollar aplicaciones para entornos &lt;em&gt;serverless&lt;/em&gt;. En este nuevo paradigma, es crucial diseñar nuestros procesos y flujos de trabajo considerando las características únicas de la computación &lt;em&gt;serverless&lt;/em&gt;, como la naturaleza efímera de las funciones y el modelo de facturación basado en el uso.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Serverless no es caro... pero el código mal adaptado sí lo es.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;En mi primer mes migrando una de mis aplicaciones al entorno &lt;em&gt;serverless&lt;/em&gt;, cometí el error de no revisar detalladamente ciertos procesos. Uno de ellos se encargaba de sincronizar los productos de la tienda en línea con un sistema de facturación externo, incluyendo la actualización de imágenes de productos. Y como mencioné en el artículo anterior, algunos procesos no son ideales para ejecutarse en un entorno &lt;em&gt;serverless&lt;/em&gt;. En este caso, estaba pagando por tiempo muerto, durante la descarga de imágenes, mi código en Lambda no estaba realizando nada más que esperar 5-8 segundos por imágen. Con 65,000 artículos en la tienda y dos sincronizaciones diarias, esto resultó en una factura de &lt;strong&gt;~$530&lt;/strong&gt; solo por el tiempo inactivo durante la descarga de imágenes.&lt;/p&gt;

&lt;p&gt;Este incidente me llevó a rediseñar el proceso de sincronización. En un primer intento de optimización, implementé la descarga de múltiples imágenes en paralelo utilizando PHP ZTS, lo que me llevó a desarrollar la librería &lt;code&gt;hds-solutions/parallel-sdk&lt;/code&gt; &lt;sup id="fnref15"&gt;15&lt;/sup&gt;. Esta versión redujo los costos a &lt;strong&gt;~160$&lt;/strong&gt;, pero aún no era suficiente; mi objetivo era que los costos estuvieran por debajo de lo que gastaba originalmente en EC2.&lt;/p&gt;

&lt;p&gt;En un segundo intento, trasladé el proceso de descarga de imágenes a una instancia EC2 dedicada. Aunque esto redujo significativamente los costos a &lt;strong&gt;~50$&lt;/strong&gt;, la aplicación quedó dividida en dos componentes separados.&lt;/p&gt;

&lt;p&gt;Finalmente, en la tercera versión del proceso de sincronización, tras conversar con el cliente, implementé un pequeño script en el servidor del sistema de facturación. En lugar de que mi aplicación descargara las imágenes, este script enviaba las imágenes directamente desde el servidor del sistema de facturación a S3 utilizando URLs pre-firmadas &lt;sup id="fnref16"&gt;16&lt;/sup&gt;. Esto eliminó por completo los costos asociados a la descarga de imágenes, quedando únicamente el costo de almacenamiento en S3.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. ¿Qué Sigue?
&lt;/h2&gt;

&lt;p&gt;¡Con esto, cerramos otro capítulo en nuestro viaje hacia el mundo &lt;em&gt;serverless&lt;/em&gt;! Hoy vimos cómo transformar a Laravel en un auténtico nómada en la nube, adaptándolo para vivir sin el peso de un servidor tradicional. Pero esto es solo el comienzo.&lt;/p&gt;

&lt;p&gt;En el próximo artículo nos adentraremos en el corazón de la integración: veremos cómo trasladar los datos de sesión y caché a DynamoDB, cómo aprovechar S3 para el manejo de archivos, configurar SQS para gestionar colas y utilizar EventBridge para orquestar tareas programadas. ¡Nos vemos en la siguiente entrega!&lt;/p&gt;




&lt;p&gt;Mientras tanto, ¿han migrado su aplicación Laravel a &lt;em&gt;serverless&lt;/em&gt;? ¡Compartan sus experiencias en los comentarios! Y para los que aún no lo han hecho, les animo a experimentar con lo que hemos implementado hasta ahora y a compartir sus dudas y experiencias.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://getcomposer.org/download" rel="noopener noreferrer"&gt;https://getcomposer.org/download&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://github.com/nvm-sh/nvm#installation-and-update" rel="noopener noreferrer"&gt;https://github.com/nvm-sh/nvm#installation-and-update&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://www.serverless.com/framework/docs/getting-started" rel="noopener noreferrer"&gt;https://www.serverless.com/framework/docs/getting-started&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/docs/installation" rel="noopener noreferrer"&gt;https://laravel.com/docs/installation&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/getting-started" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/getting-started&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/use-cases/websites" rel="noopener noreferrer"&gt;https://bref.sh/docs/use-cases/websites&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;&lt;a href="https://github.com/brefphp/bref/releases/tag/0.5.20" rel="noopener noreferrer"&gt;https://github.com/brefphp/bref/releases/tag/0.5.20&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;&lt;a href="https://github.com/getlift/lift/blob/master/docs/server-side-website.md" rel="noopener noreferrer"&gt;https://github.com/getlift/lift/blob/master/docs/server-side-website.md&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/use-cases/http/advanced-use-cases#cold-starts" rel="noopener noreferrer"&gt;https://bref.sh/docs/use-cases/http/advanced-use-cases#cold-starts&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;&lt;a href="https://github.com/aws/aws-sdk-php/tree/master/src/Script/Composer" rel="noopener noreferrer"&gt;https://github.com/aws/aws-sdk-php/tree/master/src/Script/Composer&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn12"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/laravel/octane" rel="noopener noreferrer"&gt;https://bref.sh/docs/laravel/octane&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn13"&gt;
&lt;p&gt;&lt;a href="https://laravel.com/docs/11.x/octane#managing-memory-leaks" rel="noopener noreferrer"&gt;https://laravel.com/docs/11.x/octane#managing-memory-leaks&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn14"&gt;
&lt;p&gt;&lt;a href="https://bref.sh/docs/runtimes/console" rel="noopener noreferrer"&gt;https://bref.sh/docs/runtimes/console&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn15"&gt;
&lt;p&gt;&lt;a href="https://github.com/hschimpf/parallel-sdk" rel="noopener noreferrer"&gt;https://github.com/hschimpf/parallel-sdk&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn16"&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/es_es/AmazonS3/latest/userguide/PresignedUrlUploadObject.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/es_es/AmazonS3/latest/userguide/PresignedUrlUploadObject.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>serverless</category>
      <category>aws</category>
    </item>
    <item>
      <title>Por qué Migré Mi Aplicación Laravel a AWS Serverless (Y Por Qué Podría Ahorrarte Tiempo y Dinero)</title>
      <dc:creator>Hermann D. Schimpf</dc:creator>
      <pubDate>Wed, 29 Jan 2025 00:01:51 +0000</pubDate>
      <link>https://dev.to/hschimpf/por-que-migre-mi-aplicacion-laravel-a-aws-serverless-y-por-que-podria-ahorrarte-tiempo-y-dinero-6hp</link>
      <guid>https://dev.to/hschimpf/por-que-migre-mi-aplicacion-laravel-a-aws-serverless-y-por-que-podria-ahorrarte-tiempo-y-dinero-6hp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;English version &lt;a href="https://dev.to/hschimpf/why-i-migrated-my-laravel-app-to-aws-serverless-and-why-it-could-save-you-time-and-money-dem"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ydzkg9z8tp6oqswgj45.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ydzkg9z8tp6oqswgj45.png" alt="PHP meets serverless" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Spoiler&lt;/strong&gt;: No se trata solo de ahorrar dinero—aunque mi billetera no se queja.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Imagina esto: has creado una brillante aplicación en Laravel—tu obra maestra, una navaja suiza digital con funciones tan útiles que podrían cortar mantequilla... o los comentarios de los usuarios. Pero hay un detalle. Cada mes estás pagando por una instancia EC2 que está tan infrautilizada como una membresía de gimnasio después de enero. Mientras tanto, escalar se siente como intentar aparcar un crucero en medio de un huracán.&lt;/p&gt;

&lt;p&gt;¿Te ha pasado? A mí también.&lt;/p&gt;

&lt;p&gt;Hace tres años, hice algo que la mayoría de los desarrolladores llamarían una locura: desplegué PHP en AWS Lambda. “¿PHP? ¿En serverless? ¡Eso es como poner piña en la pizza!”, decían.&lt;/p&gt;

&lt;p&gt;Pero aquí estoy, tres años después, comiendo mi pizza con piña con orgullo. Déjame contarte por qué Laravel en serverless es la actualización en la nube que no sabías que necesitabas.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. El Problema del Hosting Tradicional con Laravel
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;(ó: Por qué mis instancias EC2 estaban teniendo una crisis existencial)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Antes de serverless, mi aplicación Laravel vivía en EC2. Para los no iniciados, EC2 es la versión de Amazon de un servidor privado virtual, donde alquilas una parte de una máquina para ejecutar tu código. Suena bien, ¿verdad? Hasta que la realidad te golpea más fuerte que un &lt;code&gt;composer update&lt;/code&gt; rebelde.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Primero está: El Costo de Existir
&lt;/h3&gt;

&lt;p&gt;Ejecutar una instancia EC2 es como tener un Tesla que dejas encendido las 24 horas del día, solo por si acaso quieres conducir. Mi aplicación no siempre estaba ocupada, pero eso no detenía el contador. Entre instancias EC2, balanceadores de carga y almacenamiento compartido, gastaba alrededor de $110/mes en un stack de servidores que pasaba la mayor parte del tiempo inactivo. ¿Mi billetera? Hundida como el Titanic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Lo sé, no es mucho en el gran esquema de las cosas, pero como desarrollador/emprendedor en solitario, cada dólar cuenta.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) Después: Las Pesadillas de Escalado
&lt;/h3&gt;

&lt;p&gt;Las instancias EC2 son como ese amigo que reacciona de forma exagerada a todo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Pico de tráfico? &lt;em&gt;"¡Me voy a caer ahora, gracias!"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;¿No hay tráfico? &lt;em&gt;"¡Igual seguiré quemando tu dinero!"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gestionar el autoescalado se sentía como enseñar a un pez a hacer malabares—es posible, pero ¿a qué costo? Ajustar manualmente los grupos de escalado, configurar balanceadores de carga y rezar para no provisionar de más se sentía como un segundo trabajo al que nunca me postulé.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Y finalmente: DevOps, el Interno No Pagado
&lt;/h3&gt;

&lt;p&gt;Nadie me dijo que el desarrollo con Laravel venía con un lado de responsabilidades de sysadmin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aplicar parches de seguridad.&lt;/li&gt;
&lt;li&gt;Depurar configuraciones de nginx/apache a las 3 AM.&lt;/li&gt;
&lt;li&gt;Susurrar palabras bonitas a los comandos &lt;code&gt;sudo&lt;/code&gt;, esperando que funcionaran esta vez.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No me inscribí para esta vida.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Fue entonces cuando comencé a explorar alternativas, y serverless se destacó como la solución perfecta para resolver estos dolores de cabeza.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. AWS Serverless: El Resurgimiento de PHP en la Nube
&lt;/h2&gt;

&lt;p&gt;Aclaremos un mito: serverless no significa "sin servidores". Solo significa que los servidores son el problema de otra persona. En este caso, AWS hace el trabajo pesado mientras yo me concentro en lo que realmente disfruto: programar.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Lambda: El Mago Impulsado por Eventos
&lt;/h3&gt;

&lt;p&gt;AWS Lambda es como un superhéroe que solo aparece cuando lo necesitas. Ejecuta tu código en respuesta a eventos—solicitudes HTTP, mensajes de SQS, tareas programadas, tu nómbralo. Y cuando el trabajo termina, desaparece más rápido que una pizza gratis en una reunión de desarrolladores.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sin costos inactivos&lt;/strong&gt;: Solo pagas por el tiempo de ejecución (medido en milisegundos).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Magia de escalado automático&lt;/strong&gt;: ¿Recibiste un pico de 100,000 solicitudes? Lambda las maneja sin sudar (ni vaciar tu cuenta bancaria).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless por diseño&lt;/strong&gt;: Es como un borrón y cuenta nueva cada vez, un diseño que te obliga a pensar de forma modular.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  b) Servicios Administrados: Los Héroes Anónimos
&lt;/h3&gt;

&lt;p&gt;Serverless no es solo Lambda—es un ecosistema. AWS reemplaza tu infraestructura DIY (hazlo tú mismo) con servicios administrados que "simplemente funcionan":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base de datos&lt;/strong&gt;: Opciones como Aurora Serverless (MySQL/Postgres) para los amantes de SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt;: Almacena tus archivos sin preocuparte por quedarte sin espacio en disco.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQS&lt;/strong&gt;: Desacopla trabajos de larga duración y procésalos de forma asíncrona.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  c) La Paradoja de PHP
&lt;/h3&gt;

&lt;p&gt;Debo admitirlo: PHP no &lt;em&gt;nació&lt;/em&gt; para serverless. Es como pedirle a un pez que trepe un árbol—se quejará, pero eventualmente lo hará. Laravel, tradicionalmente dependiente de PHP-FPM, necesitó algunos ajustes para prosperar en el mundo efímero de Lambda:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sesiones&lt;/strong&gt;: Muévelas a una base de datos externa, como MySQL o Redis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Almacenamiento de archivos&lt;/strong&gt;: Redirige todas las operaciones de almacenamiento a S3, usando el facade &lt;code&gt;Storage&lt;/code&gt; de Laravel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gestión de colas&lt;/strong&gt;: Configura SQS como el controlador predeterminado para la ejecución de tareas asíncronas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching&lt;/strong&gt;: Aprovecha servicios externos como Redis o DynamoDB en lugar de almacenamiento local.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimización del tiempo de arranque&lt;/strong&gt;: Minimiza los inicios en frío (cold starts) recortando lo innecesario (dependencias no utilizadas).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variables de entorno&lt;/strong&gt;: Reemplaza los archivos &lt;code&gt;.env&lt;/code&gt; con AWS Secrets Manager o Parameter Store para una gestión centralizada y segura de la configuración.&lt;/li&gt;
&lt;/ul&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Recuerda que serverless no se trata solo de reemplazar servidores por funciones Lambda. Se trata de repensar tu arquitectura—dejando que AWS maneje los puntos de dolor operativos mientras tú te concentras en construir.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Cómo Serverless Desbloquea Todo el Potencial de Laravel
&lt;/h2&gt;

&lt;p&gt;Entonces, ¿Laravel en serverless realmente cumple sus promesas?&lt;/p&gt;

&lt;p&gt;Serverless no es solo una palabra de moda, es un cambio transformador. La belleza de Laravel en serverless radica en su capacidad para resolver los puntos débiles del alojamiento tradicional, al mismo tiempo que permite soluciones más rápidas, escalables y rentables. Pero la verdadera magia ocurre cuando profundizas en cómo se combinan estos beneficios. Vamos a desglosarlo.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Cold Starts: Separando el Mito de la Realidad
&lt;/h3&gt;

&lt;p&gt;Los &lt;em&gt;cold starts&lt;/em&gt; ocurren cuando Lambda inicializa una nueva instancia. Piensa en ello como si PHP despertara de una siesta. Los críticos los tratan como si fueran el apocalipsis, pero son manejables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Realidad&lt;/strong&gt;: Los &lt;em&gt;cold starts&lt;/em&gt; típicos con PHP + Laravel rondan los ~3-5 segundos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soluciones&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel Octane&lt;/strong&gt;: Mantiene la aplicación viva entre solicitudes, reduciendo los tiempos de inicio. Las solicitudes posteriores se procesan en ~200ms o menos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrencia aprovisionada&lt;/strong&gt;: AWS pre-calienta instancias para endpoints críticos &lt;em&gt;(cuesta extra, pero vale la pena para puntos clave)&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Para la mayoría de las aplicaciones, los retrasos menores a 3 segundos durante el tráfico bajo son aceptables. La mayoría de los usuarios no notará un &lt;em&gt;cold start&lt;/em&gt;, especialmente durante cargas máximas de tráfico cuando Lambda permanece "caliente".&lt;/p&gt;

&lt;h3&gt;
  
  
  b) Escalado Sin Dolor
&lt;/h3&gt;

&lt;p&gt;El escalado en el hosting tradicional suele parecer una batalla interminable. Con serverless, el escalado se vuelve muy sencillo: ya no es necesario ajustar las reglas de autoescalado ni cruzar los dedos durante un aumento repentino de tráfico. AWS Lambda elimina las conjeturas, escalando horizontalmente por defecto.&lt;/p&gt;

&lt;p&gt;Aquí va un ejemplo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Escenario&lt;/strong&gt;: Tu aplicación se vuelve viral 🚀 yey!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Antigüa configuración con EC2&lt;/strong&gt;: Comienza a experimentar latencia, corres a iniciar sesión en AWS, ajustas manualmente la cantidad de instancias, y rezas por lo mejor 🫠. Ah, y no olvides equilibrar correctamente esas instancias entre las zonas de disponibilidad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nueva configuración con Lambda&lt;/strong&gt;: AWS crea automáticamente tantas instancias como sea necesario, manejando miles de solicitudes concurrentes sin que levantes un dedo. Agarras unas palomitas de maíz y observas las métricas de CloudWatch como si fuera una serie de Netflix 🍿.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esto no es solo comodidad, es tranquilidad. Mientras tú te concentras en celebrar el éxito de tu aplicación, Lambda hace el trabajo pesado. ¿Y lo mejor? Solo pagas por el tiempo de computación que usas, no por la capacidad inactiva que podrías necesitar "por si acaso".&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Eficiencia de Costos: El Jugador Más Valioso
&lt;/h3&gt;

&lt;p&gt;Serverless no solo ahorra dinero, es como tener un buffet de $5 donde solo pagas por lo que comes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mi antigua configuración con EC2: &lt;strong&gt;~$110&lt;/strong&gt;/mes.

&lt;ul&gt;
&lt;li&gt;4x instancias EC2 t3.small: &lt;strong&gt;$60.00&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;1x Balanceador de Carga: &lt;strong&gt;$16.40&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;1x EBS (almacenamiento compartido entre instancias EC2): &lt;strong&gt;$7.80&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;1x instancia RDS MySQL (db.t4g.medium): &lt;strong&gt;~26.00&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Lambda: &lt;strong&gt;~$34&lt;/strong&gt;/mes &lt;em&gt;(¡un ahorro del 60%!)&lt;/em&gt;.

&lt;ul&gt;
&lt;li&gt;Lambda, API Gateway ~2.5M solicitudes (~500ms / 512MB de memoria) /mes: &lt;strong&gt;$4.80&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Servicios gestionados (S3, SQS, CloudWatch): &lt;strong&gt;~$2.90&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Instancia RDS MySQL (db.t4g.medium): &lt;strong&gt;~26.00&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Recurso&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Costo EC2&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Costo Lambda&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Computación&lt;/td&gt;
&lt;td&gt;$60.00&lt;/td&gt;
&lt;td&gt;$4.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Red (LB, API Gateway)&lt;/td&gt;
&lt;td&gt;$16.40&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Almacenamiento&lt;/td&gt;
&lt;td&gt;$7.80&lt;/td&gt;
&lt;td&gt;$2.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base de datos&lt;/td&gt;
&lt;td&gt;$26.00&lt;/td&gt;
&lt;td&gt;$26.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TOTAL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$110.20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$33.70&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;En resumen, serverless no solo ahorra dinero, libera ancho de banda mental. Cuantos menos recursos desperdicie preocupándome por el sobreaprovisionamiento, más puedo concentrarme en construir algo increíble.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Hasta ese momento, todavía utilizaba una instancia MySQL como motor de base de datos. Publicaciones posteriores explorarán cómo migrar a DynamoDB para reducir aún más los costos.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  d) Libertad de Mantenimiento: Di Adiós a las Pesadillas Operativas
&lt;/h3&gt;

&lt;p&gt;Serverless me liberó de las cadenas del mantenimiento de servidores. Aquí está cómo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No más actualizaciones manuales&lt;/strong&gt;: AWS maneja parches de seguridad, actualizaciones del sistema operativo y mejoras de tiempo de ejecución, lo que significa que siempre estás utilizando una infraestructura segura y actualizada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuraciones simplificadas&lt;/strong&gt;: Con servicios como API Gateway y S3, la complejidad de gestionar configuraciones nginx y despliegues personalizados se convierte en cosa del pasado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacidad elástica&lt;/strong&gt;: Olvídate de pagar de más por recursos de servidor no utilizados o de apresurarte a provisionar más durante picos de tráfico. Lambda escala automáticamente para satisfacer la demanda y deja de facturar cuando está inactivo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enfócate en las características, no en apagar incendios&lt;/strong&gt;: El tiempo que antes dedicaba a aplicar parches o a depurar problemas en producción ahora lo invierto en crear características y mejorar la experiencia del usuario.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Serverless no solo reduce el mantenimiento, elimina las distracciones operativas que te impiden programar.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Pero, ¿Es Laravel Serverless para Todos?
&lt;/h2&gt;

&lt;p&gt;Por muy revolucionario que sea Laravel en serverless, no es una solución universal. Para algunas aplicaciones, la naturaleza stateless y orientada a eventos de serverless puede parecer un sueño hecho realidad. Para otras, puede parecer como tratar de encajar un cuadrado en un círculo. Antes de subirte al tren de serverless, retrocedamos un poco y evaluemos si es adecuado para tu proyecto.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) La Naturaleza Stateless: Una Espada de Doble Filo
&lt;/h3&gt;

&lt;p&gt;Laravel adora las operaciones que guardan información entre interacciones, como almacenar archivos localmente y guardar sesiones en el sistema de archivos. Para pasar a serverless, debes cambiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sesiones&lt;/strong&gt;: Usa bases de datos (MySQL/Postgres) o Redis; nada más de depender del sistema de archivos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archivos&lt;/strong&gt;: Redirige las subidas de archivos a S3, o evita completamente pasar por Laravel y utiliza URL pre-firmadas de S3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs&lt;/strong&gt;: Configura Laravel para transmitirlos a CloudWatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuración&lt;/strong&gt;: Mueve las variables &lt;code&gt;.env&lt;/code&gt; a AWS Secrets Manager o Parameter Store para una gestión centralizada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Colas&lt;/strong&gt;: Migra los trabajos a AWS SQS para manejar colas y mensajes de manera escalable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  b) Consideraciones Sobre la Dependencia del Proveedor (&lt;em&gt;Vendor Lock-In&lt;/em&gt;)
&lt;/h3&gt;

&lt;p&gt;Los servicios de AWS son mágicos, pero también son propietarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Migrar de SQS a colas Redis? Prepárate para reescribir código.&lt;/li&gt;
&lt;li&gt;¿Quieres pasar de Lambda a Docker? Tómate un café: será una noche larga.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  c) Cuándo No Optar por Serverless
&lt;/h3&gt;

&lt;p&gt;Serverless no es una bala de plata para todas las cargas de trabajo. Debes evitarlo si:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Necesitas &lt;strong&gt;WebSockets&lt;/strong&gt;: Aunque se puede lograr con servicios como API Gateway + WebSocket APIs o herramientas de terceros como Ably, añade complejidad.&lt;/li&gt;
&lt;li&gt;Tu aplicación tiene &lt;strong&gt;cargas computacionales pesadas&lt;/strong&gt;: Tareas como inferencia de IA/ML o codificación de video pueden alcanzar el límite de tiempo de 15 minutos de Lambda.&lt;/li&gt;
&lt;li&gt;Dependes de &lt;strong&gt;servicios con estado&lt;/strong&gt;: Las aplicaciones que asumen un disco persistente o un estado del servidor pueden ser costosas de refactorizar.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. ¿Qué Sigue?
&lt;/h2&gt;

&lt;p&gt;Laravel en serverless tiene el potencial de transformar la forma en que construyes y despliegas aplicaciones, pero la verdadera magia está en la implementación. ¿Estás listo para dar el salto y darle a tu aplicación Laravel el tratamiento serverless? Mantente atento a la Parte 2, donde te guiaré a través de los pasos exactos para llevar esta arquitectura a la vida.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Una pregunta para ti&lt;/strong&gt;: ¿Cuál es tu mayor temor con serverless? ¡Compártelo abajo y abordaré los 3 principales en la Parte 2!&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>serverless</category>
      <category>aws</category>
    </item>
    <item>
      <title>Why I Migrated My Laravel App to AWS Serverless (And Why It Could Save You Time and Money)</title>
      <dc:creator>Hermann D. Schimpf</dc:creator>
      <pubDate>Tue, 28 Jan 2025 03:03:21 +0000</pubDate>
      <link>https://dev.to/hschimpf/why-i-migrated-my-laravel-app-to-aws-serverless-and-why-it-could-save-you-time-and-money-dem</link>
      <guid>https://dev.to/hschimpf/why-i-migrated-my-laravel-app-to-aws-serverless-and-why-it-could-save-you-time-and-money-dem</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Versión en español &lt;a href="https://dev.to/hschimpf/por-que-migre-mi-aplicacion-laravel-a-aws-serverless-y-por-que-podria-ahorrarte-tiempo-y-dinero-6hp"&gt;aquí&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ydzkg9z8tp6oqswgj45.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ydzkg9z8tp6oqswgj45.png" alt="PHP meets serverless" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Spoiler&lt;/strong&gt;: It’s not just about saving money—though my wallet isn’t complaining.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Picture this: you've built a shiny Laravel app—your magnum opus, a digital Swiss Army knife with features so sharp they could cut through butter... or user feedback. But there's a catch. Every month, you're stuck paying for an EC2 instance that’s as underutilized as a gym membership after January. Meanwhile, scaling feels like trying to parallel park a cruise ship during a hurricane.&lt;/p&gt;

&lt;p&gt;Been there? I have too.&lt;/p&gt;

&lt;p&gt;Three years ago, I did something that most developers would call madness: I deployed PHP on AWS Lambda. "PHP? On serverless? That's like putting pineapple on pizza!" they said.&lt;/p&gt;

&lt;p&gt;But here I am, three years later, eating my pineapple pizza with pride. Let me tell you why serverless Laravel is the cloud upgrade you didn't know you needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Problem with Traditional Laravel Hosting
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;(or: Why my EC2 instances were having an existential crisis)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before serverless, my Laravel app lived on EC2. For the uninitiated, EC2 is Amazon’s version of a virtual private server, where you rent a slice of a machine to run your code. Sounds nice, right? Until reality hits you harder than a rogue &lt;code&gt;composer update&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) First there is: The Cost of Breathing
&lt;/h3&gt;

&lt;p&gt;Running an EC2 instance is like owning a Tesla you keep running 24/7, just in case you feel like driving. My app wasn’t always busy, but that didn’t stop the meter from ticking. Between EC2 instances, load balancers, and shared storage, I was shelling out around $110/month for a server stack that spent most of its time idling. My wallet? Doing a Titanic impression.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I know, it’s not much in the grand scheme of things, but as a solo developer/entrepreneur, every dollar counts.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) Then is: The Scaling Nightmares
&lt;/h3&gt;

&lt;p&gt;EC2 instances are like that one friend who overreacts to everything.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Traffic spike? &lt;em&gt;"I'll crash now, thanks!"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Traffic hush? &lt;em&gt;"I’ll keep burning your money anyway!"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Managing autoscaling felt like teaching a fish to juggle—it's possible, but at what cost? Manually tweaking scaling groups, configuring load balancers, and praying I didn’t overprovision felt like a second job I never applied for.&lt;/p&gt;

&lt;h3&gt;
  
  
  c) And lastly DevOps: The Unpaid Intern
&lt;/h3&gt;

&lt;p&gt;Nobody told me Laravel development came with a side of sysadmin responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Applying security patches.&lt;/li&gt;
&lt;li&gt;Debugging nginx/apache configs at 3 AM.&lt;/li&gt;
&lt;li&gt;Whispering sweet nothings to &lt;code&gt;sudo&lt;/code&gt; commands, hoping they’d work this time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn't sign up for this life.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;That’s when I started exploring alternatives, and serverless stood out as the perfect fit for solving these headaches.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. AWS Serverless: PHP’s Cloud Glow-Up
&lt;/h2&gt;

&lt;p&gt;Let’s clear up a myth: serverless doesn't mean "no servers". It just means the servers are someone else's problem. In this case, AWS does the heavy lifting while I focus on what I actually enjoy—coding.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Lambda: The Event-Driven Wizard
&lt;/h3&gt;

&lt;p&gt;AWS Lambda is like a superhero that only shows up when you need it. It executes your code in response to events—HTTP requests, SQS messages, cron jobs, you name it. And when the job's done, it disappears faster than a free pizza at a dev meetup.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No idle costs&lt;/strong&gt;: Pay only for execution time (measured in milliseconds).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-scaling magic&lt;/strong&gt;: Got a 100,000 requests spike? Lambda handles them without breaking a sweat (or your bank account).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless by design&lt;/strong&gt;: It's like a clean slate every time—a design that forces you to think modularly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  b) Managed Services: The Unsung Heroes
&lt;/h3&gt;

&lt;p&gt;Serverless isn't just Lambda—it's an ecosystem. AWS replaces your DIY infrastructure with managed services that "just work":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: Serverless-friendly options like Aurora Serverless (MySQL/Postgres) for SQL lovers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt;: Store your files without worrying about running out of disk space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQS&lt;/strong&gt;: Decouple long-running jobs and process them asynchronously.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  c) The PHP Paradox
&lt;/h3&gt;

&lt;p&gt;I have to admit it: PHP wasn't &lt;em&gt;born&lt;/em&gt; for serverless. It's like asking a fish to climb a tree—it'll complain, but it'll eventually get there. Laravel, traditionally reliant on PHP-FPM, needed some love to thrive in Lambda's ephemeral world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sessions&lt;/strong&gt;: Move them to an external database, like MySQL or Redis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File storage&lt;/strong&gt;: Reroute all storage operations to S3, using Laravel’s &lt;code&gt;Storage&lt;/code&gt; facade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue handling&lt;/strong&gt;: Set up SQS as the default queue driver for async task execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching&lt;/strong&gt;: Leverage external services like Redis or DynamoDB instead of local storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boot time optimization&lt;/strong&gt;: Minimize cold starts by trimming fat (unused dependencies).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt;: Replace &lt;code&gt;.env&lt;/code&gt; files with AWS Secrets Manager or Parameter Store for secure and centralized configuration management.&lt;/li&gt;
&lt;/ul&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Keep in mind that serverless isn't just about swapping out servers for Lambda functions. It’s about rethinking your architecture—letting AWS handle the operational pain points while you focus on building.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. How Serverless Unlocks Laravel’s Full Potential
&lt;/h2&gt;

&lt;p&gt;So, does serverless Laravel actually deliver on its promises?&lt;/p&gt;

&lt;p&gt;Serverless isn't just a buzzword—it's a game-changer. The beauty of serverless Laravel lies in its ability to solve the pain points of traditional hosting while enabling faster, more scalable, and cost-effective solutions. But the real magic happens when you dive into how these benefits come together. Let’s break it down.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Cold Starts: Separating Fact from Fiction
&lt;/h3&gt;

&lt;p&gt;Cold starts occur when Lambda initializes a new instance. Think of it as PHP waking up from a nap. Critics treat them like the apocalypse, but they’re manageable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reality&lt;/strong&gt;: Typical cold starts with PHP + Laravel are ~3-5 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solutions&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel Octane&lt;/strong&gt;: Keeps the app alive between requests, reducing boot times. The subsequent requests are processed in ~200ms or less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provisioned Concurrency&lt;/strong&gt;: AWS pre-warms instances for critical endpoints &lt;em&gt;(costs extra, but worth it for critical endpoints)&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;For most apps, sub-3s delays during low traffic are acceptable. Most users won't notice a cold start, especially during peak traffic loads when Lambda stays warm.&lt;/p&gt;

&lt;h3&gt;
  
  
  b) Scaling Without Tears
&lt;/h3&gt;

&lt;p&gt;Scaling in traditional hosting often feels like fighting a never-ending battle. With serverless, scaling becomes effortless—no more tweaking auto-scaling rules or crossing your fingers during a traffic surge. AWS Lambda eliminates the guesswork, scaling horizontally by default.&lt;/p&gt;

&lt;p&gt;Here's an example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scenario&lt;/strong&gt;: Your app goes viral 🚀 yey!.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old EC2 Setup&lt;/strong&gt;: It starts getting latency, you scramble to log into AWS, manually adjust instance counts, and hope for the best 🫠. Oh, and don't forget to balance those instances properly across availability zones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New Lambda Setup&lt;/strong&gt;: AWS automatically spins up as many instances as needed, handling thousands of concurrent requests without you lifting a finger. You grab popcorn and watch CloudWatch metrics like it’s a Netflix series 🍿.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn’t just convenience—it’s peace of mind. While you focus on celebrating your app's success, Lambda does the heavy lifting. And the best part? You’re billed only for the compute time you use, not for the idle capacity you might need "just in case".&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Cost Efficiency: The Real MVP
&lt;/h3&gt;

&lt;p&gt;Serverless doesn't just save money—it’s like having a $5 buffet where you only pay for what you eat.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My old EC2 setup: &lt;strong&gt;~$110&lt;/strong&gt; /month.

&lt;ul&gt;
&lt;li&gt;4x EC2 t3.small instances: &lt;strong&gt;$60.00&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;1x Load Balancer: &lt;strong&gt;$16.40&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;1x EBS (shared storage between EC2 instances): &lt;strong&gt;$7.80&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;1x RDS MySQL instance (db.t4g.medium): &lt;strong&gt;~26.00&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Lambda: &lt;strong&gt;~$34&lt;/strong&gt; /month &lt;em&gt;(a 60% saving!)&lt;/em&gt;.

&lt;ul&gt;
&lt;li&gt;Lambda, API Gateway ~2.5M requests (~500ms / 512MB memory) /month: &lt;strong&gt;$4.80&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Managed services (S3, SQS, CloudWatch): &lt;strong&gt;~$2.90&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;RDS MySQL instance (db.t4g.medium): &lt;strong&gt;~26.00&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Resource&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;EC2 Cost&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Lambda Cost&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;$60.00&lt;/td&gt;
&lt;td&gt;$4.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Networking (LB, API Gateway)&lt;/td&gt;
&lt;td&gt;$16.40&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;$7.80&lt;/td&gt;
&lt;td&gt;$2.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;$26.00&lt;/td&gt;
&lt;td&gt;$26.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TOTAL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$110.20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$33.70&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In short, serverless doesn’t just save money—it frees up mental bandwidth. The fewer resources I waste worrying about over-provisioning, the more I can focus on building something amazing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;At that time, I was still using a MySQL instance as database engine. Later posts will explore migrating the database engine to DynamoDB to cut costs further.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  d) Maintenance Freedom: Say Goodbye to Ops Nightmares
&lt;/h3&gt;

&lt;p&gt;Serverless freed me from the shackles of server maintenance. Here’s how:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No more manual updates&lt;/strong&gt;: AWS handles server patching, operating system updates, and runtime upgrades, meaning you’re always running on secure, up-to-date infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplified configurations&lt;/strong&gt;: With services like API Gateway and S3, the complexity of managing nginx configs and custom deployments becomes a thing of the past.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elastic capacity&lt;/strong&gt;: Forget overpaying for unused server resources or scrambling to provision more during traffic spikes. Lambda automatically scales to meet demand and stops billing when idle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus on features, not firefighting&lt;/strong&gt;: The time I once spent applying patches or debugging production issues now goes into building features and improving user experience.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Serverless doesn't just reduce maintenance; it removes the operational distractions that keep you from coding.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. But, "Is Serverless Laravel for Everyone?"
&lt;/h2&gt;

&lt;p&gt;As revolutionary as serverless Laravel is, it's not a one-size-fits-all solution. For some apps, the stateless, event-driven nature of serverless can feel like a dream come true. For others, it might feel like trying to fit a square peg into a round hole. Before you jump on the serverless bandwagon, let’s take a step back and evaluate whether it’s the right fit for your project.&lt;/p&gt;

&lt;h3&gt;
  
  
  a) Statelessness: The Double-Edged Sword
&lt;/h3&gt;

&lt;p&gt;Laravel loves stateful operations like storing files locally and caching sessions in the filesystem. To go serverless you must change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sessions&lt;/strong&gt;: Use databases (MySQL/Postgres), or Redis—no more filesystem reliance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files&lt;/strong&gt;: Reroute uploads to S3, or bypass Laravel entirely via pre-signed S3 URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs&lt;/strong&gt;: Configure Laravel to stream them to CloudWatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration&lt;/strong&gt;: Move &lt;code&gt;.env&lt;/code&gt; variables to AWS Secrets Manager or Parameter Store for centralized management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queues&lt;/strong&gt;: Migrate jobs to AWS SQS for scalable queueing and message handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  b) Vendor Lock-In Considerations
&lt;/h3&gt;

&lt;p&gt;AWS services are magical, but they’re also proprietary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrating from SQS to Redis queues? Have fun rewriting code.&lt;/li&gt;
&lt;li&gt;Want to move from Lambda to Docker? Grab a coffee—it’ll be a long night.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  c) When Not to Go Serverless
&lt;/h3&gt;

&lt;p&gt;Serverless isn’t a silver bullet for every workload, you should consider avoiding it if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need &lt;strong&gt;WebSockets&lt;/strong&gt;: While achievable with services like API Gateway + WebSocket APIs or third-party tools like Ably, it adds complexity.&lt;/li&gt;
&lt;li&gt;Your app has &lt;strong&gt;heavy computational workloads&lt;/strong&gt;: Tasks like AI/ML inference or video encoding may hit Lambda’s 15-minute timeout.&lt;/li&gt;
&lt;li&gt;You rely on &lt;strong&gt;stateful services&lt;/strong&gt;: Applications that assume a persistent disk or server state can be costly to refactor.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. What’s Next?
&lt;/h2&gt;

&lt;p&gt;Serverless Laravel has the potential to transform the way you build and deploy applications, but the real magic is in the implementation. Are you ready to make the leap and give your Laravel app the serverless treatment? Stay tuned for Part 2, where I'll walk you through the exact steps to bring this architecture to life.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Question for you&lt;/strong&gt;: What’s your biggest serverless fear? Share below—I’ll tackle the top 3 in Part 2!&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>serverless</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
