Posted on Leave a comment

Top 10 Best AI Dark Romance Book Writers to Try in 2026

Dark romance has taken over BookTok lately. Writers are scrambling to keep up with the demand.

I’ve spent a lot of time testing AI tools to see which ones can actually handle the genre without turning everything into a generic love story.

The best AI dark romance writers can hold onto morally complex characters, slow-burn tension, and messy power dynamics across an actual story instead of forgetting what happened three chapters ago.

Some platforms can even take you from a rough idea to a complete manuscript with chapters, dialogue, and a cover.

If that’s what you want, ImagineYourBook.com is the one I’d start with.

1) ImagineYourBook.com

The gallery with some sample books

ImagineYourBook.com is my first pick if I want the AI to actually finish the book.

A lot of AI writing tools are really assistants. They’ll help with an outline, write a scene, or give you five ways to rewrite the same paragraph.

ImagineYourBook is built around the whole book instead.

You can start with one sentence or bring in a detailed outline. Then you can spell out the things that actually matter for dark romance: POV, pacing, character dynamics, tropes, boundaries, the ending you want, and details the story shouldn’t forget.

That’s a bigger deal than it sounds.

If I ask for a slow-burn romance with a morally grey hero, enemies-to-lovers tension, dual POV, no cheating, and a specific emotional payoff, I don’t want the story deciding halfway through that it’s actually a sweet workplace romance.

ImagineYourBook’s state-of-the-art Muse engine plans the book and writes it chapter by chapter while carrying the book’s context forward.

It also goes much longer than the short outputs you get from normal chatbots. You can create books from 5,000 words all the way up to 120,000 words, depending on the plan and book size.

It even recommends new books or series ideas based on your previous ideas so you never run out of good books to write:

For dark romance, I think that whole-book approach is the main advantage.

A possessive character only works if his behavior in chapter 22 makes sense with what happened in chapter four. A betrayal needs setup. A slow burn needs to actually stay slow. And if you’re writing a series, you don’t want book two acting like book one never happened.

ImagineYourBook has a series system that carries characters, canon, open plot threads, world rules, and previous-book context into the next book.

You also get a generated cover, and finished books can be exported as Word or EPUB files so you can edit the manuscript afterward.

I wouldn’t skip that editing step. AI can get you past the blank page incredibly fast, but a generated draft still needs a human reading it for voice, repetition, continuity, and scenes you want to push further.

🔥 The nice part is that you can test the actual workflow rather than a tiny demo. The 7-day trial includes one complete 10,000-word book, so I’d use a real dark romance idea and see what it does with it.

For me, that’s the easiest way to judge an AI book writer.

Don’t ask whether it can produce three impressive paragraphs.

Ask whether you still want to keep reading at chapter ten.

2) NovelCrafter

I like NovelCrafter for one big reason: it remembers what I wrote earlier.

That matters a lot in dark romance, where a hero’s backstory or a slow-burn thread can fall apart if the tool forgets chapter three.

You can start with 10,000 free words, which is enough to test whether the voice works for your story.

The setup is simple. I write, the AI helps, and my story details stay in one place instead of scattered across a dozen chat windows.

Now, a quick note to avoid confusion. There’s also Novelcrafter, a separate tool with a similar name that helps writers outline and draft with AI support.

Both show up in searches, so double-check the site before you sign up.

I found Novelcrafter (the .com one) is often described as a fit for experienced and tech-savvy fiction authors who want control over their process.

It also lets you pick which AI model you want to use, and the team tracks which models users choose most.

For dark romance, that model choice matters. Some models push back on heavier themes more than others.

I’d pick NovelCrafter over ImagineYourBook if I wanted to spend a lot of time inside the writing process and control the AI setup myself.

If I mainly wanted to turn the idea into a complete draft, I’d still start with ImagineYourBook.

3) Squibler AI Novel Writer

I like Squibler because it doesn’t just spit out a few paragraphs and call it a day.

It’s built for long-form work, so you can go from a rough idea to a full manuscript with chapters and scenes.

The AI Novel Writer is the main draw here. You chat with it, describe your story, and it helps you outline, draft, and build out characters as you go.

For dark romance specifically, Squibler has a romance-focused version of the tool. It’s aimed at writers working on love stories, whether that’s your first book or your tenth.

What helped me most was the subgenre support. The romance story generator can handle contemporary, fantasy romance, dark romance, and rom-com, and it tries to stick to what readers expect from each one.

You can also write chapter by chapter instead of dumping everything at once.

That gives you more control over pacing, which matters a lot when you’re building tension between two characters.

There’s a free option to try, so you can test it before paying for anything.

4) BookNova

I like BookNova when I want a full book, not just a few chapters.

It generates a finished fiction book — plot, characters, chapters, and even a cover — and it’s set up for Amazon KDP.

For dark romance specifically, it builds your hero and heroine, picks tropes like enemies-to-lovers or fake dating, and writes dual-POV chapters with slow-burn pacing.

You keep all the royalties, which is nice.

One thing I noticed: it handles tone across a whole novel differently depending on your genre.

A cozy mystery and a dark romance don’t sound the same, and the tool seems to know that.

It also covers a decent spread of genres beyond romance — thriller, mystery, fantasy, sci-fi, and literary fiction.

Each one has its own rules, so that range matters if you write in more than one lane.

I’d say the main draw here is speed plus structure. You’re not staring at a blank page trying to figure out where the third act turn goes.

If whole-book generation is what you’re after, I’d compare this directly with ImagineYourBook.com.

I prefer ImagineYourBook overall because of how much information I can give it about the book before it writes and how that context carries through the manuscript and into sequels.

But BookNova is still worth testing if you like its KDP-focused workflow.

5) Plot Factory

Plot Factory is less of a “write my book for me” button and more of a workspace.

I think of it as a place to keep my story organized while I draft.

The tool lets me build character profiles, track locations, and keep notes on my world.

For dark romance, that matters. Trauma backstories, power dynamics, and shifting loyalties get messy fast when you’re 60,000 words deep.

It also has a text-to-speech feature, so I can listen to my chapters read out loud.

Hearing a tense scene helped me catch dialogue that sounded stiff.

The AI writing help isn’t as strong as tools built specifically around generating fiction prose.

If you want a system that handles trope conventions and heat-level consistency across a whole manuscript, you may want something else.

But I keep coming back to Plot Factory for planning.

I outline my beats there, then move to another tool for the actual writing.

If I want to keep building the manuscript manually, that works fine.

If I’ve already done the hard planning and just want to see that outline turned into the actual book, ImagineYourBook makes more sense.

Plot Factory is a solid pick if you’re a plotter who hates losing track of details.

If you want the AI to do most of the heavy lifting, it’s probably not your first choice.

6) DreamGen

I like DreamGen for the moments when I want to build a whole world, not just a scene.

It’s an AI story generator and writing assistant that handles fan fiction, original settings, horror, and romance.

For dark romance, that flexibility matters. My stories usually mix genres, so a tool that can swing from a tense chase scene to a slow-burn confession is helpful.

DreamGen also doesn’t sanitize your prompts the way many mainstream tools do.

It’s often grouped with NovelAI as an option for writers who want mature or unconventional themes without the AI backing off.

That means I can write a possessive love interest or a morally grey villain without rewriting my prompt five times.

It saves me a lot of frustration.

The company also keeps a blog with a roundup of romance story generators, which I found useful when I was comparing options.

One thing to keep in mind: DreamGen leans more toward interactive storytelling than full manuscript management.

If you need chapter tracking across a 90,000-word book, you may want to pair it with something else.

7) Talefy AI

I like Talefy because it leans into the interactive side of storytelling.

Instead of just spitting out a chapter, it builds stories you can move through and shape as you go.

The tool has a whole dark story and dark romance category, which covers things like twisted love plots and fantasy backstories.

That’s handy when you want a starting point but don’t want the usual sweet meet-cute setup.

You’re also not stuck in one lane.

The AI story generator works across romance, fantasy, sci-fi, and mystery, so you can blend a dark romance with a fantasy world without fighting the tool.

I mostly use it for idea generation.

If I’m stuck on a morally grey love interest or a messy backstory, I’ll run a few prompts and see what sticks.

It’s worth noting that Talefy shows up on roundups of romance story generators, so it’s not some unknown tool.

For me, it’s better for sparking ideas and short interactive pieces than drafting a full 80,000-word novel.

But for brainstorming, it does the job fast.

8) AI Dungeon

AI Dungeon started out as a text adventure game, not a book writing tool.

You type what your character does, and the AI responds with what happens next.

That setup makes it a strange but interesting pick for dark romance.

I use it when I’m stuck on a scene and want to see where a character might go.

Here’s how I approach it.

I set up a scenario with two characters, give the AI some background on their relationship, and then play out the tension line by line.

The output isn’t polished prose. You’ll get repetition, and sometimes the story wanders off in a direction you didn’t ask for.

But that’s kind of the point.

I treat it like a brainstorming partner, not a ghostwriter.

If what you actually want is a finished manuscript, you’re better off with a whole-book AI writer like ImagineYourBook that’s built to carry the premise across chapters.

AI Dungeon won’t hand you chapters with a clean structure.

There’s a free tier, plus paid plans that unlock better models and longer memory.

I’d say try it if you like discovery writing.

Skip it if you need a tool that keeps track of your plot for you.

9) Dabble Writer

Dabble is the tool I reach for when my dark romance draft starts falling apart.

It’s built around organization more than raw AI generation, and that’s why I like it.

The plot grid is the standout feature for me.

I can see every scene laid out in columns, so tracking a slow-burn corruption arc across 40 chapters stops feeling impossible.

Its AI features are lighter than what you get from a dedicated AI book writer that generates full prose.

Dabble offers brainstorming help and editing suggestions, not a machine that writes the whole kidnapping plot for you.

I actually see that as a plus.

My voice stays mine, and I just get help when I’m stuck on a chapter title or a character’s motive.

Everything syncs across my laptop, phone, and browser.

I’ve written morally grey villain monologues on my phone in a parking lot. Weird flex, but it’s handy.

Pricing sits in the middle range, with a free trial to test it out.

If you want a tool that plans and drafts alongside you rather than replacing the blank page with a full manuscript, Dabble earns a spot on this list.

10) Jasper Creative Writing

Jasper started out as a marketing tool, and you can still feel that in how it works.

It’s built for brand copy, blog posts, and ads first, with fiction as more of a side feature.

That said, it does show up on plenty of best AI book writer roundups alongside dedicated fiction tools.

So I gave it a fair shot.

I used it mostly for the smaller pieces of a dark romance draft.

Character backstories, blurbs, and short scene beats came out fine.

Where it struggled was heat and edge.

My morally grey love interest kept coming back sanded down and polite, which isn’t what dark romance readers want.

Jasper also doesn’t have a story bible or a real chapter structure.

If you’re writing a series, you’ll be pasting context in over and over just to keep names and details straight.

The interface is clean, and the brand voice feature can help you stay consistent.

It’s just not built around long-form fiction the way purpose-built romance tools are.

I’d only pick Jasper if I already used it for other work and wanted one subscription instead of two.

If I were signing up specifically because I wanted AI to turn a dark romance idea into a novel, I’d pick ImagineYourBook before Jasper.

How AI Is Changing Dark Romance Storytelling

I’ve watched AI shift from a tool that constantly loses the thread to one that can hold increasingly complicated character relationships across much longer stories.

That change shows up in the tropes writers lean on and in the questions they have to ask themselves before hitting publish.

Common Themes and Tropes in AI-Assisted Dark Romance

The tricky part is that a generic prompt gives you generic output.

If you just type “romance,” you’ll get a meet-cute and a wedding, not necessarily the morally grey mess readers actually want.

So I load my prompts with specifics.

The tropes I see AI handle well include:

  • Captor and captive dynamics with shifting power
  • Enemies to lovers with real stakes, not banter
  • Mafia and organized crime settings
  • Obsession and possessive behavior from the love interest
  • Forced proximity in isolated places

Pacing matters too.

One guide on dark romance structure suggests using the first few chapters to set up the power imbalance.

Then, let small human moments from the dark character show up around chapters four through eight.

This is also why I care so much about whole-book context.

A chatbot can write a convincing isolated scene.

The harder problem is remembering that the first kiss isn’t supposed to happen until the relationship has earned it, that a character still doesn’t know a certain secret, or that a betrayal you’ve been foreshadowing for ten chapters hasn’t happened yet.

Tools designed around the entire manuscript, like ImagineYourBook, have an advantage there over starting a fresh chat every few chapters.

Ethical Considerations for AI-Generated Fiction

Here’s where I slow down.

Fiction that includes coercion or violence isn’t the same as endorsing it, but I still think about how the material reads to someone who’s lived it.

A few things I keep in mind:

Disclosure. Some platforms and retailers ask you to say whether AI helped write your book. Check the rules before you list.

Content warnings. Dark romance readers expect them. Skipping them to be edgy just annoys your audience.

Consent on the page. Some tools, like ones that focus on boundary-aware character conflict, let you write tension without losing track of how consent is handled in the story.

Your voice. AI drafts fast. That’s the appeal, and it’s also the risk if you never edit it into something that sounds like you.

That last point applies even to the tool I ranked first.

If ImagineYourBook hands me a complete manuscript, that gives me something substantial to work with. It doesn’t mean I upload the file without reading it.

I still want to change lines, strengthen scenes, fix things the AI interpreted differently than I intended, and make the final book feel like mine.

I also don’t hand a full manuscript to AI and call it done.

Running it through manuscript-level analysis tools can catch consistency problems that sentence-level grammar checkers miss.

Choosing Dark Romance Books That Match Your Comfort Level

Dark romance covers a huge range, from mild suspense to stuff that will genuinely upset some readers.

I always check the content warnings first, and I keep a clear line in my head between what I enjoy reading and what I’d accept in real life.

Understanding Content Warnings

Content warnings are lists of the heavy themes a book contains.

Most dark romance authors put them in the front matter, on their website, or in the book’s description.

Common ones include kidnapping, stalking, dubious consent, graphic violence, and on-page assault.

Some AI-assisted writers skip this step entirely, so I check reader reviews before I buy.

Goodreads is my go-to for this.

Reader shelves like top dark romance and dark romance recommendations often have reviews that flag specific scenes with page numbers.

Some sites organize books by intensity too.

One guide sorts titles by mood, spice level, and reader fit, which helps if you want something dark but not brutal.

My quick check before starting a new book:

  • Read the author’s posted warnings
  • Scan two or three 1-star and 2-star reviews
  • Look for spice ratings, usually on a 1-5 scale
  • Note the specific tropes listed, like mafia or captive romance

Separating Fictional Fantasy From Real-World Boundaries

Reading about an obsessive love interest isn’t the same as wanting one.

A lot of the appeal comes from safely exploring power dynamics and villain-gets-the-girl tropes with zero actual risk.

Fiction lets you stop anytime.

You close the book, and it’s over.

Real relationships don’t work that way, and healthy ones involve consent, honesty, and the freedom to leave.

I like books that blur the line between love and obsession on the page.

Off the page, I want none of that.

If a book starts feeling less like a fantasy and more like something that sticks with you in a bad way, put it down.

There’s no rule saying you have to finish it.

Frequently Asked Questions

I get a lot of the same questions about dark romance and AI writing tools, so I pulled together the ones that come up most often.

Below you’ll find answers about AI book writers, popular authors, legal publishing rules, spotting AI-written prose, and the content warnings worth checking before you start a book.

What is the best AI tool for writing a dark romance novel?

If I want the AI to turn the idea into the actual book, I’d start with ImagineYourBook.com.

The biggest reason is that it’s built around complete books rather than isolated pieces of text.

I can give it the premise, characters, POV, pacing, tropes, boundaries, ending, or even a full outline, and it carries those instructions through the manuscript.

It can create anything from shorter 5,000-word books to full 80,000- or 120,000-word novels.

For dark romance specifically, that matters because the genre depends so much on pacing and continuity.

You don’t just need a good kiss scene or a convincing morally grey character.

You need the relationship to evolve across tens of thousands of words without the AI forgetting why the characters hated each other in the first place.

The 7-day trial includes a complete 10,000-word book, so that’s where I’d start.

Use a real idea rather than a throwaway test prompt.

You’ll know pretty quickly whether you want to keep reading.

Who are the best dark romance authors to read right now?

If you’re new to the genre, I’d start with the names that keep showing up on bestseller lists.

Penelope Douglas, Ana Huang, H.D. Carlton, and Rina Kent are all solid entry points.

Pepper Winters and C.J. Roberts helped shape the modern version of this genre, so their older books are worth reading too.

For mafia-heavy stories, a lot of readers point to Cora Reilly and Rina Kent.

Tarryn Fisher and Colleen Hoover write darker contemporary stories that lean more psychological than violent.

Your best bet is to pick one subgenre first, then branch out.

Which romance authors are known for using AI in their writing process?

Most traditionally published authors don’t advertise this.

Publishing contracts can restrict AI use, and readers can react strongly to it.

Where you do see open AI use is in indie and self-publishing circles.

Some indie authors talk publicly about using tools for brainstorming, outlining, first drafts, editing, and even generating complete manuscripts that they then revise themselves.

One writer shared that they made about €1,092 from seven dark romance books after switching from sci-fi, using the same tools for both.

The whole workflow is becoming much more accessible.

A writer can now go from an idea to a full draft, cover, Word file, and EPUB without stitching together half a dozen different tools.

That’s a big part of why I think AI-assisted indie publishing is going to keep growing.

What are the top dark romance books for adults?

Here are titles that come up over and over in reader recommendations:

  • Haunting Adeline by H.D. Carlton — stalker romance, very heavy content
  • Credence by Penelope Douglas — isolated cabin setting, taboo themes
  • Twisted Love by Ana Huang — brother’s best friend, morally grey lead
  • Captive in the Dark by C.J. Roberts — one of the older genre-defining books
  • Corrupt by Penelope Douglas — revenge plot with a group dynamic
  • Bound by Honor by Cora Reilly — arranged marriage mafia story

I’d say check the content warnings on every single one of these.

They’re not gentle books.

Are AI-written romance books legal to publish?

Yes, you can publish them.

But there are rules you need to know.

In the U.S., purely AI-generated material generally doesn’t receive copyright protection by itself. Human-authored expression in a work can still be protected, which is another reason meaningful human editing and creative input matter.

Amazon KDP also asks publishers to disclose AI-generated content during the publishing process.

Other retailers have their own policies, and those rules can change, so check the current requirements before you upload.

And if you’re under contract with a publisher, read that contract carefully first.

How can you tell if a dark romance novel was written with AI?

There’s no reliable test, and AI detection tools produce a lot of false positives.

That said, there are patterns I notice.

Repeated sentence structures are a big one.

So is dialogue where every character sounds the same, or emotional beats that get described instead of shown.

AI drafts can also fall into repetitive emotional rhythms.

A character clenches their jaw. Someone’s breath catches. A pulse jumps. Then the same sequence happens twenty pages later with slightly different words.

Long-form continuity is another giveaway when the draft hasn’t been edited.

Names, scars, dates, motivations, or previous conversations suddenly change.

That’s one reason I prefer book-writing systems that carry story context forward, but no system makes editing optional.

A well-edited AI-assisted book can be extremely difficult to identify as AI-assisted.

The editing is what makes the difference.

What trigger warnings should I look for in dark romance books?

This genre goes to places other romance doesn’t, so I always check warnings first.

Common ones include:

  • Non-consent and dubious consent
  • Kidnapping and captivity
  • Stalking
  • Physical violence and torture
  • Sexual assault
  • Self-harm and suicidal ideation
  • Substance abuse
  • Death of a family member

Most dark romance authors put warnings in the front matter now.

If a book skips them, Goodreads reviews or StoryGraph content warnings usually fill in the blanks.

The term “dark romance” covers a pretty wide range.

Some books just have morally grey love interests, while others get graphic and violent—so the label alone won’t always clue you in.

Note that we’re affiliated with no tool here except ImagineYourBook and are proud to be part of such an outstanding book engine that we use ourselves almost daily.

The post Top 10 Best AI Dark Romance Book Writers to Try in 2026 appeared first on Be on the Right Side of Change.

Posted on Leave a comment

Announcing .NET Modernization for Beginners

We’ve built a new course to help you navigate the journey when you’re the owner of an application built on a legacy .NET framework and it needs to be modernized. The whole concept of modernization is overwhelming! Security updates aren’t being issued any longer! Dependencies are out of date because they’re not being updated!
You’re not alone. We have a suite of tools to help you modernize and this course helps you use those tools. We’ve built the GitHub Copilot modernization tooling to help you modernize your .NET code. And we wanted to create a course to help you use the tools so we created the .NET Modernization for Beginners course.

This is a free, open-source, hands-on course that walks you through modernizing a real legacy ASP.NET application all the way to .NET 10, using the GitHub Copilot modernization agent. The course walks you through the modernization journey step-by-step. Along the way you’ll learn how the tooling produces assessments and plans before it changes any code and how you can modify those to tell the coding agent exactly what needs to happen.

Why a hands-on course?

There’s no better way to learn than by doing. And in this course you’re not just going to read about the GitHub Copilot modernization and some theory, but the course will give you exercises to do it too. (You will need a GitHub Copilot subscription.)

GitHub Copilot modernization works differently from agentic development tools you may have tried before. It does not just rewrite your code behind the scenes and hand you back something you have to reverse-engineer. It produces transparent, editable artifacts that you can read, question, and adjust: an assessment.md, a plan.md, and a tasks.md. Those files become your guide and your source of truth. You review them, you shape them, and only then does the work begin. The agent assists you, but you make the calls.

The four chapters

The course is organized in the same way you’d approach a modernization journey. Each chapter builds on the last, and each one teaches you not just what to do, but why you are doing it.

Chapter 1 – Assessment

You start by pointing the modernization agent at a legacy solution and letting it analyze what is really there. You explore the generated assessment.md to understand the risks, the dependencies, and the level of effort involved, so you begin from facts instead of guesses.

Chapter 2 – Planning

Next you turn those insights into a plan. The agent produces a plan.md with recommended target frameworks, a sequence of upgrade steps, and an effort estimate. You learn how to review it, customize it for your context, and finalize a plan you actually believe in.

Chapter 3 – Upgrade & Execution

This is where the agentic development happens. The agent executes the plan while you track progress in tasks.md, moving the application forward iteratively. You will see it do the heavy lifting while you stay in the driver’s seat and make the decisions that matter.

Chapter 4 – Cloud with Azure

Modernization is not finished until your app is running where it belongs. In the final chapter you publish the modernized application to Azure App Service and look at the next steps for operating and evolving it, so the journey ends with something live, not just something that compiles.

Get started

You can be up and running in just a few minutes.

  1. Make sure you have Visual Studio 2022 (17.10 or later) or Visual Studio 2026 with a GitHub Copilot subscription.
  2. Clone the repository from github.com/microsoft/dotnet-modernization-for-beginners.
  3. Begin with Chapter 0 (Introduction).

Everything you need is in the repo, including step-by-step written instructions and companion videos for each chapter.

We’re not creating this in a vacuum and we want to hear from you. Open some issues to suggest improvements, request new chapters, or report problems. Feel free to submit PRs if you want too! And don’t forget to star the repo if you find it useful!

Legacy code does not have to be overwhelming. Grab the repo, open Visual Studio, and let us modernize .NET together, one application at a time.

The post Announcing .NET Modernization for Beginners appeared first on .NET Blog.

Posted on Leave a comment

PHP curl_multi: Send Multiple API Requests Concurrently

Calling one API from PHP is simple. Calling three APIs one after another is also simple, but it can make the user wait far longer than necessary. Each request gets its own private turn. Very polite. Not very fast.

Suppose a page needs a customer profile, recent orders, and notifications. If those APIs take 700, 1,100, and 900 milliseconds, sequential requests need about 2.7 seconds. With PHP curl_multi, the requests can run concurrently. The total time then stays close to the slowest request, which is about 1.1 seconds in this example.

This tutorial builds a working benchmark that compares both approaches. It also handles timeouts, HTTP status codes, cURL errors, invalid JSON, and connection limits. If you need a refresher on individual requests first, see this PHP cURL guide.

Quick Answer

Use curl_multi_init() to create a multi handle. Add each request with curl_multi_add_handle(), drive the transfers with curl_multi_exec(), and wait efficiently with curl_multi_select().

The requests perform network I/O concurrently. This is not PHP multithreading, and it does not make CPU-heavy work run in parallel.

<?php $urls = [ 'profile' => 'https://api.example.com/profile', 'orders' => 'https://api.example.com/orders', 'notifications' => 'https://api.example.com/notifications'
]; $multiHandle = curl_multi_init();
$handles = []; foreach ($urls as $name => $url) { $handle = curl_init($url); curl_setopt_array($handle, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_TIMEOUT => 10 ]); curl_multi_add_handle($multiHandle, $handle); $handles[$name] = $handle;
} do { $status = curl_multi_exec($multiHandle, $running); if ($status !== CURLM_OK) { throw new RuntimeException(curl_multi_strerror($status)); } if ($running > 0 && curl_multi_select($multiHandle, 1.0) === -1) { usleep(1000); }
} while ($running > 0); $responses = []; foreach ($handles as $name => $handle) { $responses[$name] = curl_multi_getcontent($handle); curl_multi_remove_handle($multiHandle, $handle); curl_close($handle);
} curl_multi_close($multiHandle);

This is the basic flow. The complete project adds response validation, individual error reporting, timing data, and safer request settings.

What This PHP curl_multi Example Builds

The example creates a small dashboard that requests data from three independent API endpoints:

  • A customer profile endpoint with a simulated delay of 700 milliseconds
  • An orders endpoint with a simulated delay of 1,100 milliseconds
  • A notifications endpoint with a simulated delay of 900 milliseconds

The first benchmark sends these requests sequentially. PHP waits for one response before starting the next request.

API request Simulated response time
Customer profile 700 ms
Recent orders 1,100 ms
Notifications 900 ms
Approximate sequential time 2,700 ms

The second benchmark starts all three requests through one cURL multi handle. While one endpoint is waiting, the other transfers can continue. The total time is therefore close to the slowest response, instead of the sum of all three responses.

Request method Approximate total time
Sequential cURL requests 2.7 seconds
Concurrent curl_multi requests 1.1 seconds
Time saved About 1.6 seconds

These numbers are not hard-coded into the dashboard. PHP measures both runs with hrtime(). Small differences between runs are normal, but the concurrent version should remain much closer to the longest individual request.

PHP curl_multi sequential and concurrent API request benchmark

PHP curl_multi completes three concurrent API requests faster than sequential cURL requests.

The performance improvement comes from overlapping network wait time. It does not make an individual API respond faster. If one endpoint takes ten seconds, the complete group can still take about ten seconds. PHP cannot persuade a slow API to drink more coffee.

How PHP curl_multi Works

A normal curl_exec() call blocks the PHP script until that transfer finishes. When several calls are placed inside a loop, the waiting time grows with every request.

The cURL multi interface changes the flow. It keeps several individual cURL handles inside one multi handle and lets their network activity progress together.

  1. Create one normal cURL handle for each API URL.
  2. Create a multi handle with curl_multi_init().
  3. Add every request with curl_multi_add_handle().
  4. Call curl_multi_exec() until no transfer is running.
  5. Use curl_multi_select() while waiting for network activity.
  6. Read each response with curl_multi_getcontent().
  7. Remove and close all handles.

Why curl_multi_exec() Runs Inside a Loop

A single call to curl_multi_exec() does not mean every request has finished. It only asks libcurl to perform the work that is currently possible.

The $running argument receives the number of active transfers. PHP must keep calling the function until that value reaches zero.

do { $status = curl_multi_exec($multiHandle, $running);
} while ($running > 0);

This loop works, but it has a problem. It can repeatedly call curl_multi_exec() while nothing is ready, wasting CPU time.

Use curl_multi_select() to Avoid a Busy Loop

curl_multi_select() pauses the script until one of the active connections can make progress or the timeout expires. This is more efficient than checking the transfers continuously.

do { $status = curl_multi_exec($multiHandle, $running); if ($status !== CURLM_OK) { throw new RuntimeException(curl_multi_strerror($status)); } if ($running > 0) { $ready = curl_multi_select($multiHandle, 1.0); if ($ready === -1) { usleep(1000); } }
} while ($running > 0);

The short sleep handles a lesser-known edge case. Some libcurl builds can return -1 when no file descriptor is ready. Without the sleep, the loop may spin quickly and consume unnecessary CPU.

Multi Errors and Request Errors Are Different

The result from curl_multi_exec() reports errors affecting the complete multi stack. A CURLM_OK result does not guarantee that every API request succeeded.

Each completed handle must still be checked separately for:

  • Connection failures reported by curl_error()
  • Timeouts and other transfer errors
  • HTTP error responses such as 404 or 500
  • Empty or invalid JSON response bodies

This distinction is easy to miss. The multi operation may succeed perfectly while one API quietly returns an error page wearing a JSON name tag.

PHP curl_multi Project Structure

This example uses plain PHP. It does not need a framework, Composer package, JavaScript library, or database.

The project keeps the HTTP client separate from the page that displays the benchmark. It also includes a local mock API, so the timing test does not depend on an external service.

php-curl-multi/
├── config.php
├── mock-api/
│ └── index.php
├── public/
│ ├── assets/
│ │ └── style.css
│ └── index.php
├── src/
│ └── ApiClient.php
└── README.md
  • config.php contains the API URL, timeouts, and connection limit.
  • mock-api/index.php returns sample JSON responses with controlled delays.
  • src/ApiClient.php sends sequential and concurrent requests.
  • public/index.php runs the benchmark and displays the results.
  • public/assets/style.css provides the small responsive layout.

Create the Configuration File

The configuration keeps values that may change between development and production outside the HTTP client class.

<?php declare(strict_types=1); return [ 'api_base_url' => rtrim( getenv('DEMO_API_BASE_URL') ?: 'http://127.0.0.1:8001', '/' ), 'connect_timeout_ms' => 1000, 'request_timeout_ms' => 5000, 'max_concurrent_requests' => 5,
];

The connection timeout controls how long cURL may spend establishing a connection. The request timeout covers the complete transfer.

The concurrency limit prevents the application from opening an excessive number of connections at once. This demo sends only three requests, but keeping the limit in the configuration makes the client safer to reuse.

Create the Local Mock API

The mock API accepts a resource query parameter. It returns profile, order, or notification data after a short delay.

Create mock-api/index.php with the following code:

<?php declare(strict_types=1); header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store'); $resource = $_GET['resource'] ?? ''; $responses = [ 'profile' => [ 'delay_ms' => 700, 'data' => [ 'name' => 'Maya Chen', 'email' => 'maya@example.com', 'membership' => 'Gold', ], ], 'orders' => [ 'delay_ms' => 1100, 'data' => [ 'count' => 3, 'latest_order' => '#1048', 'total' => '$184.50', ], ], 'notifications' => [ 'delay_ms' => 900, 'data' => [ 'unread' => 4, 'latest' => 'Your order has been shipped.', ], ],
]; if (!isset($responses[$resource])) { http_response_code(404); echo json_encode([ 'error' => 'Unknown API resource.', ], JSON_THROW_ON_ERROR); exit;
} $response = $responses[$resource]; usleep($response['delay_ms'] * 1000); echo json_encode([ 'resource' => $resource, 'simulated_delay_ms' => $response['delay_ms'], 'data' => $response['data'],
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);

The delay is intentional. It makes the difference between sequential and concurrent requests easy to see. In a real project, the waiting time would usually come from a remote API, database-backed service, payment gateway, or another server.

The API also returns a proper 404 response for an unknown resource. This gives the client a realistic HTTP error to handle instead of assuming that every response will be successful.

Create the Reusable PHP API Client

Create src/ApiClient.php. This class contains both request methods, so the benchmark can compare them under the same timeout and response-handling rules.

<?php declare(strict_types=1); final class ApiClient
{ public function __construct( private readonly int $connectTimeoutMs = 1000, private readonly int $requestTimeoutMs = 5000, private readonly int $maxConcurrentRequests = 5 ) { } public function fetchSequential(array $requests): array { $startedAt = hrtime(true); $responses = []; foreach ($requests as $name => $url) { $handle = $this->createHandle($url); $body = curl_exec($handle); $responses[$name] = $this->buildResponse( $handle, $body ); curl_close($handle); } return [ 'duration_ms' => $this->elapsedMilliseconds($startedAt), 'responses' => $responses, ]; } public function fetchConcurrent(array $requests): array { $startedAt = hrtime(true); $multiHandle = curl_multi_init(); $handles = []; curl_multi_setopt( $multiHandle, CURLMOPT_MAX_TOTAL_CONNECTIONS, $this->maxConcurrentRequests ); try { foreach ($requests as $name => $url) { $handle = $this->createHandle($url); $handles[$name] = [ 'handle' => $handle, ]; $status = curl_multi_add_handle( $multiHandle, $handle ); if ($status !== CURLM_OK) { throw new RuntimeException( curl_multi_strerror($status) ); } } do { $status = curl_multi_exec( $multiHandle, $running ); if ($status !== CURLM_OK) { throw new RuntimeException( curl_multi_strerror($status) ); } if ($running > 0) { $ready = curl_multi_select( $multiHandle, 1.0 ); if ($ready === -1) { usleep(1000); } } } while ($running > 0); $responses = []; foreach ($handles as $name => $item) { $handle = $item['handle']; $body = curl_multi_getcontent($handle); $responses[$name] = $this->buildResponse( $handle, $body ); } return [ 'duration_ms' => $this->elapsedMilliseconds( $startedAt ), 'responses' => $responses, ]; } finally { foreach ($handles as $item) { curl_multi_remove_handle( $multiHandle, $item['handle'] ); curl_close($item['handle']); } curl_multi_close($multiHandle); } } private function createHandle(string $url): CurlHandle { $handle = curl_init($url); curl_setopt_array($handle, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT_MS => $this->connectTimeoutMs, CURLOPT_TIMEOUT_MS => $this->requestTimeoutMs, CURLOPT_HTTPHEADER => [ 'Accept: application/json' ], CURLOPT_USERAGENT => 'PHPpot-curl-multi-demo/1.0', CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, ]); return $handle; } private function buildResponse( CurlHandle $handle, string|bool $body ): array { $curlError = curl_error($handle); $statusCode = (int) curl_getinfo( $handle, CURLINFO_RESPONSE_CODE ); $durationMs = round( (float) curl_getinfo( $handle, CURLINFO_TOTAL_TIME ) * 1000, 1 ); if ($body === false || $curlError !== '') { return [ 'ok' => false, 'status' => $statusCode, 'duration_ms' => $durationMs, 'data' => null, 'error' => $curlError !== '' ? $curlError : 'The request failed.', ]; } if ($statusCode < 200 || $statusCode >= 300) { return [ 'ok' => false, 'status' => $statusCode, 'duration_ms' => $durationMs, 'data' => null, 'error' => 'The API returned HTTP status ' . $statusCode . '.', ]; } try { $data = json_decode( $body, true, 512, JSON_THROW_ON_ERROR ); } catch (JsonException $exception) { return [ 'ok' => false, 'status' => $statusCode, 'duration_ms' => $durationMs, 'data' => null, 'error' => 'Invalid JSON response: ' . $exception->getMessage(), ]; } return [ 'ok' => true, 'status' => $statusCode, 'duration_ms' => $durationMs, 'data' => $data, 'error' => null, ]; } private function elapsedMilliseconds( int $startedAt ): float { return round( (hrtime(true) - $startedAt) / 1_000_000, 1 ); }
}

How the Sequential Method Works

fetchSequential() creates and executes one handle at a time. The next loop iteration cannot begin until curl_exec() returns.

This is useful as the baseline. It shows how much time the application would spend if it made the same API calls without concurrency.

How the Concurrent Method Works

fetchConcurrent() creates the same individual handles, but adds them to one multi handle before execution begins.

The associative request name is kept with each handle. This lets the method return predictable keys such as profile, orders, and notifications, even if the responses finish in a different order.

The finally block removes and closes every handle even when an exception occurs. Network code has enough ways to misbehave without leaving cleanup to good luck.

Validate Every API Response

The buildResponse() method treats transport errors, HTTP errors, and invalid JSON as separate failures. This makes error messages more useful during debugging.

Successful HTTP transport does not guarantee valid JSON. The example uses JSON_THROW_ON_ERROR so malformed responses cannot silently become null. For more examples, see the PHPpot guide to PHP JSON encode and decode.

Each result follows the same structure:

[ 'ok' => true, 'status' => 200, 'duration_ms' => 703.4, 'data' => [ // Decoded API response ], 'error' => null,
]

A consistent response format keeps the display code simple. It also prevents successful data and error messages from becoming an exciting collection of special cases.

Build the Benchmark Page

Create public/index.php. This page defines the three API requests, runs both client methods, and displays the measured time.

<?php declare(strict_types=1); require_once dirname(__DIR__) . '/src/ApiClient.php'; $config = require dirname(__DIR__) . '/config.php'; $error = null;
$sequential = null;
$concurrent = null; if (!extension_loaded('curl')) { $error = 'The PHP cURL extension is not enabled.';
} else { $requests = [ 'profile' => $config['api_base_url'] . '/?resource=profile', 'orders' => $config['api_base_url'] . '/?resource=orders', 'notifications' => $config['api_base_url'] . '/?resource=notifications', ]; try { $client = new ApiClient( $config['connect_timeout_ms'], $config['request_timeout_ms'], $config['max_concurrent_requests'] ); $sequential = $client->fetchSequential($requests); $concurrent = $client->fetchConcurrent($requests); } catch (Throwable $exception) { $error = $exception->getMessage(); }
} function escape(mixed $value): string
{ return htmlspecialchars( (string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
} function seconds(float $milliseconds): string
{ return number_format( $milliseconds / 1000, 2 ) . ' seconds';
} $timeSaved = $sequential && $concurrent ? max( 0, $sequential['duration_ms'] - $concurrent['duration_ms'] ) : 0;
?>
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0" > <title>PHP curl_multi API Benchmark</title> <link rel="stylesheet" href="assets/style.css">
</head>
<body>
<main class="page-shell"> <header class="page-header"> <p class="eyebrow">PHP cURL benchmark</p> <h1>Sequential vs concurrent API requests</h1> <p class="intro"> The same three API endpoints are called twice. The first run waits for each response. The second run uses <code>curl_multi</code>. </p> </header> <?php if ($error !== null): ?> <div class="message message-error"> <strong>Unable to run the benchmark.</strong> <span><?= escape($error) ?></span> </div> <?php else: ?> <section class="benchmark-grid" aria-label="Benchmark results" > <article class="metric-card"> <span class="metric-label"> Sequential requests </span> <strong> <?= escape( seconds($sequential['duration_ms']) ) ?> </strong> <small> One request finishes before the next starts. </small> </article> <article class="metric-card metric-card-highlight" > <span class="metric-label"> Concurrent requests </span> <strong> <?= escape( seconds($concurrent['duration_ms']) ) ?> </strong> <small> All requests wait for network responses together. </small> </article> <article class="metric-card"> <span class="metric-label"> Time saved </span> <strong> <?= escape(seconds($timeSaved)) ?> </strong> <small> Your result will vary slightly between runs. </small> </article> </section> <section class="results-section"> <div class="section-heading"> <div> <p class="eyebrow"> Concurrent response details </p> <h2>One result for each API</h2> </div> <a class="button" href="index.php"> Run benchmark again </a> </div> <div class="response-grid"> <?php foreach ( $concurrent['responses'] as $name => $response ): ?> <article class="response-card"> <div class="response-title"> <h3> <?= escape(ucfirst($name)) ?> </h3> <span class="status <?= $response['ok'] ? 'status-ok' : 'status-error' ?>"> HTTP <?= escape($response['status']) ?> </span> </div> <p class="response-time"> Completed in <?= escape( $response['duration_ms'] ) ?> ms </p> <?php if ($response['ok']): ?> <pre><?= escape( json_encode( $response['data']['data'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ) ?></pre> <?php else: ?> <div class="message message-error" > <?= escape( $response['error'] ) ?> </div> <?php endif; ?> </article> <?php endforeach; ?> </div> </section> <?php endif; ?>
</main>
</body>
</html>

Run the Same Request List Twice

Both methods receive the same associative array of API URLs. This keeps the comparison fair and makes it easy to connect each response to its purpose.

The total duration is measured inside the client. The page subtracts the concurrent duration from the sequential duration to calculate the time saved.

Escape API Data Before Displaying It

API responses are external input. Even a trusted service can return unexpected content after a configuration mistake or security incident.

The escape() function applies htmlspecialchars() before values are printed into the page. JSON shown inside the response cards is escaped as well. Receiving JSON does not make its values automatically safe for HTML output.

The page also checks whether the cURL extension is available. If it is missing, the user sees a useful message instead of PHP introducing the problem with a fatal error.

Run the PHP curl_multi Project Locally

Make sure PHP 8.1 or later is installed and the cURL extension is enabled.

You can confirm the extension from the command line:

php -m | grep curl

If cURL is enabled, the command prints curl.

Start the Mock API

Open a terminal in the project directory. On macOS or Linux, start the mock API with multiple PHP development-server workers:

PHP_CLI_SERVER_WORKERS=4 php -S 127.0.0.1:8001 -t mock-api

The mock API is now available at URLs such as:

http://127.0.0.1:8001/?resource=profile
http://127.0.0.1:8001/?resource=orders
http://127.0.0.1:8001/?resource=notifications

Start the Demo Page

Open a second terminal in the same project directory:

php -S 127.0.0.1:8000 -t public

Then open the following URL in a browser:

http://127.0.0.1:8000

The page runs both benchmarks automatically. With the supplied delays, the sequential test should take about 2.7 seconds. The concurrent test should finish in about 1.1 seconds.

Important PHP Development Server Caveat

PHP’s built-in development server uses one worker by default. A single worker can process only one request at a time.

If the benchmark page and mock endpoints are placed on the same single-worker server, the requests may become sequential. They may even wait forever because the current PHP request is trying to call another URL handled by the worker it already occupies.

This is why the example uses two ports and starts the mock API with multiple workers. It is not merely decorative terminal activity.

Run the Project with XAMPP, MAMP, or Apache

You can also place the project inside your local web root. Apache normally has multiple workers available, so it can serve the mock API requests concurrently.

For example, the URLs may be:

Demo page:
http://localhost/php-curl-multi/public/ Mock API:
http://localhost/php-curl-multi/mock-api/

Update api_base_url in config.php:

'api_base_url' => 'http://localhost/php-curl-multi/mock-api',

You can also set the API URL through an environment variable:

export DEMO_API_BASE_URL="http://localhost/php-curl-multi/mock-api"

If the concurrent result is almost as slow as the sequential result, check the mock API server first. The most common cause is a local server that still processes the API requests one at a time.

When Concurrent API Requests Improve Performance

curl_multi works best when a PHP page needs several independent HTTP responses before it can continue.

Good examples include:

  • Loading profile, order, and notification data from separate services
  • Collecting prices from several supplier APIs
  • Checking the status of multiple remote servers
  • Fetching reports from independent endpoints
  • Sending the same webhook payload to several destinations

The important word is independent. If one request needs data returned by another request, those two calls cannot start together.

Independent Requests Can Run Concurrently

$requests = [ 'profile' => $profileUrl, 'orders' => $ordersUrl, 'notifications' => $notificationsUrl,
]; $results = $client->fetchConcurrent($requests);

None of these URLs depends on another response. They are good candidates for curl_multi.

Dependent Requests Must Keep Their Order

$loginResponse = sendRequest($loginUrl); $accessToken = $loginResponse['access_token']; $profileResponse = sendAuthenticatedRequest( $profileUrl, $accessToken
);

The profile request needs the access token returned by the login request. Starting both calls together would only help them fail faster.

When curl_multi Will Not Help

Situation Why curl_multi does not solve it
One slow API request There is no other network wait time to overlap.
CPU-heavy PHP calculations curl_multi manages network transfers, not PHP computation.
Requests that depend on earlier responses Later calls cannot start until the required data exists.
An API with strict rate limits More simultaneous requests may trigger throttling.
A server that handles one request at a time The remote side still processes the calls sequentially.

The Slowest Request Still Sets the Pace

Concurrent requests reduce the total from approximately the sum of all response times to approximately the longest response time.

For three requests taking one, two, and five seconds:

  • Sequential time is approximately eight seconds.
  • Concurrent time is approximately five seconds.

The five-second API remains a five-second API. curl_multi simply stops the other requests from waiting in line behind it.

Do Not Send Hundreds of Requests at Once

Concurrency needs a limit. Opening too many connections can increase memory use, overload the remote service, or cause HTTP 429 responses.

The example limits the complete multi handle with:

curl_multi_setopt( $multiHandle, CURLMOPT_MAX_TOTAL_CONNECTIONS, 5
);

Choose the limit based on the API documentation, server capacity, and rate policy. Five or ten concurrent requests may be reasonable for one service. Five hundred is usually a creative way to meet its security team.

Common PHP curl_multi Errors and Fixes

curl_multi_exec() Returns CURLM_OK, but a Request Failed

CURLM_OK means the multi stack operated correctly. It does not mean every individual transfer succeeded.

Check each handle after execution:

$curlError = curl_error($handle); $statusCode = (int) curl_getinfo( $handle, CURLINFO_RESPONSE_CODE
); if ($curlError !== '') { echo 'cURL error: ' . $curlError;
} if ($statusCode < 200 || $statusCode >= 300) { echo 'HTTP error: ' . $statusCode;
}

A request can time out, fail DNS lookup, or return HTTP 500 while the multi handle itself remains perfectly content.

curl_multi_select() Returns -1

Some libcurl builds may return -1 when no file descriptor is ready. Add a short sleep before continuing the loop:

$ready = curl_multi_select($multiHandle, 1.0); if ($ready === -1) { usleep(1000);
}

This prevents the loop from repeatedly checking the connections and consuming unnecessary CPU.

The Concurrent Version Is Not Faster

Check whether the requests are truly independent and whether the API server can process more than one request at a time.

During local testing, a single-worker PHP development server is a common cause. Use multiple API workers or run the mock endpoints through Apache or Nginx.

Concurrency may also provide little improvement when:

  • The API responses are already extremely fast.
  • Most of the time is spent processing data after download.
  • The remote server queues requests from the same client.
  • The API applies a low concurrency limit.

A Request Never Finishes

Every handle should have both a connection timeout and a total timeout:

curl_setopt_array($handle, [ CURLOPT_CONNECTTIMEOUT_MS => 1000, CURLOPT_TIMEOUT_MS => 5000,
]);

The connection timeout covers DNS lookup and connection setup. The total timeout limits the complete request.

Without these values, one unresponsive endpoint can keep the entire group waiting. Concurrent does not mean immortal.

The API Returns HTTP 429

HTTP 429 means the service is rate-limiting the client. Reduce the concurrency limit and inspect the response headers for retry information.

A production client may need to:

  • Respect the API’s documented request limit.
  • Wait for the duration given by a Retry-After header.
  • Retry only safe requests.
  • Use exponential backoff instead of retrying immediately.

Do not treat retries as permission to hammer the same endpoint with greater determination.

json_decode() Returns null

A response may contain invalid JSON, an empty body, or an HTML error page. Use JSON_THROW_ON_ERROR to make the failure visible:

try { $data = json_decode( $body, true, 512, JSON_THROW_ON_ERROR );
} catch (JsonException $exception) { $error = 'Invalid JSON response: ' . $exception->getMessage();
}

Check the HTTP status before decoding. A server returning an HTML 500 page is not a JSON parsing mystery. It is an HTTP error wearing the wrong outfit.

Responses Are Matched to the Wrong Request

Do not depend on completion order. A fast endpoint may finish before a request that was added earlier.

Store each handle with a stable name:

foreach ($requests as $name => $url) { $handle = curl_init($url); $handles[$name] = $handle; curl_multi_add_handle( $multiHandle, $handle );
}

When reading the results, use the same name as the response key. This keeps profile data attached to profile, even when notifications finish first.

Security Considerations

Concurrent requests do not introduce a completely new security model, but they can multiply the effect of a bad URL, missing timeout, or leaked credential. Apply the same controls to every handle.

Do Not Accept Arbitrary Request URLs

The example builds its URLs from a trusted configuration value. Do not pass a URL from $_GET, $_POST, or another untrusted source directly to curl_init().

// Unsafe
$url = $_GET['url'] ?? ''; $handle = curl_init($url);

This can create a server-side request forgery vulnerability. An attacker may try to access internal services, cloud metadata endpoints, or private network addresses through your server.

For a production integration, keep API endpoints in configuration or restrict requests to an explicit list of trusted hosts.

function isAllowedApiUrl( string $url, array $allowedHosts
): bool { $parts = parse_url($url); if ( !isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'https' ) { return false; } return in_array( strtolower($parts['host']), $allowedHosts, true );
} $allowedHosts = [ 'api.example.com', 'payments.example.com',
]; if (!isAllowedApiUrl($url, $allowedHosts)) { throw new InvalidArgumentException( 'The API URL is not allowed.' );
}

A hostname allowlist is only one layer. Applications that fetch user-influenced URLs should also protect against hostnames resolving to private IP addresses and DNS rebinding.

Restrict Supported Protocols

Limit cURL to the protocols the application actually needs:

CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,

This prevents an unexpected URL from switching to protocols such as FTP or local file access.

Handle Redirects Carefully

The example disables automatic redirects:

CURLOPT_FOLLOWLOCATION => false,

A trusted public URL can redirect to an untrusted or private address. If redirects are required, validate every destination and set a small redirect limit.

CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,

Protocol restriction alone does not stop redirects to private HTTP addresses. The destination still needs validation.

Keep TLS Verification Enabled

For HTTPS APIs, PHP cURL verifies the certificate and hostname by default. Do not disable these checks to silence a certificate error.

// Do not use these settings in production.
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0,

Fix the server certificate or local certificate store instead. Disabling verification allows a network attacker to intercept API credentials and responses.

Keep API Credentials Outside the Source Code

Read tokens from environment variables or a secret manager:

$apiToken = getenv('API_TOKEN'); if (!$apiToken) { throw new RuntimeException( 'The API token is not configured.' );
} curl_setopt($handle, CURLOPT_HTTPHEADER, [ 'Accept: application/json', 'Authorization: Bearer ' . $apiToken,
]);

Do not print authorization headers in browser errors or write complete tokens into application logs.

Limit Time, Connections, and Response Size

Timeouts and connection limits protect application resources when an API becomes slow or unavailable. For APIs that may return large files or untrusted content, also enforce a maximum response size.

Without limits, five concurrent requests can become five simultaneous ways to exhaust memory.

Escape Response Data

JSON values are not safe HTML merely because they arrived from an API. Escape values with htmlspecialchars() before placing them on a page.

This matters even for a trusted service. Accounts can be compromised, stored data can contain markup, and APIs occasionally return surprises that were not included in the integration meeting.

Developer FAQ

Is PHP curl_multi truly parallel?

curl_multi performs concurrent network transfers. Several requests can make progress during the same period, but your PHP code is not executing in multiple CPU threads.

For HTTP requests, concurrent is the more accurate term. Developers often search for “parallel cURL requests,” so both descriptions are commonly used.

Does curl_multi_exec() run in the background?

No. The PHP script still waits until the multi loop finishes. The difference is that it waits for several active transfers together instead of completing them one by one.

If work must continue after the web request ends, use a queue, worker, scheduled job, or another background-processing system.

Can curl_multi send POST requests?

Yes. Each handle can have its own HTTP method, headers, and request body.

$handle = curl_init($url); curl_setopt_array($handle, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode( $payload, JSON_THROW_ON_ERROR ), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Accept: application/json', ], CURLOPT_RETURNTRANSFER => true,
]);

The configured handle can then be added with curl_multi_add_handle(). See the PHP cURL POST example for more details about sending form data and JSON request bodies.

Can GET and POST requests be mixed in one multi handle?

Yes. Every easy handle keeps its own options. One handle can send GET, another can send POST, and another can include authentication headers.

The multi handle manages when their network transfers progress. It does not require every request to use the same method or destination.

Are responses returned in the same order as the URLs?

Requests may finish in any order. Do not assume that the first completed response belongs to the first URL.

Store each handle with a stable associative key and return results using that key. The example uses profile, orders, and notifications.

How many concurrent requests should PHP send?

There is no universal safe number. It depends on the remote API, response size, available memory, connection limits, and rate policy.

Start with a small value such as five. Measure the result and respect any limits documented by the API provider. More concurrency is not automatically more performance.

Why use curl_multi_select() instead of usleep() in every loop?

curl_multi_select() waits for actual network activity. A fixed sleep may pause longer than necessary or wake repeatedly when no transfer can make progress.

A short usleep() is used only when curl_multi_select() returns -1.

Does curl_multi make a slow API faster?

No. It reduces unnecessary waiting between independent requests. The complete group still depends on its slowest request.

If one API is consistently slow, improve that service, add appropriate caching, request less data, or move nonessential work to a background process.

Can curl_multi be used for file downloads?

Yes. It can download several files concurrently. For large files, write each response directly to a file instead of keeping every body in memory.

Also limit concurrency and maximum file size. A small JSON response and a two-gigabyte archive have very different opinions about available memory.

Final Takeaway

PHP curl_multi is useful when one operation needs responses from several independent APIs. It starts the transfers together, waits efficiently for network activity, and reduces total waiting time to roughly the duration of the slowest request.

The important parts are not limited to calling curl_multi_exec(). A reliable implementation should also:

  • Use curl_multi_select() to avoid a busy loop.
  • Set connection and total-request timeouts.
  • Check cURL errors for every handle.
  • Validate HTTP status codes separately.
  • Handle invalid JSON responses.
  • Keep responses associated with stable request names.
  • Limit the number of concurrent connections.
  • Restrict URLs, protocols, and redirects.

Concurrency is not helpful for every problem. It will not accelerate CPU-heavy PHP code, remove API rate limits, or make a single slow endpoint respond faster. But when several independent HTTP calls are holding up a page, it can remove a surprising amount of avoidable waiting.

Download the PHP curl_multi Project

The downloadable ZIP contains the complete working project, including the reusable API client, local mock API, benchmark page, stylesheet, configuration, and setup instructions.

Download the PHP curl_multi concurrent API requests project

Extract the ZIP, follow the instructions in README.md, and run the mock API and demo page on separate local ports.

Posted on Leave a comment

AI-Powered MSBuild Investigation with the Microsoft Binlog MCP Server

MSBuild binary logs (.binlog files) contain a wealth of information about
your build — every property evaluation, target execution, task invocation,
error, and warning. But navigating that data manually can be overwhelming,
especially when you’re debugging a complex multi-project solution. What if your
AI coding assistant could do the investigation for you?

Today we’re introducing the Microsoft Binlog MCP Server, a
Model Context Protocol (MCP) server that
gives AI assistants like GitHub Copilot direct access to your build logs. It
parses .binlog files and exposes 15 specialized tools that enable AI-driven
build failure diagnosis, property tracing, performance analysis, and build
comparison — all through natural language conversation.

Why MCP for Build Logs?

The Model Context Protocol is an open
standard that lets AI assistants call external tools in a structured way. By
wrapping MSBuild binary log analysis in an MCP server, we give AI assistants
the ability to:

  • Investigate build failures by querying errors, warnings, and their full
    project/target/task context
  • Trace property origins to understand where a property got its value
  • Analyze performance bottlenecks by identifying the slowest projects,
    targets, and tasks
  • Compare two builds to spot differences in properties and packages
  • Read embedded source files captured during the build

Instead of manually scrolling through the
MSBuild Structured Log Viewer, you can simply ask
your AI assistant questions like “Why did my build fail?” or
“What’s making my build slow?”

15 Tools at Your AI Assistant’s Disposal

The Microsoft Binlog MCP Server provides tools organized into four
categories:

Build Investigation

Tool What It Does
binlog_overview Build status, duration, project count, error/warning counts
binlog_errors Build errors with full project, target, task, file, and line context
binlog_warnings Build warnings, filterable by warning code
binlog_search Full-text search using the StructuredLog Viewer search DSL
binlog_projects List all projects with build status and duration
binlog_properties MSBuild property values (curated defaults or filtered)
binlog_items MSBuild items like PackageReference, Compile, and more
binlog_imports Full import chain of .props and .targets files
binlog_explain_property Traces where a property gets its value — which file, target, or task set it

Embedded Files

Tool What It Does
binlog_files List or read source files captured during the build
binlog_search_files Search text across all embedded source files

Performance Analysis

Tool What It Does
binlog_expensive_projects Slowest projects by exclusive duration
binlog_expensive_targets Slowest targets across the entire build
binlog_expensive_tasks Slowest tasks across the entire build

Build Comparison

Tool What It Does
binlog_compare Diff two binlogs — compare properties, packages, and more

Getting Started

The easiest way to get started is through the
.NET Agent Skills repository. The
dotnet-msbuild plugin bundles the Microsoft Binlog MCP Server along with
curated skills and agents for MSBuild build investigation and optimization.
Pick the section below that matches your development environment.

Visual Studio

Visual Studio supports MCP servers through GitHub Copilot’s agent mode
(Visual Studio 17.14 or later). After installing the dotnet-msbuild
plugin, the Microsoft Binlog MCP Server is automatically discovered by
Copilot Chat in agent mode. Open the Copilot Chat window, switch to
Agent mode, and the binlog_* tools become available for any
conversation about a .binlog file in your solution.

Visual Studio Code

In VS Code, enable plugin support and add the marketplace to your
settings.json:

{ "chat.plugins.enabled": true, "chat.plugins.marketplaces": ["dotnet/skills"]
}

Then install the dotnet-msbuild plugin from the marketplace — the
Binlog MCP Server is configured automatically.

Prefer to wire up the MCP server directly? Add it to your
.vscode/mcp.json:

{ "servers": { "binlog-mcp": { "type": "stdio", "command": "dotnet", "args": ["tool", "run", "Microsoft.AITools.BinlogMcp"] } }
}

To pre-load a specific binlog at startup, pass the --binlog argument:

{ "servers": { "binlog-mcp": { "type": "stdio", "command": "dotnet", "args": ["tool", "run", "Microsoft.AITools.BinlogMcp", "--", "--binlog", "msbuild.binlog"] } }
}

Command Line (Copilot CLI / Claude Code)

For terminal-based AI assistants such as GitHub Copilot CLI or Claude
Code, install the plugin directly from the dotnet/skills marketplace:

/plugin marketplace add dotnet/skills
/plugin install dotnet-msbuild@dotnet-agent-skills

Restart your assistant and the binlog_* tools are ready to use. You can
verify they loaded with /skills.

Tip

To generate a binary log, add /bl to any
dotnet build, dotnet test, or dotnet pack command — for example:
dotnet build /bl.

Example: Diagnosing a Build Failure

Once the MCP server is running and your AI assistant has access to a
.binlog file, you can investigate build issues conversationally.

Here’s a typical workflow:

  1. Generate a binlog: Run dotnet build /bl to capture a binary log
  2. Ask your assistant: “My build failed. Can you investigate
    msbuild.binlog and tell me what went wrong?”
  3. The AI investigates: It calls binlog_overview to get the high-level
    status, then binlog_errors to retrieve the actual errors with full
    context, and may use binlog_explain_property or binlog_search to trace
    the root cause
  4. Get actionable guidance: The assistant synthesizes findings and suggests
    concrete fixes

For performance investigations, the AI uses the binlog_expensive_projects,
binlog_expensive_targets, and binlog_expensive_tasks tools to identify
bottlenecks and recommend optimizations.

The screenshot below shows this workflow in action inside VS Code.

GitHub Copilot in VS Code agent mode calling binlog MCP tools to diagnose an MSB4044 build failure

Try It Yourself: Compare Two Builds

Here’s a great way to take the MCP server for a spin right now. Pick a
repository you build regularly — your own product, or an open-source
project like dotnet/msbuild or
microsoft/testfx — and capture
two binary logs from different versions or configurations:

# Build version A
git checkout main
dotnet build /bl:build-a.binlog # Build version B (a different branch, SDK, or configuration)
git checkout my-feature-branch
dotnet build /bl:build-b.binlog

Then ask your AI assistant:

“Compare build-a.binlog and build-b.binlog. What MSBuild properties
and package versions changed, and did any of those changes affect build
performance?”

Behind the scenes, the assistant calls binlog_compare to diff properties
and packages, then uses binlog_expensive_projects and
binlog_expensive_targets on both logs to correlate the changes with
timing differences — turning what used to be a tedious side-by-side log
comparison into a single conversation.

Built on StructuredLogger

Under the hood, the Microsoft Binlog MCP Server uses the
MSBuild Structured Log Viewer
library — the same engine that powers the popular MSBuild Structured Log Viewer
desktop app. The binlog_search tool supports the full
StructuredLog Viewer search DSL,
including node type filters ($error, $warning, $task, $target,
$project), hierarchical scoping with under(), and exact phrase matching
with quoted strings.

Telemetry

The server emits anonymous usage telemetry (tool name, latency, result size,
success/failure) to help us improve the product. It follows the standard .NET
SDK approach: on by default, single opt-out via the
DOTNET_CLI_TELEMETRY_OPTOUT environment variable.

export DOTNET_CLI_TELEMETRY_OPTOUT=1

No binlog content, file paths, or raw error messages are ever collected — only
filenames are HMAC-SHA256 hashed for correlation.

What’s Next

The Microsoft Binlog MCP Server is in preview and we’re actively improving
it. We’d love your feedback — please file issues in the
dotnet/skills repository.

If you’re working with MSBuild builds and using AI coding assistants, give it
a try. Let your AI do the heavy lifting of build investigation while you focus
on writing code.

Posted on Leave a comment

Secure File Upload in PHP 8: A Production-Ready Implementation Guide

Why File Uploads Are a High Risk Attack Surface

File uploads are one of the most common features in web applications. They are also one of the most exploited.

In PHP 8, securely handling file uploads requires far more than calling move_uploaded_file(). A production ready implementation must validate MIME types using finfo, restrict file size, whitelist allowed formats, generate cryptographically safe file names, store files outside the public directory, and enforce server level execution restrictions.

That is the technical summary. But the real story is deeper.

File uploads look harmless.

A resume upload field.
A profile picture form.
An assignment submission box in an LMS.
A document attachment in a billing system.

Years ago, a small business site was compromised. The attacker did not brute force passwords. They did not exploit SQL injection. They uploaded a file named invoice.pdf.php. The system trusted the extension, saved it inside the public folder, and allowed the web server to execute it.

Within minutes, the server was running malicious scripts.

The feature designed to collect documents became the entry point.

The problem was not PHP.
No programming language is insecure by default. Insecure assumptions create insecure systems.

Developers often:

  • Trust file extensions
  • Trust $_FILES['type']
  • Store uploads inside public directories
  • Skip server hardening
  • Focus on making it work instead of making it safe

File upload security is not about one validation check. It is about layered defense. Just like preventing SQL injection in PHP, file uploads require strict validation.

In this guide, we will design a production ready, security first file upload implementation in PHP 8. We will examine the attack surface, define strict validation rules, isolate storage, apply server level hardening, and build a clean, minimal uploader class suitable for real world backend systems.

Because in backend engineering, the most dangerous vulnerabilities are often hidden behind the simplest features. If you are looking for a basic file upload example, see this simple PHP file upload tutorial.

How PHP Handles File Uploads Internally

Before securing file uploads, we must understand how PHP handles them.

When a user submits a form with enctype="multipart/form-data", the browser sends the file to the server along with the other form fields.

PHP does not immediately store the file in your project folder.

Instead, it saves the file in a temporary directory on the server. This location is defined by the upload_tmp_dir setting in php.ini. If not defined, PHP uses the system default temp folder.

After the upload is complete, PHP creates an entry inside the $_FILES superglobal array.

A typical $_FILES structure looks like this:

Array
( [document] => Array ( [name] => resume.pdf [type] => application/pdf [tmp_name] => /tmp/phpYzdqkD [error] => 0 [size] => 124532 )
)

Each key has a meaning:

  • name → Original file name from the user. Do not trust this.
  • type → MIME type reported by the browser. Do not trust this.
  • tmp_name → Temporary file path created by PHP.
  • error → Upload status code. Must be checked.
  • size → File size in bytes. Should be validated.

It is important to understand this clearly.

The browser controls name and type. The user can manipulate them.

Only tmp_name is generated by the server.

To permanently store the file, you must call:

move_uploaded_file($file['tmp_name'], $destination);

You can read more in the official PHP documentation for move_uploaded_file().

This function moves the file from the temporary directory to your chosen location.

If you skip validation and directly move the file, you are trusting user input. That is where problems start.

There are also PHP configuration limits that affect uploads:

  • upload_max_filesize
  • post_max_size
  • max_file_uploads

These limits are helpful, but they are not security controls. They only restrict size and quantity.

Understanding this upload lifecycle is important. Security mistakes usually happen between reading $_FILES and calling move_uploaded_file().

File upload forms should also be protected against CSRF attacks.

In the next section, we will see the common vulnerabilities that arise during this phase.

Common File Upload Vulnerabilities

File uploads fail not because of one mistake.
They fail because of small assumptions.

Here are the most common problems.

1. Trusting the File Extension

Many systems check only the extension.

Example:


resume.pdf
image.jpg

Looks safe.

But an attacker can upload:


shell.php
shell.php.jpg
invoice.pdf.php

If your system only checks .jpg or .pdf, it can be bypassed.

Extensions are easy to fake. They are just text.

Never trust extension alone.

2. Trusting $_FILES[‘type’]

Some developers check:

if ($_FILES['file']['type'] === 'image/jpeg')

This is not safe.

The browser sends this value. The user can change it.

PHP provides the finfo extension for detecting the real MIME type. You must detect MIME type on the server using finfo.

We will see that later.

3. Storing Files Inside Public Directory

This is very common.

Example:

/var/www/html/uploads/

If someone uploads malicious.php and your server allows execution, the attacker can run:

https://example.com/uploads/malicious.php

Now your server runs attacker code. This is how many small sites get compromised. Uploads should not be executable.

4. No File Size Limit

If you do not restrict size:

Someone can upload 2GB file.

  • Disk space gets full.
  • Server becomes slow.
  • Application crashes.

Size must be restricted:

  • In php.ini
  • In application logic

Both.

5. Path Traversal

If you build file paths like this:

$destination = 'uploads/' . $_FILES['file']['name'];

An attacker may try:

../../config.php

This can overwrite important files. Always control the final file name yourself. Never use user file name directly.

6. Race Conditions

If you validate first and then move later, sometimes files can be swapped or replaced.

This is rare but possible in poorly designed systems. Validation and moving must be done carefully and quickly.

7. Allowing Dangerous File Types

Some file types should never be allowed:

  • .php
  • .phtml
  • .phar
  • .exe
  • .sh

If your application does not need them, block them completely. Whitelist approach is safer than blacklist. Allow only what is required.

File upload security is not one rule. It is many small rules working together. In the next section, we will build a clear set of security principles.

Core Security Principles for Safe File Uploads

Security is not one check. It is layers.

We will apply rules in order. Do not skip steps.

Secure File Upload Steps

1. Always Check Upload Errors First

Before anything, check the error code.

if ($file['error'] !== UPLOAD_ERR_OK) { throw new RuntimeException('Upload failed.');
}

If there is an error:

  • File may be incomplete
  • File may not exist
  • Size may exceed server limit

Do not continue if error is not zero.

2. Restrict File Size in Application Code

Do not depend only on php.ini.

Add your own limit.

$maxSize = 2 * 1024 * 1024; // 2MB if ($file['size'] > $maxSize) { throw new RuntimeException('File too large.');
}

Even if server allows 10MB, your app may allow only 2MB. Control it at application level.

3. Detect MIME Type Using finfo

Do not trust $_FILES['type']. Use server side detection.

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);

This checks actual file content. It is more reliable.

4. Use a Whitelist of Allowed Types

Never allow everything except few types. Allow only what is required.

Example:

$allowed = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf',
];
if (!array_key_exists($mime, $allowed)) { throw new RuntimeException('Invalid file type.');
}

Whitelist is safer. Blacklist can miss something.

5. Generate a Safe Random File Name

Never use original file name. User can manipulate it. Generate your own name.

if (!array_key_exists($mime, $allowed)) { throw new RuntimeException('Invalid file type.');
}

This gives:
Random name,
No collisions
No injection risk

6. Store Files Outside Public Web Root

Do not store here:

/var/www/html/uploads

Better:

/var/www/storage/uploads

Files should not be directly accessible. If you need to serve them, use a controlled download script.

7. Use move_uploaded_file()

Do not use rename().

move_uploaded_file($file['tmp_name'], $destination);

This function verifies that the file came from PHP upload. Safer.

8. Disable Script Execution in Upload Folder

Even if you validate, add server protection. Disable execution using:

  • .htaccess for Apache
  • location rules for Nginx

Defense in depth.

These principles are simple. But many systems skip one or two. That is enough for compromise.

In the next section, we will combine everything and build a minimal SecureUploader class in PHP 8. Clean. Small. Production ready.

The OWASP File Upload Cheat Sheet also provides useful security recommendations.

Building a Minimal SecureUploader Class in PHP 8

Now we combine everything. The goal is simple:

  • Validate
  • Restrict
  • Rename
  • Store safely

No framework. No heavy abstraction. Just clear PHP 8 code.


<?php declare(strict_types=1); final class SecureUploader
{ private string $uploadDir; private int $maxSize; private array $allowedMimeTypes; public function __construct(string $uploadDir, int $maxSize, array $allowedMimeTypes) { $this->uploadDir = rtrim($uploadDir, '/'); $this->maxSize = $maxSize; $this->allowedMimeTypes = $allowedMimeTypes; } public function upload(array $file): string { $this->validateError($file); $this->validateSize($file); $mime = $this->detectMimeType($file['tmp_name']); $extension = $this->validateMime($mime); $filename = $this->generateFileName($extension); $destination = $this->uploadDir . '/' . $filename; if (!move_uploaded_file($file['tmp_name'], $destination)) { throw new RuntimeException('Failed to move uploaded file.'); } return $filename; } private function validateError(array $file): void { if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) { throw new RuntimeException('Upload error.'); } } private function validateSize(array $file): void { if ($file['size'] > $this->maxSize) { throw new RuntimeException('File too large.'); } } private function detectMimeType(string $tmpPath): string { $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($tmpPath); if ($mime === false) { throw new RuntimeException('Cannot detect MIME type.'); } return $mime; } private function validateMime(string $mime): string { if (!array_key_exists($mime, $this->allowedMimeTypes)) { throw new RuntimeException('Invalid file type.'); } return $this->allowedMimeTypes[$mime]; } private function generateFileName(string $extension): string { return bin2hex(random_bytes(16)) . '.' . $extension; }
}

Example Usage


$uploader = new SecureUploader( __DIR__ . '/../storage/uploads', 2 * 1024 * 1024, [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf', ]
); $filename = $uploader->upload($_FILES['document']);

Why This Design Is Good

  • Strict types enabled
  • No global variables
  • Clear separation of validation steps
  • No original file name used
  • No public directory storage
  • No silent failure

Small class. Easy to maintain. Easy to test. You can extend later if needed.

Security should be simple. Complex security often fails.

Server-Level Hardening

Even if your PHP code is perfect, server configuration matters.

Defense should not depend on one layer only.

1. Apache Hardening (.htaccess)

If you use Apache and your uploads are inside a web-accessible folder, disable script execution.

Create a .htaccess file inside the upload directory:

php_flag engine off
Options -ExecCGI
AddType text/plain .php .phtml .php3 .php4 .php5 .php7 .phar

This prevents PHP files from executing. Even if someone manages to upload a .php file, it will not run. It will be treated as plain text. That is important.

2. Nginx Hardening

In Nginx, you usually configure this in your server block.

Example:

location /uploads/ {
autoindex off;
types { }
default_type text/plain;
}

Or more strictly, block script execution:

location ~* ^/uploads/.*\.(php|phtml|phar)$ {
deny all;
}

This blocks access to executable scripts inside uploads.

3. Why This Matters

Many real attacks succeed because:

  • Code validation failed once.
  • Or developer made a mistake.
  • Or a new file type was allowed accidentally.

Server-level restriction reduces damage. Even if application logic has a bug, server can stop execution. That is called defense in depth.

4. Best Practice

Best approach is:

  • Store uploads outside public directory.
  • If that is not possible, disable execution.
  • Always use both application and server validation.

Never depend on one protection only.

Security is layers. Code layer. Server layer. Configuration layer.

Additional Safeguards for Production Systems

Basic validation is not enough for high traffic or sensitive systems. Here are extra protections you should consider.

1. Re-Encode Uploaded Images

If you allow images, do not store them directly. Attackers can hide malicious code inside image metadata.

Better approach:

  • Open image using GD or Imagick
  • Re-save it
  • Discard original file

Example idea:

$image = imagecreatefromjpeg($tmpPath);
imagejpeg($image, $destination, 90);
imagedestroy($image);

This removes hidden metadata. You keep only clean image data.

2. Virus Scanning

For document uploads like PDF or DOC files, consider scanning. You can use tools like ClamAV

Upload file.
Scan file.
If infected, reject it.

This is useful for:

  • LMS platforms
  • HR portals
  • Customer document systems

3. Rate Limiting Uploads

If someone uploads 1000 files per minute, it can overload the system.

Add rate limits:

  • Per user
  • Per IP
  • Per session

Even simple limits help.

4. Logging Upload Activity

Do not ignore uploads.

Log:

  • User ID
  • File name generated
  • Timestamp
  • IP address

If something goes wrong, logs help investigation. Security without logs is blind.

5. Limit Number of Files

If your form allows multiple files, control it. Do not allow unlimited uploads. Set clear limits.

6. Set Proper File Permissions

When storing files, ensure correct permissions.

Example:

  • Files should not be executable
  • Use minimal required permissions

Do not use full permissions like 777. Keep it restricted.

These safeguards are not complicated. But many systems skip them.

Security is habit. Not one time effort.

Secure File Upload Checklist

Use this checklist before deploying file upload to production.

Validation

  • Check UPLOAD_ERR_OK before processing.
  • Reject file if error code is not zero.
  • Restrict file size in application code.
  • Do not trust $_FILES[‘type’].
  • Detect MIME type using finfo.
  • Use whitelist of allowed MIME types only.

File Handling

  • Never use original file name.
  • Generate random file name using random_bytes.
  • Store files outside public web root.
  • Use move_uploaded_file() only.
  • Do not use rename() for uploads.

Server Configuration

  • Disable script execution in upload folder.
  • Block .php, .phtml, .phar in uploads.
  • Set proper file permissions.
  • Do not allow directory listing.

Production Safeguards

  • Re-encode images before storing.
  • Scan documents for malware if needed.
  • Limit upload rate per user or IP.
  • Log upload activity.

If your system follows all the above, risk is reduced significantly.

No system is 100 percent secure. But layered protection makes attacks much harder.

FAQ

Is move_uploaded_file() secure in PHP?

Yes, when used correctly. The function itself verifies that the file was uploaded through HTTP POST. But it does not validate file type, size, or safety. You must combine it with MIME validation, file size checks, and safe storage practices.

Is checking file extension enough for secure upload?

No. File extensions can be renamed easily. A file named image.jpg can actually contain PHP code. Always validate the real MIME type using finfo on the server.

Should uploaded files be stored inside the public folder?

It is not recommended. If stored inside a public directory, the file may become directly accessible through URL. Store files outside the web root when possible. If not possible, disable script execution in the upload folder.

What is the safest way to handle file uploads in PHP?

Use layered validation. Check upload errors. Restrict file size. Detect MIME type using finfo. Whitelist allowed types. Generate random file names. Store files outside the web root. Apply server-level restrictions.

Conclusion

File uploads look small. But they carry real risk. Many security problems do not come from advanced attacks. They come from simple assumptions. Trusting the file extension. Trusting the browser MIME type. Storing files inside a public folder. Skipping server restrictions. These small mistakes open the door.

Secure file upload is not about one function. It is about discipline. Check errors. Restrict size. Detect the real MIME type. Allow only required formats. Generate safe file names. Store files outside the web root. Disable execution at the server level. Each step is simple. Together, they make the system strong.

PHP is not insecure. Insecure design is. If you treat file uploads as an attack surface and not just a feature, your application becomes safer. Keep it simple. Keep it strict. Do not trust user input. That is enough.

Posted on Leave a comment

Is creating a book still worth it in 2026?

What are some of the benefits of writing your own book? Let’s recap the basics, starting with more non-fiction oriented benefits and moving into higher-level fiction writing benefits later in this article.

Benefit 1: Career Prospects 🚀 and Authority

Not everybody respects book authors. Yet – most do:

“Publishing a book is still a powerful authority signal: one survey found that 75% of people view professionals as more qualified thought leaders when they have authored a book, while broader B2B research shows that 73% of buyers trust thought leadership more than traditional marketing when judging expertise.” – ChatGPT Deep Research

Publishing an authority book can be viewed as rocket fuel for your career – especially if book writing is not your main job but you’re doing it on the side.

It’s a classic positive expected-value activity:

  • Some will respect you more for having written a book on the side.
  • Some will respect you much more.
  • And the rest will respect you the same. 

Nobody will respect you less.

Now you may ask: How can it help me in my specific career?

Well, it might help you get a better job or get respected more in your current job/business.

Story: A friend of mine one day decided to coauthor a technical authority book about an engineering-related topic he was interested in (on the side). At the time, he was working a job in the social sector. The book was the key trust element that got him the exact dream job position at the company creating the engineering tool he was writing about. He loves his new job more and earns twice the income. The book was a major element of this success story – he may not have gotten the job without the book. 

For example, say you publish books in your area of expertise, build authority, and ultimately lift your income by only 20% as a first- or second-order consequence. The average salary in the US is roughly $60k, so +20% creates an additional +$12k per year. Investing that additional +$12k/y into an index fund yielding 9% p.a. results in a nice additional nest egg of $613k after 20 years.

The compounding effects of book writing can be magical! Of course this carries a few assumptions but nothing too unrealistic: 9% annual yield, a one-time 20% salary boost by building authority in your field, that’s about it.

And also please note that I didn’t even mention the immediate first-order cash flow your book might generate passively on Amazon KDP, for instance.

How can you publish a book – or multiple books – in your area of expertise? 

Well, just get started writing a draft with a non-fiction engine.

You can edit the book in a Word document and include your own stories, so it becomes yours.

I know you will find that the quality of your generated book is surprisingly high. Paying the equivalent of a coffee at Starbucks to become a (published) author is not too heavy a burden.

✨ You could be a published author this week!

Benefit 2: Personal Development + Learning

If you want to master any subject, write about it. Not enough time? The next best thing is to prompt AI to write about it – then read what it wrote. 

This way, you can generate books about your weird fringe interests to learn about hyper-individualized subjects such as:

  • How to Get Yourself to Bed as a Mom (33) When Nighttime Is the Only Time You Feel Free
  • How to Run a Household Where Everyone Eats Differently Without Cooking Four Dinners
  • How to Become Socially Fluent When You’re Already Smart but Somehow Weird in Groups
  • How to Prepare for a Nuclear Conflict in Stockholm/Sweden
  • Tailor-Made Diet Recommendations for 36-Year-Old Men with IBS and Skin Problems

You get the point, it could become even more specific like ‘Understanding Thermodynamics as a 43-Year-Old Working in Elderly Care’.

These books don’t exist but you can write them easily – one or even all of them – and own the rights to publish or consume them. Producing such a highly personalized book with ImagineYourBook might be even cheaper than ordering it on Amazon!

Benefit 3: Bragging Rights

I didn’t want to leave this unnamed.

Yeah, it’s not super sophisticated but I know that many people will publish books just so they can identify as authors.

While I didn’t build a first-class book-writing engine to fulfill people’s needs for external validation, I still know that many will use it exactly for that reason. 

I will not judge you for maximizing your well-deserved bragging rights.

Benefit 4: Self-Actualization, Fiction, and Travel

Let’s not go there too deeply – but what if your basic needs are already satisfied? What if you already have money, status, and you don’t need to learn new random stuff or monetize books?

In that case, you’re probably an avid fiction reader yourself. You might even have some novel ideas or need to read certain types of fiction books that you may not easily find on Amazon.

For example, I’ve always loved “The Talented Mr. Ripley”-style stories. I love the Mediterranean and go there regularly with my family. With ImagineYourBook, I can now generate exactly the fiction book I want, set in the vacation area I’m currently visiting. For instance, I can learn local history, culture, and what great places to visit by reading a crime novel set in a small village on the Amalfi Coast in Italy.

 👉 Write your travel guide fiction book with our engine

Benefit 5: Fun and Play

Last but not least, the best thing about AI is that it allows us to produce fun little things quickly and fool around.

You can write stories with your loved ones as protagonists and their specific character traits. You can write what-if stories changing physical laws such as gravity. You can write stories about your own childhood. You can invent stories that play like live games as you read through them.

Your imagination is now the only limit.

Imagine Your Book!

The post Is creating a book still worth it in 2026? appeared first on Be on the Right Side of Change.

Posted on Leave a comment

What to Code (Book): How Indie Hackers Find Million-Dollar Micro-SaaS Ideas in the Vibe Coding Era

Most builders still think the hard part is coding.

That used to be true. It isn’t anymore.

Today, with AI tools, templates, and vibe coding workflows, a single person can build in days what used to take a small team weeks. That sounds like good news, and it is. But it changes the game. When software becomes easier to produce, the real bottleneck moves upstream.

The scarce skill is no longer just execution. It is judgment.

That is the core idea behind What to Code: in a world where almost anything can be built, the real advantage comes from choosing the right thing to build.

The book makes a simple but powerful point: most projects fail long before launch, not because the code is bad, but because the premise is weak. Builders fall in love with elegant solutions, trendy categories, or new technical capabilities, then go looking for a problem to attach them to. That is backwards.

Pain is the Way

A better approach is to start with pain.

Not vague dissatisfaction. Not “people want to be more productive.” Real pain. Repeated pain. Costly pain. The kind that already shows up in workarounds, spreadsheets, manual cleanup, delays, mistakes, or quiet frustration that someone has learned to tolerate because there is no better option.

That is where good software opportunities usually come from.

One of the most useful ideas in the book is that strong opportunities tend to have four traits:

  • the problem is real,
  • it happens repeatedly,
  • the affected users are reachable, and
  • the builder has some meaningful fit with the space.

That fit matters more than most people think. The same idea can be great for one founder and terrible for another, depending on access, trust, domain knowledge, and distribution.

The Money is in the Niche

Another big takeaway: specificity beats breadth.

Broad ideas sound exciting. “AI for small business operations” sounds bigger than “a tool that catches missing attachments before insurance claims are submitted.” But broadness usually hides weak urgency. Specificity is what makes products adoptable. A concrete problem in a real workflow is easier to explain, easier to test, easier to sell, and easier to improve.

The book is also strong on validation. Demand is not praise. It is not likes, compliments, or “that sounds useful.” Demand is behavior. Do people try it? Come back? Change their workflow? Pay? Recommend it? If not, the signal is weak, no matter how encouraging the conversation felt.

Automate the … Boring Stuff?

The deepest lesson is probably this: boring problems are underrated.

A lot of money is hiding in ugly workflows — invoicing, approvals, claims, scheduling, reporting, reconciliation, handoffs, compliance checks, repetitive admin. These problems are not glamorous, but they are expensive. And expensive, recurring friction is exactly where small software businesses become real businesses.

What to Code — the practical summary

The book’s core argument is simple: in a world where building software is getting easier, the main advantage is no longer raw execution speed. The advantage is choosing a problem that is painful, repeated, reachable, and close enough to your own edge that you can actually solve and sell it. The mistake most builders make is starting with an idea, a tool, or a capability. A better process is to start with a recurring cost in the real world: wasted time, repeated errors, messy handoffs, manual cleanup, delayed billing, unclear approvals, or ugly workarounds people tolerate because nothing better exists.

The useful filter

Question Strong signal Weak signal
Is the problem real? People already complain, workaround it, or waste time on it weekly People say it “sounds useful”
Is it repeated? Daily or weekly pain Rare or one-off pain
Is it costly? Time, money, delays, mistakes, compliance risk Mild convenience issue
Are users reachable? You know where they are and how to talk to them “Everyone” is the user
Does it fit existing workflow? Slots into something they already do Requires a whole new habit
Is software the right fix? Repetition, routing, data cleanup, search, classification Mostly cultural or political problem
Do you have builder fit? Domain knowledge, trust, access, distribution, patience No edge, no access, no credibility
Can you test fast? You can get real behavior in days You need months to learn anything

This is basically the book’s opportunity lens: real pain, repeated need, reachable users, right builder. If one of those is missing, the idea may still be interesting, but it is probably weak.

What to look for in the wild

The best software ideas often hide inside boring operational friction:

Look for this Why it matters
A spreadsheet that “shouldn’t exist” It often means the official workflow is broken
A task someone does “every time” Repetition is where software wins
A process held together by one careful person That is human glue covering system weakness
Delays before billing, approvals, or handoffs Time lag often has direct monetary cost
Re-entering data across tools Translation work is classic automation territory
Repeated manual checks Good target for software-assisted validation
Teams exporting from one system just to work in another Strong sign of poor workflow fit

The book makes an important distinction here: broad ideas sound exciting, but specificity beats breadth. “AI for small business ops” is vague. “A tool for bookkeeping firms that extracts invoice fields from emailed PDFs into the review queue” is specific enough to test, explain, and sell.

The behavior test

The clearest lesson in the manuscript is that demand is behavior, not praise.

What people say What it usually means
“Cool idea” Very weak signal
“I’d use that” Still weak
“Can you show me?” Better
“Can I try it on my real data?” Strong
“Can this fit into our workflow?” Very strong
“How much?” Strong buying signal
“This would save us every week” Excellent
They come back and use it again Best signal

The best one-sentence takeaway

Do not ask, “What could I build?”
Ask, “What costly, repeated friction can I remove for people I can actually reach?”

A practical next step

Take your top 3 ideas and score each one from 1–5 on:

  • severity,
  • frequency,
  • measurable cost,
  • reachability,
  • software fit,
  • existing workaround evidence,
  • willingness to act/pay,
  • specificity,
  • builder fit,
  • speed to useful test.

Then kill the weakest one immediately.

Conclusion

If you build software, this book gives you a much better filter for deciding what deserves your time. It pushes you away from random idea generation and toward observed reality, where the best opportunities usually hide.

If that sounds useful, you can get the full book here: What to Code on Amazon

The post What to Code (Book): How Indie Hackers Find Million-Dollar Micro-SaaS Ideas in the Vibe Coding Era appeared first on Be on the Right Side of Change.

Posted on Leave a comment

6 Best AI Book Writers (2026): Tools for Authors & Self-Publishers

Want to write a book faster? You probably want to stay organized and keep your ideas clear, too.

AI book writers now play a real role for authors in 2026. These tools have improved, and you’ve got more options than ever.

This guide lists the 6 best AI book writer tools in 2026 so you can pick the right platform for your writing goals.

You’ll see how ImagineYourBook.com, Sudowrite, Claude, Novelcrafter, Jasper AI, and NovelAI compare. I’ll also point out what features matter most and how AI is changing modern publishing (sometimes in ways nobody expected).

1. ImagineYourBook.com

ImagineYourBook.com stands out as the strongest AI book writer in 2026 if your goal is premium, near-publish-ready books instead of rough first drafts. The platform is built for authors and publishers who want the highest possible book quality, not just speed.

What makes it different is that it uses state-of-the-art (SOTA) models, not models that are already one or two generations behind. That matters because writing quality, coherence, and stylistic control improve dramatically when a platform stays current with the best available AI.

Here’s a series of books that have been generated with this AI book writer:

But the real advantage goes deeper than model choice. ImagineYourBook.com is designed as a full end-to-end book generation system. It does not just generate isolated chapters. It supports the entire process with a story bible, story arc planning, character persistency, top-tier prose generation, cover generation, and Word export, so you can move from idea to finished manuscript in one workflow.

Its biggest quality advantage comes from how it handles context. Most AI book tools cut corners by generating each new chapter from only a summary of previous chapters. That approach saves tokens, but it weakens the final book. Important details get lost. Character voices drift. Repetition increases. Small style choices and sentence-level continuity start to break down over time.

ImagineYourBook.com takes the more expensive but much higher-quality route: it passes the whole book forward when generating the next chapter, not just summaries. That means the AI keeps track of the manuscript at a much deeper level. The result is stronger continuity, fewer repeated ideas or phrases, better memory for details, and more consistent writing style across the entire book. Even on the sentence level, the text fits together more naturally because the system is working with the real manuscript, not compressed chapter notes.

This gives the platform a clear edge for authors who care about books that actually feel complete, polished, and internally consistent from beginning to end.

Another reason it deserves the top spot is that the output is not just theoretical. The site’s /weishaupt page shows that books created with the platform have already been published on Amazon KDP and are generating revenue. That is an important proof point. Many AI book tools promise results, but ImagineYourBook.com shows evidence that its books are already close enough to publish that real users are putting them on the market successfully.

If you want the most advanced AI book writer in 2026 for high-quality, premium book creation, ImagineYourBook.com is the tool to beat.

Disclaimer: As we are the tool creators, we may be biased. But we tried many other tools and found them frustrating. This is what we aimed to solve with our premium AI book writer.

2. Sudowrite

Sudowrite is all about fiction. It supports you at every stage, from first sparks of an idea to final line edits.

The platform uses a model built for storytelling. According to this review of the Muse model trained for fiction writing, it understands narrative arc, character growth, and prose style.

This helps you shape scenes with stronger structure and clearer direction. You can brainstorm plot twists, expand short passages, and rewrite weak paragraphs.

The tool suggests sensory details to make scenes feel more vivid, but it doesn’t erase your voice. One standout feature is its Story Bible.

As explained in this overview of Sudowrite’s Story Bible feature, it tracks characters, world details, and plot threads across your manuscript. This helps you avoid continuity errors in long projects or series.

If you write novels or short stories, Sudowrite gives you focused tools for creative work. It’s not just another generic content writer.

3. Claude

Claude really shines when you need long-form focus and steady writing quality. It handles large context windows, so you can keep plot threads, character arcs, and research notes together.

That makes it useful for full-length books. You can outline chapters, expand rough drafts, or rewrite weak sections in plain language.

Claude tends to follow instructions closely. That helps when you want to set tone, pacing, or point of view. You’re still in control of style and direction.

Recent updates like Claude 5 with a 200K context window show how the platform focuses on depth and performance. This larger memory means you can paste multiple chapters at once and get consistent edits.

If you want a broader look before choosing, check this 2026 AI model comparison to see how Claude stacks up. That context helps you decide if it fits your workflow.

Claude works best as a writing partner. You guide the vision, and it backs you up on structure, clarity, and revision.

4. Novelcrafter

Novelcrafter gives you strong control over story structure. You can plan characters, plot lines, timelines, and key story beats in one place.

This keeps long projects clear and organized. Many writers see it as a full planning system, not just a text generator.

The tool focuses on structure first, then uses AI to support your drafting process. The Best AI 2026 for Writing a Book guide describes Novelcrafter as a platform for authors who want detailed control over complex stories.

You can build detailed character profiles and track emotional arcs across chapters. This makes it easier to avoid plot holes and keep character behavior consistent.

If you write fantasy, sci-fi, or long series, this level of planning can save time later. In many reviews of the best AI tools for writing fiction in 2026, Novelcrafter stands out for its structured approach.

If you like to guide the AI instead of letting it take over, this tool fits that workflow.

5. Jasper AI

Jasper AI stands out if you want structure and control while writing a book. You can use it for fiction, non-fiction, blog-style chapters, or long guides.

It works well when you need steady output and clear organization. Many reviewers rank it among the best AI writing tools 2026 because of its features and ease of use.

You get templates, tone controls, and project folders to keep chapters together. This helps you manage big drafts without losing track of ideas.

Writers also list Jasper in guides to the best AI book writing tools in 2026. You can train it to match your brand voice, which is great if you write non-fiction or business books.

The interface feels clean and direct. Jasper works best when you give clear prompts and outlines. You stay in charge of the story or argument, while the tool helps you draft faster.

6. NovelAI

NovelAI gives you strong control over story style and tone. You can guide the AI to match your voice and shape scenes how you want.

This is useful for fiction writers who care about consistency. Many reviews list it among the top AI novel writing software in 2026.

Writers often praise its clean interface and focus on storytelling. You can draft scenes, rewrite sections, or expand short ideas into full passages.

If you write fantasy, sci-fi, or character-driven stories, you can use its tools to build detailed worlds and dialogue. The system keeps track of story context, so you stay on plot.

You still need to review and edit, but it can speed up early drafts. Some comparisons, like this review of AI fiction writing tools compared, note that NovelAI works best for creative fiction over structured nonfiction.

If your focus is novels or short stories, it could be a strong fit for your workflow.

Key Features to Consider in 2026

You need more than a tool that can simply generate words. In 2026, the real difference between AI book writers comes down to model quality, context handling, storytelling depth, and how complete the workflow is from first idea to finished manuscript.

One of the biggest factors is model quality. Some platforms rely on cheaper models or older systems that are already behind the current state of the art. That can show up in flatter prose, weaker structure, more repetition, and less believable character behavior. Premium tools like ImagineYourBook.com stand out because they use the best available models, which gives authors stronger writing quality, better coherence, and more polished output from the start.

Another major feature is context memory. Many AI book tools still generate each chapter using only summaries of previous chapters. That saves cost, but it also weakens continuity. The AI can lose track of details, repeat itself, or drift in tone and style. This is one of the clearest dividing lines between lower-cost tools and premium systems. ImagineYourBook.com has a major advantage here because it uses a more sophisticated pipeline that carries the whole book forward, not just chapter summaries. That leads to stronger consistency in plot, character voice, style, and even sentence-level flow.

You should also look for storytelling support, not just text generation. Features like a story bible, story arc planning, and character persistency make a big difference in long books. Without them, you spend more time fixing continuity problems later. Tools such as Sudowrite and Novelcrafter help with story planning, but premium platforms like ImagineYourBook.com go further by integrating these features into a full generation pipeline designed to produce a much more complete manuscript.

It also helps to choose a platform with an end-to-end workflow. Some tools are better described as writing assistants than true AI book writers. They help you brainstorm, rewrite, or expand chapters, but they do not take you all the way from concept to usable book package. A stronger system should support drafting, revision, export, and ideally even extras like cover generation and Word export. That reduces friction and makes the path to publishing much faster.

Finally, think about the actual quality of the final output. Some tools are good for experimentation, rough drafts, or creative play. Others are built for books that are much closer to publication quality. That is where premium positioning matters. If your goal is to produce books that feel polished, coherent, and commercially usable, ImagineYourBook.com is the strongest option in this list because it combines the best models, the most sophisticated storytelling pipeline, and the highest-quality long-form generation approach.

The best tool depends on your goal. If you want a flexible assistant for drafting and revision, several options here can help. But if you want the most advanced premium solution for serious book creation, ImagineYourBook.com is in a different tier.

Frequently Asked Questions

What are the best AI book writer tools in 2026?

The strongest options in 2026 are ImagineYourBook.com, Sudowrite, Claude, Novelcrafter, Jasper AI, and NovelAI. Each tool serves a slightly different type of writer. Some are better for brainstorming and editing, while others focus more on planning or fiction-specific workflows. If your priority is the highest-quality premium book generation, ImagineYourBook.com stands out as the top choice.

Which AI book writer creates the highest-quality books?

If quality is your main priority, ImagineYourBook.com is the strongest option on this list. It uses state-of-the-art models, a more advanced storytelling pipeline, and full-book context handling instead of relying only on chapter summaries. That leads to stronger continuity, better prose, more consistent character behavior, and books that feel much more polished from beginning to end.

What makes one AI book writer better than another?

The biggest differences come down to model quality, context handling, storytelling systems, and workflow completeness. Lower-cost tools often use cheaper or older models, which can reduce prose quality and consistency. Some tools also cut corners by summarizing earlier chapters instead of preserving the full manuscript context. Premium tools like ImagineYourBook.com invest more in generation quality, which shows up in the final book.

Are AI-generated books publishable?

Yes, AI-assisted books can absolutely be published. The real question is how much editing they need before they are ready. Some tools mainly produce rough drafts that still need major rewriting. Others get much closer to a publishable standard. ImagineYourBook.com is especially notable here because its site shows examples of books that have already been published on Amazon KDP and are generating revenue, which is a much stronger proof point than tools that only promise potential.

Which AI book writer is best for fiction?

That depends on what kind of fiction workflow you want. Sudowrite is strong for fiction-focused brainstorming and scene work. Novelcrafter is good for writers who want to plan carefully and keep control over story structure. NovelAI can be useful for style-driven fiction experiments. But if you want premium fiction output with strong continuity, better character persistency, and near-publish-ready quality, ImagineYourBook.com is the most advanced option here.

Which AI book writer is best for complete end-to-end book creation?

Most tools on this list work best as assistants. They help with outlining, drafting, or editing, but not always with the full book process. ImagineYourBook.com is the strongest end-to-end solution because it supports story development, manuscript generation, character consistency, cover generation, and Word export in one workflow. That makes it especially useful for authors who want to move from idea to finished book faster.

Do cheaper AI book tools produce lower-quality writing?

In many cases, yes. Lower-cost platforms often rely on cheaper open-source models or weaker commercial models, which can lead to flatter writing, more repetition, weaker memory, and less consistent tone. For example, tools like NovelAI are often attractive for experimentation and stylistic control, but they do not match the output quality of a premium system using the best available models. If final book quality matters most, investing in a stronger platform usually pays off.

Is Claude or Jasper enough to write a full book?

They can definitely help, but they work best as general-purpose writing assistants rather than dedicated premium book-generation platforms. Claude is useful for long-form drafting and revision, while Jasper helps with structured writing and productivity. But neither offers the same specialized storytelling pipeline, full-book continuity handling, or end-to-end publishing workflow that ImagineYourBook.com provides.

What should I look for before choosing an AI book writer?

Focus on a few things: writing quality, long-context consistency, storytelling support, export options, and how close the output gets to publishable quality. If you only need help brainstorming or drafting, several tools can work. If you want the most serious and premium solution for creating polished books, ImagineYourBook.com is the clear best fit.

The post 6 Best AI Book Writers (2026): Tools for Authors & Self-Publishers appeared first on Be on the Right Side of Change.

Posted on Leave a comment

JSFX on Fedora Linux: an ultra-fast audio prototyping engine

Introduction

Writing a real-time audio plugin on Linux often conjures up images of a complex environment: C++, toolchains, CMake, CLAP / VST3 / LV2 SDK, ABI…

However, there is a much simpler approach : JSFX

This article offers a practical introduction to JSFX and YSFX on Fedora Linux: we’ll write some small examples, add a graphical VU meter, and then see how to use it as an CLAP / VST3 plugin in a native Linux workflow.

JSFX (JesuSonic Effects – created by REAPER [7]) allows you to write audio plugins in just a few lines, without compilation, with instant reloading and live editing.

Long associated with REAPER, they are now natively usable on Linux, thanks to YSFX [3], available on Fedora Linux in CLAP and VST3 formats via the Audinux repository ([4], [5]).

This means it’s possible to write a functional audio effect in ten lines, then immediately load it into Carla [8], Ardour [9], or any other compatible host, all within a PipeWire / JACK [11] environment.

A citation from [1] (check the [1] link for images):

In 2004, before we started developing REAPER, we created software designed for creating and modifying FX live, primarily for use with guitar processing.

The plan was that it could run on a minimal Linux distribution on dedicated hardware, for stage use. We built a couple of prototypes.

These hand-built prototypes used mini-ITX mainboards with either Via or Intel P-M CPUs, cheap consumer USB audio devices, and Atmel AVR microcontrollers via RS-232 for the footboard controls.

The cost for the parts used was around $600 each.

In the end, however, we concluded that we preferred to be in the software business, not the hardware business, and our research into adding multi-track capabilities in JSFX led us to develop REAPER. Since then, REAPER has integrated much of JSFX’s functionality, and improved on it.

So, as you can see, this technology is not that new. But the Linux support via YSFX [3] is rather new (Nov 2021, started by Jean-Pierre Cimalando).

A new programming language, but for what ? What would one would use JSFX for ?

This language is dedicated to audio and with it, you can write audio effects like an amplifier, a chorus, a delay, a compressor, or you can write synthesizers.

JSFX is good for rapid prototyping and, once everything is in place, you can then rewrite your project into a more efficient language like C, C++, or Rust.

JSFX for developers

Developing an audio plugin on Linux often involves a substantial technical environment. This complexity can be a hindrance when trying out an idea quickly.

JSFX (JesuSonic Effects) offers a different approach: writing audio effects in just a few lines of interpreted code, without compilation and with instant reloading.

Thanks to YSFX, available on Fedora Linux in CLAP and VST3 formats, these scripts can be used as true plugins within the Linux audio ecosystem.

This article will explore how to write a minimal amplifier in JSFX, add a graphical VU meter, and then load it into Carla as a CLAP / VST3 plugin.

The goal is simple: to demonstrate that it is possible to prototype real-time audio processing on Fedora Linux in just a few minutes.

No compilation environment is required: a text editor is all you need.

YSFX plugin

On Fedora Linux, YSFX comes in 3 flavours :

  • a standalone executable ;
  • a VST3 plugin ;
  • a CLAP plugin.

YSFX is available in the Audinux [5] repository. So, first, install the Audinux repository:

$ dnf copr enable ycollet/audinux

Then, you can install the version you want:

$ dnf install ysfx
$ dnf install vst3-ysfx
$ dnf install clap-ysfx

Here is a screenshot of YSFX as a VST3 plugin loaded in Carla Rack [8]:

Screenshot of YSFX effect VST3 plugin loaded in Carla-rack

You can :

  • Load a file ;
  • Load a recent file ;
  • Reload a file modified via the Edit menu ;
  • Zoom / Unzoom via the 1.0 button ;
  • Load presets ;
  • Switch between the Graphics and Sliders view.

Here is a screenshot of the Edit window:

Screenshot of the editor Window opened via the YSFX plugin.

The  Variables  column displays all the variables defined by the loaded file.

Examples

We will use the JSFX documentation available at [4].

JSFX code is always divided into section.

  • @init : The code in the @init section gets executed on effect load, on samplerate changes, and on start of playback.
  • @slider : The code in the @slider section gets executed following an @init, or when a parameter (slider) changes
  • @block : The code in the @block section is executed before processing each sample block. Typically a block is the length as defined by the audio hardware, or anywhere from 128-2048 samples.
  • @sample : The code in the @sample section is executed for every PCM (Pulse Code Modulation) audio sample.
  • @serialize : The code in the @serialize section is executed when the plug-in needs to load or save some extended state.
  • @gfx [width] [height] : The @gfx section gets executed around 30 times a second when the plug-ins GUI is open.

A simple amplifier

In this example, we will use a slider value to amplify the audio input.

desc:Simple Amplifier
slider1:1<0,4,0.01>Gain @init
gain = slider1; @slider
gain = slider1; @sample
spl0 *= gain;
spl1 *= gain;

slider1, @init, @slider, @sample, spl0, spl1 are JSFX keywords [1].

Description:

  • slider1: create a user control (from 0 to 4 here);
  • @init: section executed during loading;
  • @slider: section executed when we move the slide;
  • @sample: section executed for each audio sample;
  • spl0 and spl1: left and right channels.
  • In this example, we just multiply the input signal by a gain.

Here is a view of the result :

Screenshot of the simple gain example

An amplifier with a gain in dB

This example will create a slider that will produce a gain in dB.

desc:Simple Amplifier (dB)
slider1:0<-60,24,0.1>Gain (dB) @init
gain = 10^(slider1/20); @slider
gain = 10^(slider1/20); @sample
spl0 *= gain;
spl1 *= gain;

Only the way we compute the gain changes.

Here is a view of the result :

Screenshot of the simple gain in dB example

An amplifier with an anti-clipping protection

This example adds protection against clipping and uses a JSFX function for that.

desc:Simple Amplifier with Soft Clip
slider1:0<-60,24,0.1>Gain (dB) @init
gain = 10^(slider1/20); @slider
gain = 10^(slider1/20);
function softclip(x) ( x / (1 + abs(x));
); @sample
spl0 = softclip(spl0 * gain);
spl1 = softclip(spl1 * gain);

Here is a view of the result :

Screenshot of the simple gain in dB with. a soft clip example

An amplifier with a VU meter

This example is the same as the one above, we just add a printed value of the gain.

desc:Simple Amplifier with VU Meter
slider1:0<-60,24,0.1>Gain (dB) @init
rms = 0;
coeff = 0.999; // RMS smoothing
gain = 10^(slider1/20); @slider
gain = 10^(slider1/20); @sample
// Apply the gain
spl0 *= gain;
spl1 *= gain;
// Compute RMS (mean value of the 2 channels)
mono = 0.5*(spl0 + spl1);
rms = sqrt((coeff * rms * rms) + ((1 - coeff) * mono * mono)); @gfx 300 200 // UI part
gfx_r = 0.1; gfx_g = 0.1; gfx_b = 0.1;
gfx_rect(0, 0, gfx_w, gfx_h); // Convert to dB
rms_db = 20*log(rms)/log(10);
rms_db < -60 ? rms_db = -60; // Normalisation for the display
meter = (rms_db + 60) / 60;
meter > 1 ? meter = 1; // Green color
gfx_r = 0;
gfx_g = 1;
gfx_b = 0; // Horizontal bar
gfx_rect(10, gfx_h/2 - 10, meter*(gfx_w-20), 20); // Text
gfx_r = gfx_g = gfx_b = 1;
gfx_x = 10;
gfx_y = gfx_h/2 + 20;
gfx_printf("Level: %.1f dB", rms_db);

The global structure of the code:

  • Apply the gain
  • Compute a smoothed RMS value
  • Convert to dB
  • Display a horizontal bar
  • Display a numerical value

Here is a view of the result :

Screenshot of the simple example with a VU meter

An amplifier using the UI lib from jsfx-ui-lib

In this example, we will use a JSFX UI library to produce a better representation of the amplifier’s elements.

First, clone the https://github.com/geraintluff/jsfx-ui-lib repository and copy the file ui-lib.jsfx-inc into the directory where your JSFX files are saved.

desc:Simple Amplifier with UI Lib VU
import ui-lib.jsfx-inc
slider1:0<-60,24,0.1>Gain (dB) @init
freemem = ui_setup(0);
rms = 0;
coeff = 0.999;
gfx_rate = 30; // 30 FPS @slider
gain = 10^(slider1/20); @sample
spl0 *= gain;
spl1 *= gain;
mono = 0.5*(spl0 + spl1);
rms = sqrt(coeff*rms*rms + (1-coeff)*mono*mono); // ---- RMS computation ----
level_db = 20*log(rms)/log(10);
level_db < -60 ? level_db = -60; @gfx 300 200
ui_start("main"); // ---- Gain ----
control_start("main","default");
control_dial(slider1, 0, 1, 0);
cut = (level_db + 100) / 200 * (ui_right() - ui_left()) + ui_left(); // ---- VU ----
ui_split_bottom(50);
ui_color(0, 0, 0);
ui_text("RMS Level: ");
gfx_printf("%d", level_db);
ui_split_bottom(10);
uix_setgfxcolorrgba(0, 255, 0, 1);
gfx_rect(ui_left(), ui_top(), ui_right() - ui_left(), ui_bottom() - ui_top());
uix_setgfxcolorrgba(255, 0, 0, 1);
gfx_rect(ui_left(), ui_top(), cut, ui_bottom() - ui_top());
ui_pop();

The global structure of the example:

  • Import and setup: The UI library is imported and then allocated memory (ui_setup) using @init;
  • UI controls: control_dial creates a thematic potentiometer with a label, integrated into the library;
  • Integrated VU meter: A small graph is drawn with ui_graph, normalizing the RMS value between 0 and 1;
  • UI structure: ui_start(“main”) prepares the interface for each frame. ui_push_height / ui_pop organize the vertical space.

Here is a view of the result :

Screenshot of the simple example with JSFX graphic elements

A simple synthesizer

Now, produce some sound and use MIDI for that.

The core of this example will be the ADSR envelope generator ([10]).

desc:Simple MIDI Synth (Mono Sine)
// Parameters
slider1:0.01<0.001,2,0.001>Attack (s)
slider2:0.2<0.001,2,0.001>Decay (s)
slider3:0.8<0,1,0.01>Sustain
slider4:0.5<0.001,3,0.001>Release (s)
slider5:0.5<0,1,0.01>Volume @init
phase = 0;
note_on = 0;
env = 0;
state = 0; // 0=idle,1=attack,2=decay,3=sustain,4=release @slider
// Compute the increment / decrement for each states
attack_inc = 1/(slider1*srate);
decay_dec = (1-slider3)/(slider2*srate);
release_dec = slider3/(slider4*srate); @block
while ( midirecv(offset, msg1, msg23) ? ( status = msg1 & 240; note = msg23 & 127; vel = (msg23/256)|0; // Note On status == 144 && vel > 0 ? ( freq = 440 * 2^((note-69)/12); phase_inc = 2*$pi*freq/srate; note_on = 1; state = 1; ); // Note Off (status == 128) || (status == 144 && vel == 0) ? ( state = 4; ); );
); @sample
// ADSR Envelope [10]
state == 1 ? ( // Attack env += attack_inc; env >= 1 ? ( env = 1; state = 2; );
); state == 2 ? ( // Decay env -= decay_dec; env <= slider3 ? ( env = slider3; state = 3; );
); state == 3 ? ( // Sustain env = slider3;
); state == 4 ? ( // Release env -= release_dec; env <= 0 ? ( env = 0; state = 0; );
); // Sine oscillator
sample = sin(phase) * env * slider5;
phase += phase_inc;
phase > 2*$pi ? phase -= 2*$pi; // Stereo output
spl0 = sample;
spl1 = sample;

Global structure of the example:

  • Receives MIDI via @block;
  • Converts MIDI note to frequency (A440 standard);
  • Generates a sine wave;
  • Applies an ADSR envelope;
  • Outputs in stereo.

Here is a view of the result :

Screenshot of the synthesizer example

Comparison with CLAP / VST3

JSFX + YSFX

Advantages of JSFX:

  • No compilation required;
  • Instant reloading;
  • Fast learning curve;
  • Ideal for DSP prototyping;
  • Portable between systems via YSFX.

Limitations:

  • Less performant than native C++ for heavy processing;
  • Less suitable for “industrial” distribution;
  • Simpler API, therefore less low-level control.

CLAP / VST3 in C/C++

Advantages:

  • Maximum performance;
  • Fine-grained control over the architecture;
  • Deep integration with the Linux audio ecosystem;
  • Standardized distribution.

Limitations:

  • Requires a complete toolchain;
  • ABI management/compilation;
  • Longer development cycle.

Conclusion

A functional audio effect can be written in just a few lines, adding a simple graphical interface, and then loaded this script as an CLAP / VST3 plugin on Fedora Linux. This requires no compilation, no complex SDK, no cumbersome toolchain.

JSFX scripts don’t replace native C++ development when it comes to producing optimized, widely distributable plugins. However, they offer an exceptional environment for experimentation, learning signal processing, and rapid prototyping.

Thanks to YSFX, JSFX scripts now integrate seamlessly into the Linux audio ecosystem, alongside Carla, Ardour, and a PipeWire-based audio system.

For developers and curious musicians alike, JSFX provides a simple and immediate entry point into creating real-time audio effects on Fedora Linux.

Available plugins

ysfx-chokehold

A free collection of JS (JesuSonic) plugins for Reaper.

Code available at: https://github.com/chkhld/jsfx

To install this set of YSFX plugins:

$ dnf install ysfx-chokehold

YSFX plugins will be available at /usr/share/ysfx-chokehold.

ysfx-geraintluff

Collection of JSFX effects.

Code available at: https://github.com/geraintluff/jsfx

To install this set of YSFX plugins:

$ dnf install ysfx-geraintluff

YSFX plugins will be available at /usr/share/ysfx-geraintluff.

ysfx-jesusonic

Some JSFX effects from Cockos.

Code available at: https://www.cockos.com/jsfx

To install this set of YSFX plugins:

$ dnf install ysfx-jesusonic

YSFX plugins will be available at /usr/share/ysfx-jesusonic.

ysfx-joepvanlier

A bundle of JSFX and scripts for reaper.

Code available at: https://github.com/JoepVanlier/JSFX

To install this set of YSFX plugins:

$ dnf install ysfx-joepvanlier

YSFX plugins will be available at /usr/share/ysfx-joepvanlier.

ysfx-lms

LMS Plugin Suite – Open source JSFX audio plugins

Code available at: https://github.com/LMSBAND/LMS

To install this set of YSFX plugins:

$ dnf install ysfx-lms

YSFX plugins will be available at /usr/share/ysfx-lms.

ysfx-reateam

Community-maintained collection of JS effects for REAPER

Code available at: https://github.com/ReaTeam/JSFX

To install this set of YSFX plugins:

$ dnf install ysfx-reateam

YSFX plugins will be available at /usr/share/ysfx-reateam.

ysfx-rejj

Reaper JSFX Plugins.

Code available at: https://github.com/Justin-Johnson/ReJJ

To install this set of YSFX plugins:

$ dnf install ysfx-rejj

And all the YSFX plugins will be available at /usr/share/ysfx-rejj.

ysfx-sonic-anomaly

Sonic Anomaly JSFX scripts for Reaper

Code available at: https://github.com/Sonic-Anomaly/Sonic-Anomaly-JSFX

To install this set of YSFX plugins:

$ dnf install ysfx-sonic-anomaly

YSFX plugins will be available at /usr/share/ysfx-sonic-anomaly.

ysfx-tilr

TiagoLR collection of JSFX effects

Code available at: https://github.com/tiagolr/tilr_jsfx

To install this set of YSFX plugins:

$ dnf install ysfx-tilr

YSFX plugins will be available at /usr/share/ysfx-tilr.

ysfx-tukan-studio

JSFX Plugins for Reaper

Code available at: https://github.com/TukanStudios/TUKAN_STUDIOS_PLUGINS

To install this set of YSFX plugins:

$ dnf install ysfx-tukan-studio

YSFX plugins will be available at /usr/share/ysfx-tukan-studio.

Webography

[1] – https://www.cockos.com/jsfx

[2] – https://github.com/geraintluff/jsfx

[3] – https://github.com/JoepVanlier/ysfx

[4] – https://www.reaper.fm/sdk/js/js.php

[5] – https://audinux.github.io

[6] – https://copr.fedorainfracloud.org/coprs/ycollet/audinux

[7] – https://www.reaper.fm/index.php

[8] – https://github.com/falkTX/Carla

[9] – https://ardour.org

[10] – https://en.wikipedia.org/wiki/Envelope_(music)

[11] – https://jackaudio.org

Posted on Leave a comment

Modernize .NET Anywhere with GitHub Copilot

Modernizing a .NET application is rarely a single step. It requires understanding the current state of the codebase, evaluating dependencies, identifying potential breaking changes, and sequencing updates carefully.

Until recently, GitHub Copilot modernization for .NET ran primarily inside Visual Studio. That worked well for teams standardized on the IDE, but many teams build elsewhere. Some use VS Code. Some work directly from the terminal. Much of the coordination happens on GitHub, not in a single developer’s local environment.

The modernize-dotnet custom agent changes that. The same modernization workflow can now run across Visual Studio, VS Code, GitHub Copilot CLI, and GitHub. The intelligence behind the experience remains the same. What’s new is where it can run. You can modernize in the environment you already use instead of rerouting your workflow just to perform an upgrade.

The modernize-dotnet agent builds on the broader GitHub Copilot modernization platform, which follows an assess → plan → execute model. Workload-specific agents such as modernize-dotnet, modernize-java, and modernize-azure-dotnet guide applications toward their modernization goals, working together across code upgrades and cloud migration scenarios.

What the agent produces

Every modernization run generates three explicit artifacts in your repository: an assessment that surfaces scope and potential blockers, a proposed upgrade plan that sequences the work, and a set of upgrade tasks that apply the required code transformations.

Because these artifacts live alongside your code, teams can review, version, discuss, and modify them before execution begins. Instead of a one-shot upgrade attempt, modernization becomes traceable and deliberate.

GitHub Copilot CLI

For terminal-first engineers, GitHub Copilot CLI provides a natural entry point.

You can assess a repository, generate an upgrade plan, and run the upgrade without leaving the shell.

  1. Add the marketplace: /plugin marketplace add dotnet/modernize-dotnet
  2. Install the plugin: /plugin install modernize-dotnet@modernize-dotnet-plugins
  3. Select the agent: /agent to select modernize-dotnet
  4. Then prompt the agent, for example: upgrade my solution to a new version of .NET

Modernize .NET in GitHub Copilot CLI

The agent generates the assessment, upgrade plan, and upgrade tasks directly in the repository. You can review scope, validate sequencing, and approve transformations before execution. Once approved, the agent automatically executes the upgrade tasks directly from the CLI.

GitHub

On GitHub, the agent can be invoked directly within a repository. The generated artifacts live alongside your code, shifting modernization from a local exercise to a collaborative proposal. Instead of summarizing findings in meetings, teams review the plan and tasks where they already review code. Learn how to add custom coding agents to your repo, then add the modernize-dotnet agent by following the README in the modernize-dotnet repository.

VS Code

If you use VS Code, install the GitHub Copilot modernization extension and select modernize-dotnet from the Agent picker in Copilot Chat. Then prompt the agent with the upgrade you want to perform, for example: upgrade my project to .NET 10.

Visual Studio

If Visual Studio is your primary IDE, the structured modernization workflow remains fully integrated.

Right-click your solution or project in Solution Explorer and select the Modernize action to perform an upgrade.

Supported workloads

GitHub Copilot modernization supports upgrades across common .NET project types, including ASP.NET Core (MVC, Razor Pages, Web API), Blazor, Azure Functions, WPF, class libraries, and console applications.

Migration from .NET Framework to modern .NET is also supported for application types such as ASP.NET (MVC, Web API), Windows Forms, WPF, and Azure Functions, with Web Forms support coming soon.

The CLI and VS Code experiences are cross-platform. However, migrations from .NET Framework require Windows.

Custom skills

Skills are a standard part of GitHub Copilot’s agentic platform. They let teams define reusable, opinionated behaviors that agents apply consistently across workflows.

The modernize-dotnet agent supports custom skills, allowing organizations to encode internal frameworks, migration patterns, or architectural standards directly into the modernization workflow. Any skills added to the repository are automatically applied when the agent performs an upgrade.

You can learn more about how skills work and how to create them in the Copilot skills documentation.

Give it a try

Run the modernize-dotnet agent on a repository you’re planning to upgrade and explore the modernization workflow in the environment you already use.

If you try it, we’d love to hear how it goes. Share feedback or report issues in the modernize-dotnet repository.