WordPress security: the 10-minute monthly checklist that catches real problems

Most WordPress security advice is useless.

“Use a strong password.” “Keep plugins updated.” “Install a security plugin.”

These are fine, but they don’t tell you what to actually check each month on client sites. Here’s what I check — based on the actual vulnerabilities and incidents I’ve seen managing sites for paying clients.

Why 10 minutes and not an hour

A 10-minute monthly check that actually happens is infinitely better than a comprehensive quarterly audit that gets skipped.

The goal isn’t perfect security — it’s catching the 80% of problems that show up most often before they become incidents.

The monthly checklist

1. Verify WordPress core and plugins are current (2 min)

wp --allow-root core check-update
wp --allow-root plugin list --update=available --format=table

Plugins with known CVEs get exploited within days of disclosure. Being current matters more than any security plugin.

What to watch for: Plugins not receiving updates for 6+ months. These become unpatched vulnerabilities. Deactivate and replace them.

2. Check for default admin username (30 sec)

wp --allow-root user get admin --field=login 2>/dev/null && echo "WARNING: user admin exists"

Still the most common brute-force target. If a client’s site has a user named “admin” with editor or admin role, rename it.

wp --allow-root user update admin --user_login=something_else

3. Verify wp-config.php permissions (30 sec)

stat -c "%a" /path/to/wp-config.php

Should be 600 or 640. If it’s 644 or 777, fix it:

chmod 600 /path/to/wp-config.php

644 means any process on the shared server can read your database credentials.

4. Check for exposed debug.log (30 sec)

[ -f /path/to/wp-content/debug.log ] && echo "WARNING: debug.log exists" || echo "clean"

Debug logs often contain: database credentials, API keys, internal paths, email addresses, and error messages that reveal vulnerability details. If it exists, read it before deleting it — then delete it.

Also check that WP_DEBUG_LOG is disabled in wp-config.php on production sites.

5. Audit admin users (2 min)

wp --allow-root user list --role=administrator --fields=ID,user_login,user_email,user_registered

Compare against who should have admin access. Former employees and contractors frequently retain admin accounts. Anyone not recognized: disable immediately.

wp --allow-root user delete USER_ID --reassign=1

6. Check for world-writable PHP files (1 min)

find /path/to/wordpress -name "*.php" -perm /o+w 2>/dev/null

Any output is a problem. World-writable PHP files can be modified by other users on shared hosting or by malware. Fix:

find /path/to/wordpress -name "*.php" -perm /o+w -exec chmod o-w {} ;

7. Review recent logins for anomalies (1 min)

Install the WP Last Login plugin (free) or use Wordfence logs. Look for:

  • Logins from unexpected countries
  • Multiple failed login attempts followed by success
  • Logins at unusual hours for that client

If something looks wrong: force password reset on that account immediately.

8. Check SSL certificate expiry (30 sec)

echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate

An expired SSL breaks contact forms, ecommerce checkouts, and admin logins. Set a calendar alert for 30 days before expiry. Better: automate renewal with Let’s Encrypt + certbot.

9. Verify backups ran (1 min)

Check your backup plugin’s last successful run date. If it’s more than 2 days ago for a WooCommerce site, or more than 7 days for a static site, something is wrong.

Also check that the backup destination is off-server. Backups on the same hosting account as the site don’t protect against:

  • Hosting account compromise
  • Host-side data loss
  • Account suspension

10. Run a quick malware scan (2 min)

Wordfence (free tier) does a file integrity check:

wp --allow-root wordfence scan

Or check core file integrity manually:

wp --allow-root core verify-checksums
wp --allow-root plugin verify-checksums --all

Modified core or plugin files that don’t match the official checksums indicate tampering. Investigate before dismissing.

The red flags that need immediate action

These aren’t monthly checks — they’re things to act on the moment you see them:

  • Unknown admin users — potential backdoor account
  • Core or plugin files modified without a recent update — likely compromise
  • Sudden traffic spike with no marketing campaign — bot activity or exploit
  • Site redirecting visitors to another domain — active malware
  • Contact form sending spam — either compromised or open relay

If you see any of these: put the site in maintenance mode first, investigate second.

Automating this

Running these checks manually across 10 client sites takes about an hour per month. The WP-CLI commands above work well in a script that loops over your client list and emails you a report.

The approach: each check writes a pass/fail to a log, then the script generates an HTML summary. If anything critical is flagged, it sends an alert email immediately.

Free checklist + the automation bundle:

What security issues do you encounter most often on client WordPress sites? Brute force, compromised plugins, or something else?

More in this series: WordPress Agency Toolkit

All tools and templates: devautomation.gumroad.com

Building a Spring Boot Monolith Application and a DevSecOps Pipeline Around It

A lot of CI/CD tutorials show a simple “Hello World” app and a Jenkins job that prints Build Successful. That never felt close to how systems work in real environments. At least, they never helped me.

So I decided to build a small University Management Spring Boot monolith application with JWT authentication and protected APIs, and then build a complete pipeline around it — from source control to deployment, quality checks, security scans, artifact management and validation. Since I’m not a developer, the code itself isn’t great it was just me exploring a new language along with some help from AI (Needless to say!)

The application itself is intentionally simple:

  • JWT-based authentication (/auth/login)
  • Protected APIs (/students)
  • Spring Security integration
  • Maven build and Docker packaging

The goal wasn’t to show how to build an application (In that case, this would be the worst tutorial). Th goal is, to show a CI/CD(partly) flow to someone in less than 15 minutes.

So I created a mini DevSecOps pipeline that includes:

  • GitHub webhooks
  • Jenkins Controller + Agent
  • SonarQube quality gates
  • Docker image creation
  • Trivy security scans
  • Nexus artifact storage
  • Automated deployment using Docker Compose

Just incase, anyone’s interested in the code, it stays here

By the end, a Git push automatically builds, scans, deploys and validates the application.

*The diagram: *

The App:

  • Register using the /auth/register API
[root@server1 config]# curl -v -X POST "http://localhost:8081/auth/register?username=ram&password=redhat123"

  • Pass the credentials to the /auth/login API to login . The App will return a JWT token, which will then be used to interact with the API.
[root@server1 config]# curl -X POST 
"http://localhost:8081/auth/login?username=ram&password=redhat123"
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyYW0iLCJpYXQiOjE3NzkxNzA4MjIsImV4cCI6MTc3OTE3NDQyMn0.4AwQQqEa07f_-YId7LSlXYnctaWf7TdqG6xPYxTum_4

  • This step is actually not required, just a part of convenience.
[root@server1 config]# export TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyYW0iLCJpYXQiOjE3NzkxNzA4MjIsImV4cCI6MTc3OTE3NDQyMn0.4AwQQqEa07f_-YId7LSlXYnctaWf7TdqG6xPYxTum_4
  • Create entries using the JWT token to authenticate using the /students API.
[root@server1 config]# curl -X POST 
http://localhost:8081/students 
-H "Authorization: Bearer $TOKEN" 
-H "Content-Type: application/json" 
-d '{
"name":"John",
"email":"[email protected]"
}'
{"id":1,"name":"John","email":"[email protected]"}
  • Query the API using the token.
[root@server1 config]# curl -H "Authorization: Bearer $TOKEN" http://localhost:8081/students;echo
[{"id":1,"name":"John","email":"[email protected]"}]

The pipeline:

  • We will make use of Jenkins to run the pipeline.
  • I have chosen to run the jobs off of a jenkins agent, simply to separate orchestration and execution

The very first stage of the pipeline after the developer pushes the code is to

  • checkout the code from source control
  • Build the application using Maven.

  • The build generates a jar file, which is quite famously referred to as the artifact.
  • Now, this artifact is scanned by our quality gates which checks it for any code smells, Bugs, Vulnerabilities, etc.

  • The sonarqube interface shows something like:

My 2 cents: Do not forget to add Jenkins call back in Sonarqube, else the job in Jenkins will wait to get the status from sonarqube and will eventually timeout.

  • If our code passes quality gates, we build the docker image and bake the artifact into the image.

  • Earlier, we scanned for vulnerabilities, bugs in the code. Now, it’s time to scan for vulnerabilities in the docker image. I used Trivy for this. The ideal way to do this is to exit the job when there are vulnerabilities in the image. But for this lab I’m just running the scan as the image i pulled had loads of them.

  • Additionally, I also posted the scan results on a nexus repository with the job number appended.

  • This is the smallest CD part of the pipeline where we bring up the app using the image we built earlier using docker-compose.

  • Finally, keeping the artifact on the agent is not a good idea, so I moved it to a Nexus repo and ran a cleanup of the workspace to remove it from the agent.

Future enhancements: I must admit, this document is incomplete without :

  • converting this to a microservice
  • Moving the application to K8s or Openshift.
  • Adding monitoring for our App.

MainWP vs ManageWP vs custom scripts: how I manage 15+ WordPress sites in 2025

Managing multiple WordPress sites for clients is a solved problem — but the solution depends on how many sites you manage and what your actual bottleneck is.

I’ve used all three approaches. Here’s when each one makes sense.

The three real options

ManageWP — cloud-hosted dashboard, monthly subscription (~$2/site or $100+/month flat), handles updates, backups, uptime, performance checks. Polished UI.

MainWP — self-hosted on your own WordPress install, free core, paid extensions for advanced features. You own the data.

Custom scripts — WP-CLI + SSH + your own automation. Free forever, runs exactly how you want, no third-party dependencies.

ManageWP: best for non-technical agency owners

ManageWP is the easiest to set up and the easiest to explain to clients. The dashboard looks like a proper SaaS product.

Strengths:

  • Cloud-hosted (no maintenance of the dashboard itself)
  • White-label client reports that look professional
  • Good uptime monitoring
  • One-click safe updates (tests after updating)
  • Works with WooCommerce

Weaknesses:

  • Gets expensive at scale. 20 sites at $2/site = $40/month. 50 sites = $100+/month.
  • You’re dependent on their infrastructure. If ManageWP has downtime, so does your monitoring.
  • Less customizable — you get what they give you
  • Premium features (performance checks, SEO audits, etc.) add cost quickly

Best for: agencies managing 5-20 sites who want a polished tool and don’t mind paying for it.

MainWP: best for large volumes and data ownership

MainWP installs on a WordPress site you control. Your data stays yours.

Strengths:

  • Free core with no per-site fees
  • Good extension ecosystem
  • Works well at 50+ sites
  • You control the data and the dashboard

Weaknesses:

  • You’re now managing another WordPress site (the MainWP dashboard)
  • Extensions add up: advanced reports, client portal, vulnerability scanning are paid
  • Requires more setup than ManageWP
  • The child plugin must be installed on every site you manage

Best for: agencies with 20+ sites who want to own their data and avoid per-site pricing.

Custom scripts: best for technical freelancers who want full control

If you’re comfortable with the command line, WP-CLI + SSH + bash/PowerShell gives you more flexibility than either dashboard solution.

The basic flow:

#!/bin/bash
# Run on one client, or loop over clients.json for all
WP_PATH="/var/www/html/client"

# Backup first
wp --path="$WP_PATH" --allow-root db export backup_$(date +%Y%m%d).sql

# Update everything
wp --path="$WP_PATH" --allow-root core update
wp --path="$WP_PATH" --allow-root plugin update --all
wp --path="$WP_PATH" --allow-root theme update --all

# Check for issues
wp --path="$WP_PATH" --allow-root doctor check --all

Strengths:

  • Zero monthly cost
  • Runs on your schedule, not a third party’s
  • Fully customizable (add any check you want)
  • Generates reports exactly how you want them
  • No dependency on external services staying online
  • Handles edge cases that dashboards don’t

Weaknesses:

  • Takes time to build (10-15 hours upfront)
  • Requires SSH access to servers (not always available on shared hosting)
  • No visual dashboard (unless you build one)
  • Debugging when something goes wrong requires CLI knowledge

Best for: technical freelancers managing 8+ sites who want the lowest possible ongoing cost and maximum control.

The math: what each approach actually costs

Assuming 15 sites, 2 hours/month manual time per site without automation:

ManageWP ($2/site):

  • Monthly cost: $30
  • Time: ~3 hours/month reviewing dashboards + handling flagged issues
  • Annual: $360 + ~36 hours of your time

MainWP (free core + $100/year extensions):

  • Monthly cost: ~$8
  • Time: ~3 hours/month + occasional dashboard maintenance
  • Annual: ~$100 + ~36 hours of your time

Custom scripts (built once):

  • Monthly cost: $0
  • Build time: 12-15 hours (one time)
  • Monthly time: ~45 minutes reviewing automated reports
  • Annual: $0 + ~9 hours of your time

At $80/hour, the time saved by custom scripts vs ManageWP:

  • ManageWP: 36 hours x $80 = $2,880 in time + $360 in fees = $3,240/year
  • Custom scripts: 12 hours build + 9 hours/year = 21 hours x $80 = $1,680 year one, $720 year two onward

Custom scripts break even in year one and cost 80% less by year two.

What I actually use

I use all three, depending on the client:

  • ManageWP: clients who want a client portal and professional white-label reports. I bill the ManageWP cost to the client.
  • Custom scripts: my standard setup for most clients. Free, runs exactly as I need.
  • MainWP: inherited from a previous agency setup. I’m gradually moving those sites to scripts.

For the sites on custom scripts, I run a maintenance script that handles updates, backups, security checks, and HTML report generation — one command per site, or one command for all sites via a JSON config file.

Which should you choose?

Choose ManageWP if:

  • You’re less technical and want a polished dashboard
  • You have fewer than 20 sites
  • You can bill the cost to clients
  • You want one-click safe updates with rollback

Choose MainWP if:

  • You have 20+ sites
  • Data ownership matters to you
  • You’re comfortable running another WordPress installation

Choose custom scripts if:

  • You’re comfortable with the command line
  • You want zero ongoing cost
  • You have SSH access to client servers
  • You want maximum flexibility

Don’t choose scripts if:

  • You’re not comfortable debugging shell scripts
  • Your clients are on shared hosting without SSH
  • You want a visual dashboard without building one

The custom script option, ready-made

If the scripts approach sounds right but you don’t want to spend 12 hours building them from scratch, I put the full toolkit together: bulk update script (Bash for Linux, PowerShell for Windows), security audit, uptime monitor, HTML report generator, and clients.json template.

WordPress Agency Automation Bundle — 19 PLN, use DEVTO for 20% off.

What do you use for managing multiple WordPress sites? Curious whether the community has moved more toward cloud dashboards or self-hosted in recent years.

More from this series

All tools: devautomation.gumroad.com

# How to Apply for the GitHub Student Developer Pack Without Getting Rejected 🚀

It can look something like this:

In the previous post, I showed what you actually get from the GitHub Student Developer Pack and why it’s worth claiming if you’re learning to code.

Now let’s talk about the important part:

How do you actually get approved?

Because yes, GitHub can reject your application if your profile looks messy, your name doesn’t match your documents, or your account information feels suspicious.

So before you smash the “Apply” button, fix the basics first.

1. Set your legal name on GitHub 🪪

First, go to:

https://github.com/settings/profile

In the Name field, write your real legal name.

Not your nickname or backend_god_777
Your actual name.

This matters because GitHub may ask you to verify your student status with a document, and the name on your GitHub profile should match the name on that document.

Don’t lie here. It’s not worth it. If your name doesn’t match, you’re basically asking the verification system to slap your application into the rejection bin.

2. Fill in your billing information, but don’t add a card 💳

Next, go to:

https://github.com/settings/billing/payment_information

Scroll to Billing information and fill it in properly.

You need to add:

  • First name
  • Last name
  • Real address
  • City
  • Country
  • State / Province
  • Postal / ZIP code

The VAT / GST ID field can be left empty if you don’t have one.

Important: you don’t need to add a payment card for this step. Just fill in the billing information so your account looks complete and consistent.

Again, don’t write random nonsense here.

If your GitHub profile says one thing, your billing information says another thing, and your student document says something completely different, the verification system will not magically trust you.

It will probably just reject you and go drink coffee somewhere.

3. Apply for GitHub Education benefits 🎓

Now go to:

https://github.com/settings/education/benefits

This is where you apply for the GitHub Student Developer Pack.

Fill in the required information carefully. Choose your school/college/university, add the requested details, and submit your application.

GitHub may ask you to verify that you’re actually a student. Depending on your case, this can include things like a student email, school-issued document, student ID, transcript, or another proof of enrollment.

Make sure your document is readable, current, and matches the name you added earlier.

This is not the moment to upload a blurry potato photo taken from across the room.

4. Prepare a proper student document 📄

Before submitting the application, make sure your proof of student status actually looks official.

In my case, the document had:

  • my first name and last name
  • my class / grade
  • the academic year
  • the issue date of the certificate
  • the signature of an authorized person
  • the official blue round stamp / seal

That last part matters. A plain document with just your name typed on it may not be enough. GitHub needs to see that it came from a real educational institution, not from your “trust me bro” department.

Also, make sure the document is readable. No blurry photos, no weird angles, no dark screenshots where the text looks like ancient cave writing.

Your name on the document should match the legal name you added to your GitHub profile earlier. If the names don’t match, don’t be surprised if the verification system throws your application into the shadow realm.

5. Check your email 📩

After submitting your application, check your email.

If everything was submitted correctly, GitHub should send you a confirmation email saying they received your application. It may also give you an estimated date for when you should get a final response.

It can look something like this:

If you get rejected, don’t panic.

Read the reason carefully. Usually, it’s something boring but fixable:

  • name mismatch
  • unclear document
  • expired proof
  • wrong school information
  • incomplete GitHub profile
  • document doesn’t clearly show active enrollment

Fix the issue and apply again properly.

The goal is not to “trick” GitHub.

The goal is to make your information clean, consistent, and easy to verify.

6. Don’t rush the application 🧠

A lot of people get rejected because they apply too fast with a half-empty GitHub profile and messy account details.

Before applying, quickly check this:

  • Your GitHub name matches your document
  • Your billing information is filled in
  • Your school information is correct
  • Your document is clear and readable
  • Your proof shows that you’re currently a student
  • Your email is accessible

Small details matter here.

The verification system is not your friend. It doesn’t understand “bro trust me.”

It wants clean information.

Give it clean information.

Final checklist ✅

Before applying, do this:

  1. Go to https://github.com/settings/profile and set your real legal name.
  2. Go to https://github.com/settings/billing/payment_information and fill in your billing information.
  3. Go to https://github.com/settings/education/benefits and submit your application.
  4. Check your email for updates.
  5. If GitHub asks for extra verification, send a clear and valid document.

That’s it.

No magic.
No shady tricks.
No fake data.

Just make your account look real, consistent, and verifiable.

Best API Testing Tools in 2026: Postman Alternatives That Actually Work

Best API Testing Tools in 2026: Postman Alternatives That Actually Work

Postman dominates. But it is heavy, expensive, and slow for what it does. Here are the alternatives that are worth your time in 2026.

Why Look Beyond Postman?

Postman is feature-rich but bloated. The app takes seconds to start. The team workspaces require paid plans. For solo developers, it is often overkill.

The alternatives are faster, cheaper, and sometimes better designed.

Bruno — The Open-Source Favorite

Bruno is the most loved Postman alternative. Open-source, stores collections as plain text files (git-friendly), fast.

The interface is clean. Environment variables work. Import from Postman is supported.

Best part: it is free and will always be free. No cloud subscription required.

If you want Postman without the baggage, Bruno is your tool.

Insomnia — The Design-Focused Option

Insomnia has a beautiful interface and solid functionality. Plugin ecosystem for custom auth flows. Environment variables and secret management built in.

Free tier for solo use. $5/seat/mo for teams.

The design is genuinely pleasant to work with. If aesthetics matter to you, Insomnia wins.

HTTP Toolkit — The Intercept Tool

HTTP Toolkit is different. It is designed around intercepting and debugging HTTP traffic. Set up proxy rules to capture and modify requests from any application.

Open-source. Free. Supports HTTPS inspection, which makes debugging third-party API calls straightforward.

If you need to see what your code is actually sending, HTTP Toolkit is excellent.

RapidAPI (formerly Paw) — The macOS Native Choice

RapidAPI is Mac-native and fast. Beautiful Mac interface. The code generation is excellent — export requests as Python, JavaScript, curl, or whatever language you need.

$10/mo or $99 one-time purchase. Mac users love it.

Curli.ai — The AI-Powered One

Curli uses AI to help build and test API requests. Describe what you want in natural language and it generates the curl command or code snippet.

Interesting approach. The AI assistance is useful for complex authentication flows.

Postman — Still the Standard

Postman still has the largest ecosystem. API documentation, mock servers, monitoring, and team collaboration.

If you work in a large team that needs all of that, Postman is worth it. For individuals, the free tier is limiting.

Quick Comparison

Tool Price Platform Best For
Bruno Free All Git-friendly, solo devs
Insomnia Free/$5 All Design-conscious users
HTTP Toolkit Free All Traffic inspection
RapidAPI $99 once Mac Mac-native power users
Postman Free/$12 All Large teams

My Pick

I use Bruno for personal projects. Git-friendly collections mean I never lose my API notes.

For teams, Insomnia or Postman depending on budget.

Stop paying for tools when free alternatives match 90 percent of your workflow.

X Communities is shutting down May 30. Here’s what I built for builders who need a permanent home.

X Communities shuts down May 30.

If you’ve been using the #buildinpublic community there to document your indie dev journey — that timeline is gone. The posts, the context, the progress log you’ve been building for months.

I’ve been in the same boat. I build things in public and I want a record of it that doesn’t disappear when a platform decides to restructure, pivot, or shut down.

So I built Openstage.

What it is

A public profile page for builders. Your URL is openstage.dev/yourusername.

Connect GitHub and your commits start flowing into your timeline automatically. Add milestones, shipped features, links, notes manually. It becomes a living record of what you built and when.

Here’s mine: openstage.dev/tuxnotfound

Why it’s different

Most build-in-public tools are either:

  • Social feeds (Twitter, IH) — ephemeral, algorithm-dependent, rented space
  • Changelogs (GitHub releases) — dev-facing, not built for public storytelling
  • Personal blogs — great but manual and slow

Openstage sits in the gap: a public-facing builder profile that maintains itself from your GitHub activity, permanent, and fully yours.

What’s in it

  • 🔗 Profile at openstage.dev/yourusername
  • ⚙️ GitHub OAuth sync — commits auto-appear on your timeline
  • 📝 Manual entries: milestones, shipped features, links, notes, releases
  • 🔥 Streaks — consecutive days with any activity
  • 📊 Analytics (Pro) — views, countries, referrers, entry clicks
  • OG card, embed widget, GitHub README badge

Free forever for the core profile. Pro tier is $7/month or $49 lifetime.

If you’re losing your X Communities home, come claim your profile before May 30. Takes 2 minutes with GitHub OAuth.

👉 openstage.dev

Drop your openstage link in the comments — I’ll follow along.

A real-world walkthrough of regression using coffee, code, and actual data

scatter plot

I failed my first stats class in college.

Not because the math was hard. Because every example in the textbook was about iris flowers or Boston housing prices and I couldn’t figure out why anyone in real life would care. So the formulas just slid off my brain.

Years later, I’m a backend dev in Mumbai with a serious filter coffee habit and an even more serious sleep deficit. One Sunday morning, somewhere between my second tumbler of decoction and a half-broken Saturday deploy, I asked myself a question I could actually feel in my body: does more coffee actually mean I write more code?

That question is what finally taught me linear regression. Not the textbook. Not the YouTube playlist. A Google Sheet, my git log, and 30 cups of filter coffee.

Here’s the whole thing, including the math by hand, the part where I got it wrong, and the Python at the end.

The setup (and why I think most tutorials skip it)

Every linear regression tutorial I’d ever opened started with this line:

y = mx + b

And then immediately jumped into matrices. Cool. Useless.

The thing tutorials skip is what problem are you even trying to solve. Linear regression is, at its core, one question:

If I know X, can I make a reasonable guess at Y?

That’s it. If I know the size of a flat in Malad, can I guess the rent? If I know how many standup minutes I sat through, can I guess how many I actually retained? If I know how much coffee I drank today, can I guess how many lines of code I’ll commit?

I’m going to answer that last one.

The data I actually collected

For 30 weekdays I tracked two numbers:

  • X: cups of filter coffee I drank that day (one tumbler = one cup, no chai counted)
  • Y: lines of code I committed that day (net additions, from git log --shortstat)

Here are the first 10 rows. The full 30 are at the bottom of this post if you want to play with them yourself.

Day Coffees (X) Lines of code (Y)
1 2 142
2 4 287
3 1 95
4 3 210
5 5 305
6 2 160
7 0 40
8 4 250
9 3 195
10 6 360

scatter plot

Before doing any math, Before doing any math, I dropped the numbers into a scatter plot maker and just sat with it for a minute. You should always do this first. Eyeballs are a free regression model.
and just looked at it. You should always do this first. Eyeballs are a free regression model.

What I saw: a clear-ish upward smear. Not a perfect line — day 5 (5 coffees → 305 lines) and day 10 (6 coffees → 360 lines) sit a bit off the trend, and day 7 (zero coffee, 40 lines) was the day there was a 4-hour power cut and I worked off my laptop battery in a café. But overall, more coffee, more code.

That smear is what linear regression turns into a number.

What we’re actually trying to find

We want to draw the single straight line that fits this cloud of points as well as possible.

A line has two parts:

  • Slope (m): how many lines of code does one extra cup of coffee buy me?
  • Intercept (b): how many lines of code do I write on a day with zero coffee?

If I find good values for m and b, then for any future day, I can plug in my coffee count and get a prediction.

But “fits as well as possible” — what does that mean? This is the bit that took me embarrassingly long to internalize.

The “least squares” idea, explained without the squares

Imagine I draw a random line through the cloud. For each real data point, there’s a gap between where the point actually is and where my line thinks it should be. That gap is called the residual — basically, how wrong the line was for that day.

If I add up all those gaps, I get a “total wrongness” score. The best line is the one with the smallest score.

One catch: some gaps are positive (line predicted too low), some are negative (predicted too high). If you just add them, they cancel each other out and you get a line that looks correct on paper but is actually terrible.

The fix is to square each gap before adding them. Negatives become positives, big mistakes get punished more than small ones, and now you have a real “wrongness” number worth minimizing.

That’s why it’s called least squares regression. We’re picking the line that makes the sum of squared gaps as small as possible.

That’s the whole intuition. The formulas below are just the calculus result of “find the m and b that minimize that sum.”

Doing it by hand on 5 data points

Let me use just 5 points so the numbers stay sane:

X Y
2 142
4 287
1 95
3 210
5 305

Step 1: averages.

  • Mean of X: (2+4+1+3+5)/5 = 3
  • Mean of Y: (142+287+95+210+305)/5 = 207.8

Step 2: deviations and products.

For each row I compute (X - meanX), (Y - meanY), their product, and (X - meanX)².

X Y X − 3 Y − 207.8 product (X−3)²
2 142 −1 −65.8 65.8 1
4 287 1 79.2 79.2 1
1 95 −2 −112.8 225.6 4
3 210 0 2.2 0 0
5 305 2 97.2 194.4 4

Sums: products = 565.0, squared deviations = 10.

Step 3: slope.

m = sum of products / sum of squared deviations
m = 565 / 10
m = 56.5

Step 4: intercept.

b = meanY - m * meanX
b = 207.8 - 56.5 * 3
b = 207.8 - 169.5
b = 38.3

So my fitted line is:

lines_of_code ≈ 56.5 * coffees + 38.3

Read in English: every extra cup of coffee buys me ~56 more lines of code, and on a zero-coffee day I’d still grind out about 38 lines. That intercept matches my power-cut day pretty well (40 lines).

I checked this against an online linear regression calculator and got the same numbers, which was nice — I’d been low-key worried I’d dropped a sign somewhere. (When you’re learning this, do the by-hand version and then verify with a tool. Two methods agreeing is how you know you actually understand it, not just memorized it.)

The mistake I made the first time

When I ran this on all 30 days, I got a slope of about 53 and an intercept of about 42. Close enough.

But then I made a prediction:

“If I drink 15 cups of coffee, I’ll write 53 × 15 + 42 = 837 lines of code.“

This is wrong, and it’s wrong in a way that’s worth understanding. My data only ranged from 0 to 6 cups of coffee. Predicting for 15 cups is called extrapolation, and a linear model has no idea that at 15 cups I would actually be in the ER, not in VS Code.

The relationship is probably linear-ish in the range I measured. Outside that range, all bets are off. Most real-world relationships curve, plateau, or break at some point. Linear regression is a local approximation, not a universal law.

This is the most common mistake I see beginners make, and honestly it’s the most common mistake I see in production dashboards too. “Our user growth was linear for six months, so we’ll hit ten lakh users by Q4.” No you won’t.

How good is my line, actually?

A line that’s “best” can still be terrible. Imagine if my data were a random cloud — there’d still be a best line, but it would predict nothing useful.

The standard score for this is R² (pronounced “R squared”). It’s a number between 0 and 1:

  • R² = 1: the line passes through every point. Perfect.
  • R² = 0: the line is no better than just guessing the average.
  • R² = 0.7: the line explains about 70% of the variation in Y.

For my 30-day coffee experiment, R² came out to about 0.89. That’s strong — coffee really does seem to track my coding output. But correlation isn’t causation, and here’s where I have to be honest: on high-coffee days I was probably also more rested, more motivated, and not stuck in a 90-minute Outer Ring Road jam. The coffee might be a side effect of a productive day, not the cause of one.

Linear regression will happily fit a line. It will not tell you what causes what. That’s your job.

The Python version (5 lines)

Once you understand what’s happening, the code is almost a letdown:

import numpy as np

x = np.array([2, 4, 1, 3, 5, 2, 0, 4, 3, 6])
y = np.array([142, 287, 95, 210, 305, 160, 40, 250, 195, 360])

slope, intercept = np.polyfit(x, y, 1)
print(f"y = {slope:.1f}x + {intercept:.1f}")

If you want R² and the rest of the diagnostics, scipy.stats.linregress is the friendlier version:

from scipy.stats import linregress

result = linregress(x, y)
print(f"slope: {result.slope:.2f}")
print(f"intercept: {result.intercept:.2f}")
print(f"r-squared: {result.rvalue**2:.3f}")
print(f"p-value: {result.pvalue:.4f}")

The p-value is asking: “what’s the chance this slope is just noise?” Anything under 0.05 is the traditional cutoff for “probably real.” Mine came in at 1e-14, which means: almost certainly not a coincidence. (Doesn’t mean coffee causes code. Just means the relationship is real, whatever it is.)

When to reach for linear regression in real life

Honestly, more often than you’d think. Some boring-but-genuinely-useful examples from my own work:

  • Estimating ticket resolution time from ticket length. Helps with sprint planning.
  • Predicting cloud bill from monthly active users. Helps when pricing a new tier — and trust me, the finance team will ask.
  • Figuring out at what page-load speed users start churning on a landing page. The slope tells you how many sign-ups each extra 100ms costs.
  • Calibrating a sensor. If your IoT temperature reading drifts linearly off a reference thermometer, regression gives you the correction formula in two lines of Python.

The pattern is always the same: you have an X you can measure cheaply, and a Y you actually care about, and you want a quick “if I move the dial on X by this much, what happens to Y?”

It is not magic. It is a ruler. But it’s a ruler that knows about uncertainty, which is more than most rulers can claim.

What to try this week

If you want this to actually stick, don’t just read it. Pick something annoying in your life that has two numbers attached to it, and track it for two weeks.

Some ideas to steal:

  • Hours of sleep vs. time-to-first-coffee (or chai) the next morning
  • Number of standup attendees vs. how long it actually ran
  • PRs opened that day vs. PRs you reviewed
  • Daily commute minutes vs. how cranky you felt at the 6 PM standup

Plug it into any regression tool (I obviously used this one), look at the slope, and ask: does this number match my gut? If yes, you’ve quantified your intuition. If no, you’ve found something interesting.

That’s the whole point of stats, when you strip away the textbook flavor. It’s a tool for arguing with your own assumptions.

The full dataset

If you want to reproduce what I got, here’s all 30 days:

coffees = [2,4,1,3,5,2,0,4,3,6,2,3,4,1,5,3,2,4,5,1,3,4,2,6,3,4,2,5,3,4]
lines   = [142,287,95,210,305,160,40,250,195,360,170,225,260,80,330,
           205,150,265,310,90,200,255,165,355,200,270,155,315,210,275]

Drop those into the snippet above. You should get a slope around 53, intercept around 42, R² around 0.89.

And then go track something of your own. That’s when it actually clicks.

If you spot a math mistake, please yell at me in the comments. I’d rather be embarrassed than wrong.