You're about to launch your new app or API, or even just a big refactor of your current project.
Will it stand up and deliver when you put it in production? Or will it wither and collapse?
How do you know? Well, you would test that, of course. We have Anthony Shaw back on the podcast
to dive into a wide range of tools and techniques for performance and load testing of web apps.
This is Talk Python To Me, episode 479, recorded August 8th, 2024.
Are you ready for your host, please?
You're listening to Michael Kennedy on Talk Python to Me.
Live from Portland, Oregon, and this segment was made with Python.
Welcome to Talk Python to Me, a weekly podcast on Python.
This is your host, Michael Kennedy.
Follow me on Mastodon, where I'm @mkennedy, and follow the podcast using @talkpython,
both accounts over at fosstodon.org, and keep up with the show and listen to over nine years of episodes at talkpython.fm.
If you want to be part of our live episodes, you can find the live streams over on YouTube.
Subscribe to our YouTube channel over at talkpython.fm/youtube and get notified about upcoming shows.
This episode is brought to you by Sentry.
Don't let those errors go unnoticed.
Use Sentry like we do here at Talk Python.
Sign up at talkpython.fm/sentry.
And it's brought to you by WorkOS.
If you're building a B2B SaaS app at some point, your customers will start asking for enterprise features like SAML authentication,
SKIM provisioning, audit logs, and fine-grained authorization.
WorkOS helps ship enterprise features on day one without slowing down your core product development.
Find out more at talkpython.fm/workos.
Anthony, welcome back to Talk Python to me again, again and again.
So awesome to have you back.
Always good.
Yeah, yeah.
Great to be here.
Good to catch up.
Good to have you on the show.
I know we've got a really interesting topic to talk about.
Performance, load testing.
How do you know if your website or your API is going to work when you ship it to the world in a real world way?
Not just how many requests per second can it take, but a real use case, as I know.
You're going to tell us all about, so that's going to be awesome.
Before we do, you know, just a quick introduction to who you are and maybe for most 98% of the world who knows you, what have you been up to?
Yeah, so I am the Python advocacy lead at Microsoft.
I do a bunch of open source work, maintain some projects and stuff like that.
Wrote a book on Python, the Python compiler called CPython Internals.
But these days, I'm mostly known as the person that created VS Code Pets, which was a bit of fun, but has become the most popular piece of software I've ever written.
So that's been interesting.
It's now got over a million active users.
I'm not sure how that works.
Careful what you create.
You might get known for it, you know?
Yeah, I know.
Yeah, it's interesting when you go to conferences and stuff now and I'm like, oh, I work on this project and this project.
And then you'd mention the pets thing and they're like, oh, you're the pets person.
Oh, you're the developer.
I'm like, you spent a year writing a deep book on the internals of CPython and it's runtime.
I don't know who you are, but this pets thing is killer, man.
Yeah, there's a cat that runs around in VS Code.
So that's cool.
Can it be a dog as well?
What kind of pets can we have?
Oh, there's everything.
Yeah, it can be a dog.
There's like chickens and turtles and snakes and everything you can think of.
It's a pretty active repository as well.
We get a lot of feature requests for new shades of fur and new pets and new behaviors and stuff like that.
So, yeah, if you haven't checked it out, then check out the VS Code pets extension for VS Code.
Yeah, I installed it for a while.
I had to uninstall it.
It was too much.
If you're one of those people that likes having distractions, then it's helpful.
If you find it hard to have a little thing running around whilst you're trying to code, then it might be a bit much.
A little bit like power mode.
Oh, yeah.
I don't know.
Are you familiar with the power mode?
I used the one in JetBrains.
Yeah, I used the power mode for JetBrains when that was around.
It was pretty cool.
Yeah, if you want maximum distraction for your work.
Yeah, it reminds me of Unreal Tournament, the sort of power modes for that.
That's right.
That's awesome.
Well, let's start off with some stories.
So, I think everyone has a sense of, like, why do you want your website or your API or your microservice thing of API and website, however you combine these things, to work well and understand.
But it's always fun to share some stories.
I know you got some.
Yeah, so we want to talk about load testing.
And I think a fun thing to do with load testing is to reflect on times where people haven't done it the right way and it's caused a really public disaster.
But the analogy that I use is the halftime problem, which I'm English, and so this is mostly a soccer thing.
But we've got really big soccer games.
And it would be the same, I guess, with the Super Bowl.
Yeah, I was just thinking the American Super Bowl, football Super Bowl.
It's got to be.
Yeah, but the difference is that in soccer, at 45 minutes, you've got a 15-minute break.
Whereas in the Super Bowl, every 15 minutes, you've got a 15-minute break.
Well, and it's just loaded with commercials.
So, you've got, like, every five minutes, there's, like, oh, here's two more commercial breaks.
Where soccer is a flowing game, there's, like, no break until halftime.
That's the difference, I think.
So, what happens with a big game, so if it's, like, a FA Cup final or, like, a, you know, like, a Champions League final or something, then you basically
have 100 million people all stop watching the TV at the same time for 15 minutes, go to the
kitchen, turn on the kettle, make a cup of tea, go to the bathroom.
And so, the electricity providers actually have to plan for this because the kettle uses, like,
a couple of kilowatts of energy.
Especially in the UK where it's 240.
Yeah, exactly.
Yeah.
And all of a sudden, you've got, like, tens of millions of people all switching on their two kilowatt kettles at the same time.
So, the electricity providers basically have to plan around the soccer games so that they get this massive spike in load in their grid.
So, they actually do look at the sports schedules to basically plan around this.
Especially if there's something like a World Cup or something like that.
And this is kind of like a load testing thing where you kind of need to plan ahead and think about what the load on the system is going to be.
And is it a spike in traffic or is it, like, distributed?
And I've definitely seen a couple of occasions where it's gone a bit wrong.
So, here in Australia, we had a census maybe eight years ago.
And normally, it's a paper census.
You fill in the form, say who you are, what you do.
But this time, they wanted to do it online as well.
And they really encouraged people to use the online version.
And they set up the system, set up the website, and said, okay, everyone can fill in the census.
There's only 20-something million people in Australia.
It's not a very big population.
But on the last night before the census was due, it crashed.
Because everybody logged on at the last minute and tried to fill it in.
And they hadn't tested it properly.
And it just wasn't ready for the load.
And then the postmortem, they said, oh, we did the load testing.
And we tested it.
And it seemed fine.
But we expected everybody to fill in the census over the six months they had to do it.
We didn't think that people would leave it to the last minute,
which is more like, well, what did you expect to happen?
This is human nature that you're going to put it off until the last possible moment.
Yeah, I can only see two spikes.
A small spike at the beginning and a huge spike at the end.
And like nothing in the middle.
Yeah, exactly.
So they had to kind of delay the deadline and then provision new infrastructure and stuff like that.
So yeah, it was interesting.
But it kind of highlighted how you need to think about load testing.
Yeah, that's nuts.
And I guess the mode before, the style before was kind of a distributed queuing system using paper envelopes where the queuing literally was physical.
And then it would be processed at the rate at which the system can handle it, right?
Which, you know, however fast you can get them in based on paper.
But then you try to turn it into an interactive system.
And oh, no.
Yeah, yeah, exactly.
You know, I have another example to share.
Like we've seen websites that, you know, fall down like this.
It fell down.
And when something has to be done, I think it's probably really tricky to communicate.
Let's say 10 million people tried to fill it out that day and it crashed on them.
Well, how do they know that the deadline's extended and how do they come back, right?
Like there's, it creates this cascading chain of really challenging problems all of a sudden to deal with it.
You know, we had the healthcare Obamacare stuff in the U.S. where soon as that thing opened, it just completely died.
And for weeks, people couldn't get insurance, which was bad.
But that's not the story I want to share.
I want to share a different one.
There was this app, this person who was frustrated about, they're an artist, like a photographer, I believe.
And they're frustrated about LLMs and diffusion models taking everyone's art and generating new art from it.
And kind of, you know, I guess it's up to people's opinion whether or not it counts as stealing or copyright theft or it's fair use or whatever.
But this person wasn't a fan of that.
So they came up with a web app based on serverless that would look at stuff and say, this is real art.
This is AI generated art.
And it's not exactly the same, but they ended up with a $96,000 Vercel bill.
Because they didn't really take into account how much it's going to cost per request to handle this on their serverless.
It was like 20 cents a user.
And everyone's like, you know, this is right at the height of the LLM boom and all the AI art boom and stuff.
And people just swarm to it.
Yeah, yeah.
So that's a different kind of problem, right?
Yeah, that's the cost issue.
Yeah, there are definitely other ones to keep in mind.
And we mentioned like, you know, the census and the Obamacare thing.
And you might be listening and thinking, well, you know, my website is not going to get, you know, 15 million people logging in all at the same time.
So like, is this really an issue that I need to handle?
And like a few months ago, I was doing a load test of an AI app.
So like a chat app and designed the load test and ran it with 10 users and it had issues.
So like, this doesn't need to be a tens of millions of users problem.
And the issue that it uncovered was like, oh, there's a rate limit on the number of tokens you can send to the LLM.
And by default, it's a really small number.
And if you have 10 people asking questions simultaneously, then that gets exhausted really quickly.
And it just throttles you.
And then like the app just says, oh, rate limit error.
And you just wouldn't have noticed because if you're a single user clicking around, typing in, you know, questions, or even if you've written some fancy like UI testing or something, you're like, oh, we've tested it.
And it works great.
But, you know, you run 10 people using it at the same time.
And that's where you start to notice problems.
I've more than once considered putting some kind of LLM AI augment, you know, rag sort of thing in front of Talk Python because, God, at this point, eight, nine years of full transcripts, human corrected with all sorts of, you know, pretty highly accurate stuff.
It would be awesome to have people have conversations about it.
But I don't know, just the workload.
I can see so many people coming to it and go, that's neat.
I'm going to ask it about my homework.
It has nothing to do with it.
Just using it as like a free AI to just, instead of bothering to download something or use ChatGPT, I'll just use yours.
And I'm like, eh, probably not worthwhile.
All the trouble.
Yeah, you can set them up so that they only answer questions about your own stuff.
They don't act as a fancy front end for ChatGPT.
We've got plenty of demos like that, that you give it your own internal documents and it only answers questions about those.
And you can set it up so that if it doesn't, if it can't figure out a reliable answer, it tells you.
It says, I can't.
Yeah, that's great.
Rather than just making something up.
Yeah, that's really nice.
Yeah, I forgot how closely you guys work with OpenAI and all them, right?
Yeah.
I mean, the compute in Azure to run and train all that stuff is, it's out of control.
Yeah, the GPUs alone are insane.
Yeah, it's, you know, I talked to Mark Rosinovich, a bit of a diversion, but about some of the hardware and some of the data center stuff.
And it's like, we're going to create distributed GPUs that connect back over fiber to the actual hardware.
So you can scale your GPUs and CPUs independently.
There's just all kinds of crazy, interesting stuff there.
But that's a side, a bit of a side note, a bit of a side note.
All right.
So let's talk about basically what do you want to do to design a load test?
Like it's more than just let me write a loop and see how many requests I can make, you know, while true, call, you know, use requests and call this or call this endpoint or something, right?
Yeah, so there's, I guess, what I normally highlight with load testing is that the wrong way to start is to try and work out how many requests per second your server can handle.
That's interesting for throughput, but it's not a good measure of real user traffic.
And the main reason is that every user is unique.
They do different things.
They follow different paths.
They put in different data.
And also when you're running like a benchmark, then it's just it's not waiting between requests.
It's just trying to hammer as much as it can.
So real users pause and read and click and wait.
And there's normally latency.
They don't request the same page over and over and over, right?
They request a blend of pages.
Here they go to the homepage and they do a search and they explore what they find in the search and they go back and do another search and then they go to check out or whatever, right?
And as you're pointing out, they don't just hold down control or command R and just flicker the screen as hard as they can.
They they're they're reading and they're interacting.
And so what you're saying is if you really want to say how many actual users, not just kind of a throughput number, but how many people using the app do you possibly expect it could keep working or right?
You got to factor all these things in, right?
Yeah.
So you got to factor in the randomness of where they go, what they type in the weights between each click or each API request.
And then something else that's really important is that most modern applications is not just the initial HTTP request.
It's the 95 additional requests to all the scripts and the resources and the Ajax calls and everything like that.
So if it's if it's a browser based application, then typically, you know, it's not just the initial request.
It's everything that happens after it.
And I've definitely seen times where people have done a load test and said, oh, yeah, the website runs great.
And then in the app, they had like a JavaScript polar that would like refresh the data or something every minute.
And then they didn't test that.
And then you get thousands of users who leave the tab in their browser and it's just polling in the background.
So you've basically got this continuous stream of traffic to a special API that does like polling.
And they hadn't load tested that and that produced a huge like spike in load and that caused the issue.
It's like 75 percent of the workload is the polling.
Yeah, exactly.
What's your browser?
What's your browser tab story?
Are you a person that just has tons and tons of tabs open?
I don't know.
I try and clean them up as much as possible.
Yeah.
And have like a couple open maybe.
And there's some researching something.
And then when I'm finished, I just do close all tabs.
Yeah, that's me as well.
I think there's a there's a good number of people that just have like 50 tabs open.
They just leave them.
And think about what that does for your website.
If you've got some kind of timer based deal, right?
You got to consider those people that have just left it.
Yeah.
This portion of Talk Python to me is brought to you by Sentry.
Code breaks.
It's a fact of life.
With Sentry, you can fix it faster.
As I've told you all before, we use Sentry on many of our apps and APIs here at Talk Python.
I recently used Sentry to help me track down one of the weirdest bugs I've run into in a long time.
Here's what happened.
When signing up for our mailing list, it would crash under a non-common execution pass, like situations where someone was already subscribed or entered an invalid email address or something like this.
The bizarre part was that our logging of that unusual condition itself was crashing.
How is it possible for our log to crash?
It's basically a glorified print statement.
Well, Sentry to the rescue.
I'm looking at the crash report right now, and I see way more information than you'd expect to find in any log statement.
And because it's production, debuggers are out of the question.
I see the traceback, of course, but also the browser version, client OS, server OS, server OS version, whether it's production or Q&A, the email and name of the person signing up.
That's the person who actually experienced the crash.
Dictionaries of data on the call stack and so much more.
What was the problem?
I initialized the logger with the string info for the level rather than the enumeration dot info, which was an integer based enum.
So the logging statement would crash saying that I could not use less than or equal to between strings and ints.
Crazy town.
But with Sentry, I captured it, fixed it, and I even helped the user who experienced that crash.
Don't fly blind.
Fix code faster with Sentry.
Create your Sentry account now at talkpython.fm/sentry.
And if you sign up with the code TALKPYTHON, all capital, no spaces, it's good for two free months of Sentry's business plan, which will give you up to 20 times as many monthly events as well as other features.
You did mention that the CSS and the, you talked about the AJAX, but I think also things like CSS images, JavaScript files, not JavaScript execution, but just getting the files, right?
Some of these frameworks are a couple hundred megs.
Like if you're not somehow distributing that through a CDN, that could be worth considering as well, I think.
Yeah, it is definitely.
And so in Python, for example, you've got in Django, there's an extension called white noise.
So in Django, you've got static content.
There is a really easy extension you can install because normally you have to kind of configure static and say, okay, my static files are here.
And then you have to set up Nginx or whichever web server you're doing to serve that static content directly.
So as a bit of a workaround, people often install this white noise extension that basically uses Python as their static server.
So like every time you request a CSS or whatever, Python actually is the thing that reads it from disks and serves it back, which is great for like development.
But like you should never use that in production because Python is not a very good CDN.
So yeah, that's kind of with load testing, you would just test the endpoint and say, oh, yeah, it works great.
But then you actually run it in a browser.
And if you're using something like white noise, it's actually creating 10 times, 20 times more load than you expected because it's pulling in all this like CSS and JavaScript and images and stuff like that.
Yeah, it can be out of control.
I ran into an issue with my website where, I don't know, I got tired of being too careful with font awesome fonts.
And I had like missed one.
I'm like, I'll just put the whole thing.
I'll just put the whole CSS file in.
I'm sure it'll be fine.
It wasn't fine.
Like the web app is running at like 10% CPU usage and Nginx trying to serve up all the JavaScript.
Is it like 80?
I'm like, what is it doing?
Like, oh, it was serving up a megabyte size to font stuff.
Every request.
This is not good.
So yeah, in terms of projects that I've worked on in the past, I've worked on load testing some like big campaigns and stuff like that.
Particularly around sports events, awesome television.
Back when people used to watch live television instead of streaming at all, you know, they'd have like a murder mystery or something on a big soap opera.
And so I'd like load test the application so that when everyone wants to find out who murdered the vicar's wife or whatever, then they'd all kind of click on the website at the same time and just trying to load test it.
So some of the things I'd seen were trying to make sure you simulate browser load correctly, trying to distribute traffic in a realistic way.
And then spikes in traffic are basically a different problem.
So the thing we talked about, like the halftime problem where you've got everybody turning on the kettle at the same time, that's a special type of load test.
It's a predictable spike as opposed to just out of the blue spike, right?
That one is predictable, which is really nice.
And then you get things like seasonal traffic.
So for a lot of e-commerce applications and stuff like that, you would expect in the lead up to Christmas or in the Black Friday sale in the US, for example, like Cyber Monday, you know, you'd expect like a spike in traffic for those.
But it will be distributed over a day or two.
So you want to be able to like properly assess those.
Another spike that I think you and I can both relate to is the driver of the day, Formula One.
Oh, right.
You've got 10 laps left within the race.
Everyone now go to this thing and press this button and you have five minutes to do it or something, right?
Like that's going to be a mega spike.
Yeah, there can be some big, big loads in traffic.
Sudden spikes are really hard to handle because you get bottlenecks in the network.
You get bottlenecks in the database.
You get bottlenecks in the web server.
So those are kind of a unique type of problem to test for.
If you're looking more like waves of traffic, so like, oh, you know, traffic would build up during the weekday or the type of application I have actually gets busier at the weekend or in the evenings.
Then those are kind of the ones where you test ramped traffic.
So all these load testing tools have a ramp up time or ramp configuration where certain locusts, for example, you say, how many concurrent users do you want to get to?
So let's say that's like a thousand or something.
And then how much do you want to ramp up?
And you generally should always use a ramp because if you've got a thousand users, unless you've put them all in a room and said, okay, everybody click on the button at the same time.
Like that's not going to happen.
People, people log in gradually over a period of time.
And if you don't use ramping and load testing tools, you actually create, you kind of end up simulating stuff that's not realistic.
So you'd see like a massive spike in response times as the application gets backed up.
But if it's not really realistic that you get those types of spikes, then don't, don't simulate it that way.
We're going to talk about it a little bit, but there's a lot of different layers of caching as well.
And if the app has just come to life, many of those layers of caching are not set up and warmed up.
So that's a big deal.
And it could be as simple as the web app parsing the Jinja or Chameleon or Django template, right?
That's the very first time that can be slow.
But then if you set it up right in production, it will not parse at the second, third and fourth request, right?
So if we talk about Locust as an example, you've got on the screen, I think it's my favorite load testing.
Locust is so good.
It's really flexible and you define the user flows in Python code.
So you write a class that represents a user and then you program in the steps that they would take.
So like, oh, they start on the homepage, then they click on this page, then they click on this page.
And then you can program in like the pauses and the randomness and stuff like that.
And actually set up different types of personas or different types of users.
I think this is a great way of designing a load test because you're thinking about what would the user do rather than like what's the throughput.
So if you set up a Locust test where they start off at a point in your site, if your site has a login, how many of your users would log in?
Because I think it's important to start off.
If this is a new application, it's really hard to know.
So you're going to have to come up with an educated guess or test like a range of parameters.
But if this isn't a website that you've had running for, you know, a year or two, and you've got like a history of traffic, then you can look at it and say, okay, how many people on the homepage are logged in?
Because there's a big difference.
Because when you talk about caching and like CDNs and stuff like that, you know, you can cache the rendered template, but you generally wouldn't cache the whole rendered template if they're logged in.
Because at the top, it might say, you know, hello, Anthony, and you wouldn't want to cache that so that when Mike clicks on the website, it says hello, Anthony.
And he's like, who the hell's that?
That would be very disturbing, wouldn't it?
So there's a balance between caching and serving up dynamic content.
And then also if you're presenting, you know, lists of products or someone's got shopping cart or something like that, obviously that's unique to them.
So you're kind of trying to simulate this as best as possible.
Otherwise, you just create these really optimistic load tests where you're like, oh, we tested the homepage and it can handle 50,000 users.
But in practice, if a thousand of those log in, then the whole thing falls apart because all of a sudden you're not using the cached version of the homepage.
Right.
You know, another example that you brought up is, let's just take Talk Python, right?
It's got coming up on 500 episodes.
If you test and suppose I put really aggressive caching, like on the database results that come back from a single episode for the page and then just render that out of memory, basically, at least just out of the objects that are in memory.
If you do your test to just hit the one, like let's just randomly pick episode 300, hit that one a bunch of times.
But in reality, people are hitting all four or 500 and it's not all cached the same.
Or maybe there's so much memory that each one has to hold that it like putting all 500 in memory and the cache like runs it out of RAM on the server.
All sorts of stuff that happens as you get this kind of dynamic mix, right?
You want to introduce randomness.
So for example, on the login, if you've got a login flow or if you're clicking on a particular product, then try not to hard code which one it is.
If you've got caching, you might not even see the caching either.
Like databases do a lot of caching.
So like if you're always pulling the same thing from the database, the database server is probably going to cache that.
So, you know, it's going to be a lot faster.
So if you can randomize the like the inputs or the pages that you click on or the flows that people take, then that's great.
Locust actually has like an extra when you define a task, which is a decorator on a function.
You can have like how often this happens.
So you can have some tasks which happen more sort of frequently than others.
So you can say, OK, you know, five times more people go to the homepage and then every so often somebody does a search.
But then when you do the search, when you want to low test the search, you know, you want to randomize that a bit more than just always searching for the same thing.
Right, right, right, right.
Yeah.
And just so for people who haven't seen Locust, you create a class, you give it a couple of functions.
And then, for example, to test the homepage, you just literally put the task decorator on the function.
And you say self.client.get slash self.client.get, maybe some additional assets or not.
And then you can even hard code into the class what the domain is or localhost or support or whatever.
And that's it.
And you just you can assign a weight to these tasks like homepage five times more likely than about.
So just put task of five instead of task by itself.
Right.
It's incredible.
Yeah, it's really, really helpful.
And then there's a library.
There's a couple of things you need to keep in mind as well.
And like if the user logs in and then it probably creates some sort of session, whether that's like a cookie or a token or something like that.
So you need to store that somewhere in the class so that subsequent requests use the same login session.
And then also, you know, frameworks like Django and Flask have got cross-site request forgery like protection.
So they generate these CSRF tokens in the forms as well.
So there's normally like a bit involved in getting the cookie or the session ID, getting the CSRF value.
And then like, say, if you're submitting forms or you're like doing a search or something, you need to code a bit in Locust to work around the security controls.
Right.
For example, you might do a request to the page that has the form and then pull back the CRF token from a cookie and then use that as part of the form submission data.
Otherwise, it might just say invalid.
This portion of Talk Bython is brought to you by WorkOS.
If you're building a B2B SaaS app, at some point, your customers will start asking for enterprise features like SAML authentication, skim provisioning, autologs and fine grained authorization.
That's where WorkOS comes in with easy to use APIs that help you ship enterprise features on day one without slowing down your core product development.
Today, some of the fastest growing startups in the world are powered by WorkOS, including ones you probably know, like Perplexity, Vercel and Webflow.
WorkOS also provides a generous free tier of up to one million monthly active users for AuthKit, making it the perfect authentication layer for growing companies.
It comes standard with useful features like RBAC, MFA and bot protection.
If you're currently looking to build SSO for your first enterprise customer, you should consider using WorkOS.
Integrate in minutes and start shipping enterprise plans today.
Just visit talkpython.fm/workos.
The link is in your podcast player show notes.
Thank you to WorkOS for supporting the show.
You know, another one that would be really tricky would be like turnstile or reCAPTCHA.
You're probably basically not getting that.
It's not worth it.
Yeah, you're stuck there.
You've got to fill in those ones.
Just maybe turn it off real quick for your test and then turn it back on.
I don't know.
I mean, you could do it, turn it off in development or something like that, right?
Yeah.
So when I look at this, this locus stuff, I see behind the scenes something like requests or HTTPX,
where all it does is pull back the string of the HTML, or maybe even it just gets the head and actually throws away the content.
I bet it streams it back.
But what I don't imagine it does is it doesn't parse the HTML, realize that it's a view front end,
execute the JavaScript that then has three API calls to more stuff on the back on the server, right?
If it's a rich front end app, it probably doesn't treat it the same if we write it just like this, right?
Yeah.
So the other one I wanted to highlight is an extension to locust so you can connect it with playwrights.
At this thing?
It does HTML parsing.
That's more like looking at the HTML to see if like a particular, you know, like a beautiful soup or responses was the framework for this.
Right.
Make sure timeout does not appear in the text or something like that, right?
Yeah.
Make sure the page doesn't have error in big letters.
That's one thing you can do is checking that the content of the page actually contains the right thing.
And then playwright is a UI, like a web testing tool.
Playwright works really well with pytest.
So I recommend using playwright anyway, if you're running a web application, because you can write pytest tests in playwright.
Even better is if you want to get started with playwright, it has a code generator.
So when you pip install it, you can run it in the code gen mode.
It pops up a browser and then you just go on your website and just click around and then do what you would do normally.
Type in, you know, fill in the forms, click on the buttons.
And then whilst you're doing that in another tab, it actually generates all the Python code for the pytests.
So it basically generates the pytest test code in a separate window automatically as you're clicking around in the browser.
So like in terms of writing UI tests is quite difficult because often you have to be like, OK, how do I find the button?
How do I click on the right button?
How do I find the form?
Especially with JavaScript, because it's not, you know, often things don't have a specific idea or you've got to figure out what selectors to use and stuff like that.
So this makes it a lot easier because you can use the code gen.
So that's a browser test.
So with a load test normally in Locust, you're just making HTTP requests, but you're not actually rendering the page or running the AJAX or the JavaScript code.
Whereas with Playwright, when you run Playwright, it's actually spinning up a browser and then driving the browser from Python.
So you can plug Locust and Playwright together.
There's a Playwright extension for Locust.
So you can say, OK, each load test user, I actually want that to be a browser.
And so when you say I want to test 100 concurrent users, it actually spins up 100 browsers.
In a very, well, Playwright's actually really interesting how it works, but like there's a headless mode for browsers these days.
So it doesn't actually run 100 windows like you can't see them.
I can't use my computer while this is running.
It's just overwhelmed by.
But yeah, I don't recommend running 10,000 concurrents on a single laptop because it's going to run pretty slowly.
It's going to have a bad time.
But that is really important to actually test.
So that will test it.
All the stuff that we've been talking about.
It'll test.
If there's polling, it'll test that.
If the CSS is not delivered over a CDN, it's going to go get that potentially white noise or wherever it's coming from.
It's going to do all the things.
Yeah, the challenge with it is that, you know, like you mentioned, running a browser, even if it's just a tab, uses a lot of resources.
Whereas making a HTTP request using a request or something, you know, it doesn't really need anything.
So you can quite happily in Locust, you know, make 100,000 requests and your local machine that you're testing on will be fine.
It will.
So, yeah, you can actually get low testing as a service.
And the reason you'd probably want to use that is if you're testing a scenario where your local dev environment or your test environment just isn't big enough, where you need to distribute it, then you need more horsepower, basically, to go and run all the requests.
Especially for something like Playwright, you know, where you need a lot of resources for the browser.
Yeah, the little screenshot they have for Locust says 21,400 users are currently accessing the website.
It's like, that's a lot of browser instances.
That's a lot of browser instances.
That looks like an API test, which would be a lot easier to do.
APIs are easy.
Yeah, so we, there's a service called Azure Low Testing.
There is other options as well.
I'm sure AWS has one and Google probably has one as well.
Azure Low Testing, I know.
And we are launching Locust support for that.
At the moment, it supports Jamie to test.
But yeah, we're going to be launching Locust support for that.
I think by the time this episode comes out, it will probably be in public preview.
So I've been using that and testing that.
And the reason you would use it is, like I said, if you can run the Locust test locally on your machine and it runs great and brilliant, but, you know, you can ask us to spin up 50, 100, or even more instances running in parallel.
And so if you want to do a really large scale test, then you can just basically get that from a cloud service provider like Azure.
Yeah, awesome.
I didn't realize you guys were bringing that online.
That's cool.
Yeah, nobody knows about us.
Yeah.
Can you do distributed?
I think you can do some distributed stuff with the Locust even, can't you?
Just, yeah.
Yeah, you can.
But you've got to have your own infrastructure, right?
A lot of it uses SSH and you've got to, like, basically it kind of works over the shell.
Yeah.
So, yeah, we've kind of got our own distribution and, like, coordination system and stuff like that.
And then also when you're running the test, your output, I guess, is what is the response time of the pages?
And, like, what does that look like?
And I think what confuses a lot of people is the first time they use Locust or JMeeter or one of the other tools, they get all these percentiles back.
And they're like, oh, the 90th percentile response time is this and the 95th is this and the 99th is that.
And they're like, okay, well, if I go back to my high school math, I think I can remember what percentiles are, but, like, which one matters?
Because normally there's a massive difference between the 90th and the 99th percentiles with low testing.
And, you know, you might say, oh, well, the 99th is like 10 seconds, but the 90th is like 300 milliseconds.
So, you know, was that a good output or a bad output?
I'm not really sure how to interpret the results.
So, yeah, you'll see in the UI for Locust, it gives you percentiles.
All the other low testing tools are very similar.
It's basically like a distribution of the response times for those particular pages.
And what you're trying to understand is, like, what is the expected response time?
So, you know, if it's a bell curve, then what's the center point of that?
Because the 99th is interesting, but often, like, it's because the cache was warming up or, you know, there was like one user that took 10 seconds.
The cache expired and then it got recreated right then or something.
Yeah, exactly.
So for 99th percentile, you know, if it's 10 seconds, you might have one user that took 10 seconds and 99 users that took, you know, a couple of hundred milliseconds.
So it's, you know, do you want to, you know, factor for that one user or do you want to focus on the other, on the bulk of the group?
Yeah.
And if you're like my daughter, you'll just say that the Internet's broken.
Yeah.
It could be, or it could be that YouTube is slow for some odd reason for you for a minute.
Like, it's not necessarily the entire Internet that is the fault here.
Yeah.
Well, most users, I think if it took 10 seconds to respond with just clicking the refresh button.
Yeah, exactly.
So just generate more load.
Yeah.
You might have to program that into your load test.
If it takes longer than five seconds, then issue another three requests.
Yeah.
That's a really good point, actually.
Let's see.
Are there any pictures of the reports here?
Let's see.
Yeah.
You got these, you got these nice graphs that shows you response times.
Yeah.
They're not just graphs.
They're live graphs.
You can kind of see it like flowing as it's testing, right?
As it's ramping up or whatever.
Yeah, definitely.
So what we do with the load testing services is we've got that graph.
But then you can also say, I also want to see how many crews a second were happening on the database.
What was the memory usage of the web applications?
Like how many, like what was the pod size if you're using Kubernetes?
Like stuff like that.
I've got a couple of demos where I've got like parameter-based load testing, where when you're setting up a Kubernetes environment, you're like, oh, I'm not sure how much memory I should allocate the containers or like how many of them I should have in a pod.
So you can basically just use something like GitHub Actions to give a matrix and say, okay, I want to test these configurations.
Let's see what happens with half a gig of RAM per container or two gigs of RAM per container.
Let's do two, four, eight in a cluster.
And it will run the same load tests for every configuration.
And then you can just compare all the graphs and say, okay, we don't want to over-allocate infrastructure because, you know, people often go a bit nuts.
They're like, oh, how many instances in a cluster do we need on the front end?
And they allocate like 16.
But then when it's actually running, 15 of them are idle.
So it's kind of just over-provisioning.
So load testing can be a great way of not just like planning for spikes in traffic or being able to like cater for it.
But actually the other way around, which is you run a load test and you realize you could probably actually do with less infrastructure.
So like things like memory as well, like memory is expensive when you're buying it from a service provider.
And, you know, CPUs and stuff like that or the number of instances.
And actually, let's see what happens if we turn some of those things down.
Will that impact the response times?
Can we get away with it basically?
Because in a lot of cases, you can actually spend a lot less money and get the same performance or even just a negligible difference.
Or maybe you identify somewhere where you could add some level of caching.
Yeah.
And then all of a sudden you get, you can preview, you get 10x the load per worker process.
You know, we could, we could have a smaller machine or smaller cluster or whatever.
Yeah, exactly.
So one, one thing I want, would like maybe to talk to the folks about is just how do you interpret these graphs?
So you have like a request per second.
And then you talked about, say, the 95th percentile response time and this ramping up.
And usually when you look at these graphs, it's really obvious, like, yeah, we can add more users.
But here is where we start to suffer consequences if we add any more users than this.
Doesn't always just completely fall over.
It just behaves worse until it does.
It's kind of a bit like a feedback system for anyone who studies that.
When you configure a ramp in Locust, you're saying, okay, how many users per second do we want to add?
And you start off with a slow ramp is my suggestion.
So like, you know, every 10 seconds, add one user.
And would like be a really slow way of doing it.
And then what you're looking at is the response times for each page, or you can get an average of everything.
So if you're starting off with a load test that just tests one page, you slowly ramp up the users.
Then you're looking at the response time graph.
Often when you run a load test where it starts, the response times spike up at the beginning.
Because, you know, the service was probably asleep.
The database server was probably asleep.
You know, it needs to like kick a few things into action to get it responding.
So you often see like a spike at the beginning.
That's fine.
Don't worry about that.
If as long as it's a short spike and not, it doesn't go on for hours.
But we know once everything's warmed up, then you should see a stable graph.
So the response times should stick around the same level, even as you add more and more users to a point.
So it's basically looking at the two graphs, which is the response time and the number of users,
and trying to understand how many users does it need to get to before that response timeline starts going up.
And you basically know that's where you've introduced some sort of bottleneck.
Something's reaching its limit.
Yeah.
That's where someone, you know, people are all of a sudden sitting in a queue somewhere.
The hard part is actually figuring out where that queue is.
If you don't have the time, but you've got the money, you can just throw more infrastructure at it.
But often you can fix some of those queues by looking at where, what are we caching and where?
Right. Look at the CPU usage of the various aspects.
What's the database CPU load?
What's the web process?
If the web bit is kind of chill, but the database is at 100%, maybe you need better indexes or something.
Yeah, because often you look at the infrastructure on the back end.
And even though the response times are going up, so there's a bottleneck, the CPU is maybe still at 60%.
And the memory might only still be at 50%.
So you're like, okay, you know, more CPUs isn't necessarily the issue, but things are getting stuck somewhere.
And that might be that, you know, each page, actually, I remember one low test I did where each, it was a PHP application using like some framework.
And the performance like was horrible and we couldn't figure out why.
And we put on some debugging tools and realized that every single page ran 200 SQL queries.
Because of the way they've written it.
It was like, oh, that'd be why.
Because, you know, you're looking at the CPU and memory of the actual web servers and they're fine.
Like the web servers are just like basically just sitting there continually waiting for the database server to respond.
So, you know, you look at the resource usage and you might be thinking, well, why is it getting slower?
But often it's because you're making calls to the database or a backend API or something.
And it's just sitting there idle waiting for the responses.
So I know you mentioned the show a few times, but like the N plus one issues and stuff like that for ORMs in particular is where you get those types of scale where you're, you should not be seeing that many SQL queries.
It's so easy to do because you write the same code, get the things and then loop over some and interact with some aspect of them.
And if you don't eagerly do the query at the first part, each one of those is a separate query.
The more you get back, the worse it is.
Yeah, exactly.
And I mentioned like an AI app, like with this LLM rag app that we did a low test on with like 10 users.
And like the CPU and memory were fine on the front end because it's not really doing anything.
It's just calling the LLM.
But it hit like a token limit, like really, really quickly.
And because we were capturing that and tracing it, then we could see like that's what the bottleneck was, that it was getting rate limited on the on the back end.
One of the challenges that people can run into, this is really important, is testing with way less data than you're going to have in production.
I've got 10 entries in the database because that's what I bothered to test type in while I was playing with the app.
But I've got a million in production.
Stuff like that, right?
I've got three products and there's a million in production.
I've seen this a few times where people have done load testing and they're like, oh, on the all users page or on the list products page, like it runs super fast because they've deployed the dev environment and it's got like no products in the database.
It doesn't matter if you have indexes because there's only three things, just return them all.
Yeah.
Or it's good.
Yeah.
And you don't see things like the N plus one because, you know, there's only like a couple of products, if any at all.
So you often want to seed the application with as much fake data as possible.
And the library that I absolutely love for this is called Mimesis, which is a fake data generator, kind of similar to Faker, but it's got a lot more support for like different languages and environments and stuff like that.
So if you wanted to say, okay, let's create a hundred thousand users and you need to generate names and addresses and locations and stuff like that, you can do that using Mimesis really easily.
And also if you want to do like test like different cultures or locales.
So, you know, not just testing like English names, but testing all sorts of different countries and stuff like that or different phone numbers, then yeah, you can use Mimesis to do that.
Yeah.
It's got all kinds of different things.
You get credit cards and phone numbers and all kinds of stuff.
Yeah.
I need a hundred thousand Brazilian phone numbers and it will just give you them in exactly the right format.
Yeah.
So then you save those to your database once.
Yeah.
And then you can run your tests and see if you have your plus one problem or you, your indexes don't fit into memory or whatever the problem might be.
Right.
Yeah.
So for Django or for SQLAlchemy, you can do the, like these load and dump commands.
So what I kind of recommend is to keep it fast is to use Mimesis to generate a seed data file, even if that's like JSON or something, and then just do like a bulk load in the test environment.
And then you can reset it every time.
So you can basically just do a rollback and then just reset and do a bulk load.
If you're using like a document database, then they've got similar tools for that.
So if you're using Mongo, then you could just do a bulk load from like a test command, basically.
Right.
Just do a load it up with some fake data, do a Mongo dump.
And then whenever you're ready to reset it, just do a Mongo restore --drop and it'll.
Yeah, exactly.
From scratch.
Yes.
Yep.
And you can reuse those for your integration tests as well.
If you're writing, if you've got integration tests with Django or Flask, then often you need a database fixture and you want to seed that with some information, then you can just reuse the same data.
But also do transactions, right?
Like put a, put a transactions that's always rolled back.
So once you load it up, you're not, you're not breaking it.
But that's tricky if the code itself calls commit, I suppose.
Yeah.
Some idea.
No, it changed it.
Darn it.
Yeah.
There was a question in the chat.
How does load testing change when you have non-deterministic processes, e.g. LLM queries, which is a really good question.
And I think it's kind of related to this where you've got a, you want to introduce like an element of randomness.
But we talked about like a, you know, a user search page, or if you've got like a chat feature or something, then you want to kind of vary the question, especially if you've got any kind of caching.
So that is tricky though, because at the moment, like the LLM calls, depending on which model you're using and how it's set up and stuff like that, like, but they can take, you know, a second to 10 seconds to get a response.
Easy.
Yeah.
So, you know, you need to kind of factor that in.
It's like, how is your app, how does your app handle sitting and waiting for 10 seconds before it gets a response back?
And often you'll find that you'll max out the number of like threads you've got, all the number of workers in like G Unicorn or Uvicorn or something like that, because they're all just sitting there waiting.
That's a place where async and await would be pretty awesome.
Yeah.
Right.
Because you can sort of let the thread go and just, you'll get back to it.
Also, maybe you just need a different architecture.
You know, maybe it's not just more caching or more hardware.
It's like, so we're going to put the, there's a pending request in the database and we're going to push that off to somewhere.
And then we'll just set up like some kind of JavaScript thing to check if it's done and then pull down the answer or, you know, something that's not necessarily blocking potentially.
I think in a way it's great for, like you mentioned, great for that.
Actually, there was an announcement pretty recently that you can run Uvicorn now, like as a production.
Yeah.
Without being wrapped through G Unicorn.
Yeah.
That's awesome.
For this kind of scenario we just talked about where you're waiting on backend calls and stuff like that, use the async version of those libraries.
Like if it's OpenAI, there's an async version of the OpenAI SDK.
Use that.
And then at least like Uvicorn is going to just have, it will quite happily run like hundreds and hundreds of those requests that are just waiting on backend calls.
Absolutely.
So one thing I want to throw in here, this is coming from me, not from you, but I want to get your thoughts on it.
So I recently set up this thing called Uptime Kuma.
Are you familiar with Uptime Kuma?
No.
So this is an open source, self-hosted Uptime monitoring tool.
And just run it in Docker.
Off it goes.
And one of the things that's nice about this is, well, I, over on Talk Python.
Now, if you go to the bottom, it's got a server status and you can come in here, you can see the websites behaving well.
The RSS feed is behaving well, the courses size, but also the mobile API, all these things.
But the reason I think this is, I mean, status, like, is it working or not?
It's not really that relevant.
That's like an operational thing.
But what's cool about it is if you go and dig into the thing, you go to the dashboard, like, let's say, you could even group them, right?
So like, let's do this.
I'll do the API.
So for like the mobile apps API or the mobile app for Talk Python courses, you can actually see over time what it's seen in response time for like days.
So with your load tests, you're saying, let me just hammer it and see how long, how it responds to that, right?
And if you point something like this, or there's many other tools like this, but this is a nice one.
You can just say, just keep a record and let me go back and look for, you know, for 24 hours.
How is the load looked?
And you can see there's some weird spike there.
It probably did like a deployment, like right around then or something.
But in general, you know, 40, 50 milliseconds, 35, 81, right?
It's handling it.
Because if the thing was overwhelmed, then these tests would also suffer the latency that everyone else is suffering, you know?
And this is like every 30 seconds.
Yeah, that's really cool.
I do want to give a shout out to OpenTelemetry.
I hope more people, this, every year, more and more people find out about this and they're like, oh, this solves a lot of my problems.
It's a CNCF project.
So it's a big open source standard.
It's basically is a standard for instrumentation, like an observability.
So you kind of plug these instrumentation libraries into your app.
And there's ones for fast, FastAPI, Django, Flask, like the backends, like Mongo, like SQLAlchemy, stuff like that.
And you basically just install these packages and it will just start to capture all this information about, oh, there's a request and it had these parameters and it took this long.
And so in Python, like with a very small amount of code and a few packages, you can instrument all this stuff in your application.
And then the other thing that OpenTelemetry does is it will have, it's kind of like a pluggable exporter.
So wherever you want to send all that data to, you can kind of pick and choose like which platform you want to send it to.
And there are some like local ones you can use as well.
Like you showed this tool for uptime.
There's some Docker containers for that.
So like you just spin up a Docker container and it will just take all that telemetry data and give it to you in a GUI.
So you can see like all the traces, how long they took, what calls it made on the backend, what queries it was running.
Like OpenTelemetry is a brilliant way of doing that.
And it's not just Python.
Like if you've got some components of your application that are written in other languages, like odds are that it is also supported.
So it's kind of like a framework, I guess, for capturing data.
And when you're doing load tests, this is how we do like postmortems to figure out or even just looking at the stats and seeing like where did things go slow.
And I've got some videos and stuff of demos I've done with this where I've got like applications.
I've done a load test on them and then I can see, oh, there was a spike in load.
Let's go back and look at the data to see what caused that and which calls was it?
You know, what was the resources, the memory usage?
And often like, you know, was it the database or was it an API call?
And why did it take so long?
What were the special parameters, stuff like that?
So you.
What were we waiting on?
Was it the actual web app or was it a database?
But like you can retroactively know that, right?
Yeah.
And I can tell them, she's a great, great way of doing that.
Yeah.
That's super cool.
That's way more holistic than my just give me a graph or response time.
Yeah.
You know, that's still cool.
It's still cool.
It's, you know, 20 minutes to set it up in the Docker cluster.
Yeah.
Awesome.
Well, let's talk about, I know we're basically out of time, Anthony.
Let's just talk really quickly about one thing that I think is a little bit tricky.
And maybe just to get your thoughts on, you know, my Vercel example of cost is sort of
in this space.
And that's serverless.
What are your thoughts on serverless?
Like you have way more control when you have a VM or you've got a Kubernetes cluster or whatever
it is you're working with.
Right.
I don't know.
I always kind of see serverless.
It's a really cool idea, but it's more of a commercial concept than a technical one.
Yeah.
I'm thinking more about just the, you don't control the warmup.
Yeah.
You don't control the machine.
You know, there's just a lot of stuff that's black box to you.
I think most of the platforms and most of the providers have got a, like a range of options.
Like I speak to Azure Functions, which is like our serverless Python engine.
You can use that as like just pay per request, but you know, it then has certain optimizations
so that it has like a warm startup time.
So, you know, if it's, you know, if you haven't made any requests for, you know, a certain number
of hours, then the application will fall asleep.
So it's not using resources and we're not charging you money.
But if you don't want to have that, if you want the startup time to be super fast, then
you can use a different service level basically and have like faster startups.
You can have like pre-allocated resources, which then becomes not serverless, but it's
the same framework.
It's the same architecture and Lambda is the same.
AWS Lambda is the same.
You can kind of set it up in those different options.
Things kind of get tricky when you get into Kubernetes because you've got real infrastructure
on the background.
And, you know, when you look at Kubernetes clusters or Kubernetes as a service or however you're
setting it up, the first question is like, how many VMs do I need to provision and how
big do they need to be?
So yes, you're kind of building like a serverless abstraction on top of it, but you've got real
infrastructure in the background.
That's actually really hard because in a lot of cases, you've just got tons of idle infrastructure.
So I think low testing is a good way of looking at like trying to right size what you've got
provisioned.
In many cases, scaling down some pits and scaling up others.
I don't mess with Kubernetes.
It's too much for me.
I don't need all that.
I don't need all that stuff.
I do use Docker though, which is really, really nice.
And I know a lot of these tools we talked about support running the Docker or working with Docker
and so on.
Yeah.
All right.
Well, hopefully people have some really concrete ideas and tools that they can use like Locus.io,
which we're both huge fans of.
Playwright, same deal, but also some of the philosophy and ideas behind it, which is super
important too.
So much appreciated you coming on the show to share that and just the chance to catch up.
How about you give us a quick wrap up, important takeaways for people who want to go out and
test their stuff now?
Yeah.
So I think step one is to look at your application and understand what your users are likely to
do.
So if you want to design a low test, start simple and maybe even start with Playwright
because you can just, you just spin up the browser recorder, click around in the website
and, you know, simulate what a user would be doing.
Stitch that together with Locust and test 10 users.
Don't go nuts.
Start off with a small number, simple test, and you will uncover the things that need optimizing.
I don't think I've ever encountered an application that just ran really efficiently the first time
and then just keep working on that.
So yeah, instead, because often you would end up trying to,
kind of optimize things that are not going to get touched or don't really make a difference
when you actually test it.
So yeah, start simple.
I recommend using Locust and Playwright if you want, or you can just write a simple Locust
test and then put the instrumentation in the backend so that you can see not just the response
times, but you can see as much data as you can, as possible on what's happening and how
long it's taking.
And I'll share like a couple of links of some simple dashboards you can use with OpenTelemetry,
whether it will capture that data locally or in the cloud and show you, you know, a trace
of every request.
Awesome.
And do that with real data.
Don't do it with three entries.
Yeah.
It's not going to, not going to mean what you think it means if you do it with only
three entries.
Yeah.
All right.
Well, always great to have you on the show.
Thanks for being here, Anthony.
Catch you later.
Great to be back.
Thanks for much.
Yeah.
Bye.
This has been another episode of Talk Python to Me.
Thank you to our sponsors.
Be sure to check out what they're offering.
It really helps support the show.
Take some stress out of your life.
Get notified immediately about errors and performance issues in your web or mobile applications with
Sentry.
Just visit talkpython.fm/sentry and get started for free.
And be sure to use the promo code talkpython, all one word.
This episode is brought to you by WorkOS.
If you're building a B2B SaaS app, at some point, your customers will start asking for
enterprise features like SAML authentication, skim provisioning, audit logs, and fine-grained
authorization.
WorkOS helps ship enterprise features on day one without slowing down your core product development.
Find out more at talkpython.fm/workos.
Want to level up your Python?
We have one of the largest catalogs of Python video courses over at Talk Python.
Our content ranges from true beginners to deeply advanced topics like memory and async.
And best of all, there's not a subscription in sight.
Check it out for yourself at training.talkpython.fm.
Be sure to subscribe to the show.
Open your favorite podcast app and search for Python.
We should be right at the top.
You can also find the iTunes feed at /itunes, the Google Play feed at /play,
and the direct RSS feed at /rss on talkpython.fm.
We're live streaming most of our recordings these days.
If you want to be part of the show and have your comments featured on the air,
be sure to subscribe to our YouTube channel at talkpython.fm/youtube.
This is your host, Michael Kennedy.
Thanks so much for listening.
I really appreciate it.
Now get out there and write some Python code.
I'll see you next time.