Hello, the end goal is to make 2 separate scripts to transfer files and have the option to resume upload.
I started with a vibe-coded template as I am still wrapping my head around Amp.
The issue is that I am seeing a cap of 2 max connections when connection: keep-alive is set (default)
Here is the server+client as a debug script
<?php
require __DIR__ . "/vendor/autoload.php";
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use Amp\Http\HttpStatus;
use Amp\Http\Server\DefaultErrorHandler;
use Amp\Http\Server\RequestHandler\ClosureRequestHandler;
use Amp\Http\Server\Request as ServerRequest;
use Amp\Http\Server\Response;
use Amp\Http\Server\Router;
use Amp\Http\Server\SocketHttpServer;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use function Amp\async;
// Start server inline
$logger = new Logger("test");
$logger->pushHandler(new StreamHandler(STDOUT));
$server = SocketHttpServer::createForDirectAccess($logger);
$errorHandler = new DefaultErrorHandler();
$router = new Router($server, $logger, $errorHandler);
$router->addRoute(
"PATCH",
"/test",
new ClosureRequestHandler(function (ServerRequest $request) use (
$logger,
): Response {
$body = $request->getBody();
$body->increaseSizeLimit(100 * 1024 * 1024);
$received = 0;
while (($chunk = $body->read()) !== null) {
$received += strlen($chunk);
}
$logger->info("PATCH received $received bytes");
return new Response(HttpStatus::NO_CONTENT, [
"x-received" => (string) $received,
]);
}),
);
$server->expose("0.0.0.0:18080");
$server->start($router, $errorHandler);
// Run client in same process
$client = new HttpClientBuilder()->followRedirects(0)->build();
$chunkSize = 512 * 1024;
$chunk = str_repeat("A", $chunkSize);
echo "Sending 5 PATCH requests on same connection...\n";
for ($i = 0; $i < 5; $i++) {
$req = new Request("http://127.0.0.1:18080/test", "PATCH");
$req->setHeader("content-type", "application/offset+octet-stream");
$req->setHeader("content-length", (string) $chunkSize);
$req->setHeader("upload-offset", (string) ($i * $chunkSize));
//$req->setHeader("connection", "close");
$req->setBody($chunk);
$resp = $client->request($req);
$resp->getBody()->buffer();
echo "Request $i — status: " .
$resp->getStatus() .
" x-received: " .
$resp->getHeader("x-received") .
"\n";
echo "Request $i headers:\n";
foreach ($resp->getHeaders() as $k => $v) {
echo " $k: " . implode(", ", $v) . "\n";
}
}
$server->stop();
Here is the output
[2026-03-30T06:58:00.269438+00:00] test.NOTICE: Total client connections are limited to 1000. [] []
[2026-03-30T06:58:00.271427+00:00] test.NOTICE: Client connections are limited to 10 per IP address (excluding localhost). [] []
[2026-03-30T06:58:00.271461+00:00] test.NOTICE: Request concurrency limited to 1000 simultaneous requests [] []
[2026-03-30T06:58:00.277372+00:00] test.NOTICE: Request methods restricted to GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE. [] []
[2026-03-30T06:58:00.277412+00:00] test.DEBUG: Starting server [] []
[2026-03-30T06:58:00.281930+00:00] test.INFO: Started server [] []
[2026-03-30T06:58:00.281969+00:00] test.INFO: Listening on http://0.0.0.0:18080/ [] []
Sending 5 PATCH requests on same connection...
[2026-03-30T06:58:00.355374+00:00] test.INFO: PATCH received 524288 bytes [] []
Request 0 — status: 204 x-received: 524288
Request 0 headers:
x-received: 524288
connection: keep-alive
keep-alive: timeout=15
date: Mon, 30 Mar 2026 06:58:00 GMT
transfer-encoding: chunked
[2026-03-30T06:58:00.400123+00:00] test.INFO: PATCH received 524288 bytes [] []
Request 1 — status: 204 x-received: 524288
Request 1 headers:
x-received: 524288
connection: keep-alive
keep-alive: timeout=15
date: Mon, 30 Mar 2026 06:58:00 GMT
transfer-encoding: chunked
PHP Fatal error: Uncaught Amp\Http\Client\ParseException: Invalid status line: 0 in /srv/vendor/amphp/http-client/src/Connection/Internal/Http1Parser.php:158
Stack trace:
#0 /srv/vendor/amphp/http-client/src/Connection/Http1Connection.php(332): Amp\Http\Client\Connection\Internal\Http1Parser->parse()
#1 /srv/vendor/amphp/http-client/src/Connection/Http1Connection.php(264): Amp\Http\Client\Connection\Http1Connection->readResponse()
#2 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/AbstractDriver.php(430): Amp\Http\Client\Connection\Http1Connection->{closure:Amp\Http\Client\Connection\Http1Connection::request():255}()
#3 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/AbstractDriver.php(567): Revolt\EventLoop\Internal\AbstractDriver->invokeMicrotasks()
#4 [internal function]: Revolt\EventLoop\Internal\AbstractDriver->{closure:Revolt\EventLoop\Internal\AbstractDriver::createCallbackFiber():565}()
#5 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/DriverSuspension.php(64): Fiber->resume()
#6 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/AbstractDriver.php(430): Revolt\EventLoop\Internal\DriverSuspension::{closure:Revolt\EventLoop\Internal\DriverSuspension::resume():61}()
#7 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/AbstractDriver.php(621): Revolt\EventLoop\Internal\AbstractDriver->invokeMicrotasks()
#8 [internal function]: Revolt\EventLoop\Internal\AbstractDriver->{closure:Revolt\EventLoop\Internal\AbstractDriver::createCallbackFiber():565}()
#9 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/DriverSuspension.php(64): Fiber->resume()
#10 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/AbstractDriver.php(430): Revolt\EventLoop\Internal\DriverSuspension::{closure:Revolt\EventLoop\Internal\DriverSuspension::resume():61}()
#11 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/AbstractDriver.php(621): Revolt\EventLoop\Internal\AbstractDriver->invokeMicrotasks()
#12 [internal function]: Revolt\EventLoop\Internal\AbstractDriver->{closure:Revolt\EventLoop\Internal\AbstractDriver::createCallbackFiber():565}()
#13 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/AbstractDriver.php(503): Fiber->start()
#14 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/AbstractDriver.php(558): Revolt\EventLoop\Internal\AbstractDriver->invokeCallbacks()
#15 [internal function]: Revolt\EventLoop\Internal\AbstractDriver->{closure:Revolt\EventLoop\Internal\AbstractDriver::createLoopFiber():538}()
#16 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/AbstractDriver.php(96): Fiber->resume()
#17 /srv/vendor/revolt/event-loop/src/EventLoop/Internal/DriverSuspension.php(117): Revolt\EventLoop\Internal\AbstractDriver->{closure:Revolt\EventLoop\Internal\AbstractDriver::__construct():90}()
#18 /srv/vendor/amphp/amp/src/Future.php(251): Revolt\EventLoop\Internal\DriverSuspension->suspend()
#19 /srv/vendor/amphp/http-client/src/Connection/Http1Connection.php(279): Amp\Future->await()
#20 /srv/vendor/amphp/http-client/src/Connection/HttpStream.php(96): Amp\Http\Client\Connection\Http1Connection->request()
#21 /srv/vendor/amphp/http-client/src/functions.php(20): Amp\Http\Client\Connection\HttpStream->{closure:Amp\Http\Client\Connection\HttpStream::request():96}()
#22 /srv/vendor/amphp/http-client/src/Connection/HttpStream.php(96): Amp\Http\Client\processRequest()
#23 /srv/vendor/amphp/http-client/src/Connection/ConnectionLimitingPool.php(131): Amp\Http\Client\Connection\HttpStream->request()
#24 /srv/vendor/amphp/http-client/src/Connection/HttpStream.php(96): Amp\Http\Client\Connection\ConnectionLimitingPool->{closure:Amp\Http\Client\Connection\ConnectionLimitingPool::getStream():124}()
#25 /srv/vendor/amphp/http-client/src/functions.php(20): Amp\Http\Client\Connection\HttpStream->{closure:Amp\Http\Client\Connection\HttpStream::request():96}()
#26 /srv/vendor/amphp/http-client/src/Connection/HttpStream.php(96): Amp\Http\Client\processRequest()
#27 /srv/vendor/amphp/http-client/src/Interceptor/DecompressResponse.php(43): Amp\Http\Client\Connection\HttpStream->request()
#28 /srv/vendor/amphp/http-client/src/Connection/InterceptedStream.php(53): Amp\Http\Client\Interceptor\DecompressResponse->requestViaNetwork()
#29 /srv/vendor/amphp/http-client/src/functions.php(20): Amp\Http\Client\Connection\InterceptedStream->{closure:Amp\Http\Client\Connection\InterceptedStream::request():36}()
#30 /srv/vendor/amphp/http-client/src/Connection/InterceptedStream.php(36): Amp\Http\Client\processRequest()
#31 /srv/vendor/amphp/http-client/src/Interceptor/ModifyRequest.php(36): Amp\Http\Client\Connection\InterceptedStream->request()
#32 /srv/vendor/amphp/http-client/src/Connection/InterceptedStream.php(53): Amp\Http\Client\Interceptor\ModifyRequest->requestViaNetwork()
#33 /srv/vendor/amphp/http-client/src/functions.php(20): Amp\Http\Client\Connection\InterceptedStream->{closure:Amp\Http\Client\Connection\InterceptedStream::request():36}()
#34 /srv/vendor/amphp/http-client/src/Connection/InterceptedStream.php(36): Amp\Http\Client\processRequest()
#35 /srv/vendor/amphp/http-client/src/Interceptor/ModifyRequest.php(36): Amp\Http\Client\Connection\InterceptedStream->request()
#36 /srv/vendor/amphp/http-client/src/Connection/InterceptedStream.php(53): Amp\Http\Client\Interceptor\ModifyRequest->requestViaNetwork()
#37 /srv/vendor/amphp/http-client/src/functions.php(20): Amp\Http\Client\Connection\InterceptedStream->{closure:Amp\Http\Client\Connection\InterceptedStream::request():36}()
#38 /srv/vendor/amphp/http-client/src/Connection/InterceptedStream.php(36): Amp\Http\Client\processRequest()
#39 /srv/vendor/amphp/http-client/src/PooledHttpClient.php(36): Amp\Http\Client\Connection\InterceptedStream->request()
#40 /srv/vendor/amphp/http-client/src/functions.php(20): Amp\Http\Client\PooledHttpClient->{closure:Amp\Http\Client\PooledHttpClient::request():29}()
#41 /srv/vendor/amphp/http-client/src/PooledHttpClient.php(29): Amp\Http\Client\processRequest()
#42 /srv/vendor/amphp/http-client/src/Interceptor/RetryRequests.php(34): Amp\Http\Client\PooledHttpClient->request()
#43 /srv/vendor/amphp/http-client/src/InterceptedHttpClient.php(38): Amp\Http\Client\Interceptor\RetryRequests->request()
#44 /srv/vendor/amphp/http-client/src/functions.php(20): Amp\Http\Client\InterceptedHttpClient->{closure:Amp\Http\Client\InterceptedHttpClient::request():28}()
#45 /srv/vendor/amphp/http-client/src/InterceptedHttpClient.php(28): Amp\Http\Client\processRequest()
#46 /srv/vendor/amphp/http-client/src/Interceptor/ModifyRequest.php(48): Amp\Http\Client\InterceptedHttpClient->request()
#47 /srv/vendor/amphp/http-client/src/InterceptedHttpClient.php(38): Amp\Http\Client\Interceptor\ModifyRequest->request()
#48 /srv/vendor/amphp/http-client/src/functions.php(20): Amp\Http\Client\InterceptedHttpClient->{closure:Amp\Http\Client\InterceptedHttpClient::request():28}()
#49 /srv/vendor/amphp/http-client/src/InterceptedHttpClient.php(28): Amp\Http\Client\processRequest()
#50 /srv/vendor/amphp/http-client/src/HttpClient.php(33): Amp\Http\Client\InterceptedHttpClient->request()
#51 /srv/vendor/amphp/http-client/src/functions.php(30): Amp\Http\Client\HttpClient->{closure:Amp\Http\Client\HttpClient::request():33}()
#52 /srv/vendor/amphp/http-client/src/HttpClient.php(30): Amp\Http\Client\processRequest()
#53 /srv/debug.php(63): Amp\Http\Client\HttpClient->request()
#54 {main}
thrown in /srv/vendor/amphp/http-client/src/Connection/Internal/Http1Parser.php on line 158
Setting $req->setHeader("connection", "close"); in the client fixes it.
Or setting $response->setHeader("connection", "close"); on the server also fixes it.
[2026-03-30T07:21:47.507749+00:00] test.NOTICE: Total client connections are limited to 1000. [] []
[2026-03-30T07:21:47.509760+00:00] test.NOTICE: Client connections are limited to 10 per IP address (excluding localhost). [] []
[2026-03-30T07:21:47.509805+00:00] test.NOTICE: Request concurrency limited to 1000 simultaneous requests [] []
[2026-03-30T07:21:47.515764+00:00] test.NOTICE: Request methods restricted to GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE. [] []
[2026-03-30T07:21:47.515816+00:00] test.DEBUG: Starting server [] []
[2026-03-30T07:21:47.520374+00:00] test.INFO: Started server [] []
[2026-03-30T07:21:47.520412+00:00] test.INFO: Listening on http://0.0.0.0:18080/ [] []
Sending 5 PATCH requests on same connection...
[2026-03-30T07:21:47.594326+00:00] test.INFO: PATCH received 524288 bytes [] []
Request 0 — status: 204 x-received: 524288
Request 0 headers:
x-received: 524288
connection: close
date: Mon, 30 Mar 2026 07:21:47 GMT
[2026-03-30T07:21:47.640191+00:00] test.INFO: PATCH received 524288 bytes [] []
Request 1 — status: 204 x-received: 524288
Request 1 headers:
x-received: 524288
connection: close
date: Mon, 30 Mar 2026 07:21:47 GMT
[2026-03-30T07:21:47.684191+00:00] test.INFO: PATCH received 524288 bytes [] []
Request 2 — status: 204 x-received: 524288
Request 2 headers:
x-received: 524288
connection: close
date: Mon, 30 Mar 2026 07:21:47 GMT
[2026-03-30T07:21:47.728075+00:00] test.INFO: PATCH received 524288 bytes [] []
Request 3 — status: 204 x-received: 524288
Request 3 headers:
x-received: 524288
connection: close
date: Mon, 30 Mar 2026 07:21:47 GMT
[2026-03-30T07:21:47.772027+00:00] test.INFO: PATCH received 524288 bytes [] []
Request 4 — status: 204 x-received: 524288
Request 4 headers:
x-received: 524288
connection: close
date: Mon, 30 Mar 2026 07:21:47 GMT
[2026-03-30T07:21:47.772423+00:00] test.INFO: Stopping server [] []
[2026-03-30T07:21:47.773048+00:00] test.DEBUG: Stopped server [] []
Is this a bug regarding keep-alive or a limitation on the server(2 connection limit)?
Hello, the end goal is to make 2 separate scripts to transfer files and have the option to resume upload.
I started with a vibe-coded template as I am still wrapping my head around Amp.
The issue is that I am seeing a cap of 2 max connections when connection: keep-alive is set (default)
Here is the server+client as a debug script
Here is the output
Setting $req->setHeader("connection", "close"); in the client fixes it.
Or setting $response->setHeader("connection", "close"); on the server also fixes it.
Is this a bug regarding keep-alive or a limitation on the server(2 connection limit)?