documentation/cookbook/programmatic/php/inserting-ilp.md
QuestDB doesn't maintain an official PHP library, but since the ILP (InfluxDB Line Protocol) is text-based, you can easily send your data using PHP's built-in HTTP or socket functions, or use the official InfluxDB PHP client library.
This guide covers three methods for sending ILP data to QuestDB from PHP:
HTTP with cURL (recommended for most use cases)
InfluxDB v2 PHP Client (easiest to use)
influxdata/influxdb-client-php and guzzlehttp/guzzleTCP Socket (highest throughput)
The ILP protocol allows you to send data to QuestDB using a simple line-based text format:
table_name,comma_separated_symbols comma_separated_non_symbols optional_timestamp\n
Each line represents one row of data. For example, these two lines are well-formed ILP messages:
readings,city=London,make=Omron temperature=23.5,humidity=0.343 1465839830100400000\n
readings,city=Bristol,make=Honeywell temperature=23.2,humidity=0.443\n
The format consists of:
For complete ILP specification, see the ILP reference documentation.
QuestDB supports ILP data via HTTP or TCP. HTTP is the recommended approach for most use cases as it provides better reliability and easier debugging.
To send data via HTTP:
http://localhost:9000/write (or your QuestDB instance endpoint)Content-Type: text/plain headerThe following PHP class provides buffered insertion with automatic flushing based on either row count or elapsed time:
<?php
class DataInserter {
private $endpoint = 'http://localhost:9000/write';
private $buffer = [];
private $bufferSize = 10;
private $flushInterval = 30; // time in seconds
private $lastFlushTime;
public function __construct($bufferSize = 10, $flushInterval = 30) {
$this->bufferSize = $bufferSize;
$this->flushInterval = $flushInterval;
$this->lastFlushTime = time();
}
public function __destruct() {
// Attempt to flush any remaining data when script is terminating
$this->flush();
}
public function insertRow($tableName, $symbols, $columns, $timestamp = null) {
$row = $this->formatRow($tableName, $symbols, $columns, $timestamp);
$this->buffer[] = $row;
$this->checkFlushConditions();
}
private function formatRow($tableName, $symbols, $columns, $timestamp) {
$escape = function($value) {
return str_replace([' ', ',', "\n"], ['\ ', '\,', '\n'], $value);
};
$symbolString = implode(',', array_map(
function($k, $v) use ($escape) { return "$k={$escape($v)}"; },
array_keys($symbols), $symbols
));
$columnString = implode(',', array_map(
function($k, $v) use ($escape) { return "$k={$escape($v)}"; },
array_keys($columns), $columns
));
// Check if timestamp is provided
$timestampPart = is_null($timestamp) ? '' : " $timestamp";
return "$tableName,$symbolString $columnString$timestampPart";
}
private function checkFlushConditions() {
if (count($this->buffer) >= $this->bufferSize || (time() - $this->lastFlushTime) >= $this->flushInterval) {
$this->flush();
}
}
private function flush() {
if (empty($this->buffer)) {
return; // Nothing to flush
}
$data = implode("\n", $this->buffer);
$this->buffer = [];
$this->lastFlushTime = time();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->endpoint);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: text/plain']);
curl_exec($ch);
curl_close($ch);
}
}
// Usage example:
$inserter = new DataInserter(10, 30);
// Inserting rows for London
$inserter->insertRow("test_readings", ["city" => "London", "make" => "Omron"], ["temperature" => 23.5, "humidity" => 0.343], "1650573480100400000");
$inserter->insertRow("test_readings", ["city" => "London", "make" => "Sony"], ["temperature" => 21.0, "humidity" => 0.310]);
$inserter->insertRow("test_readings", ["city" => "London", "make" => "Philips"], ["temperature" => 22.5, "humidity" => 0.333], "1650573480100500000");
$inserter->insertRow("test_readings", ["city" => "London", "make" => "Samsung"], ["temperature" => 24.0, "humidity" => 0.350]);
// Inserting rows for Madrid
$inserter->insertRow("test_readings", ["city" => "Madrid", "make" => "Omron"], ["temperature" => 25.5, "humidity" => 0.360], "1650573480100600000");
$inserter->insertRow("test_readings", ["city" => "Madrid", "make" => "Sony"], ["temperature" => 23.0, "humidity" => 0.340]);
$inserter->insertRow("test_readings", ["city" => "Madrid", "make" => "Philips"], ["temperature" => 26.0, "humidity" => 0.370], "1650573480100700000");
$inserter->insertRow("test_readings", ["city" => "Madrid", "make" => "Samsung"], ["temperature" => 22.0, "humidity" => 0.355]);
// Inserting rows for New York
$inserter->insertRow("test_readings", ["city" => "New York", "make" => "Omron"], ["temperature" => 20.5, "humidity" => 0.330], "1650573480100800000");
$inserter->insertRow("test_readings", ["city" => "New York", "make" => "Sony"], ["temperature" => 19.0, "humidity" => 0.320]);
$inserter->insertRow("test_readings", ["city" => "New York", "make" => "Philips"], ["temperature" => 21.0, "humidity" => 0.340], "1650573480100900000");
$inserter->insertRow("test_readings", ["city" => "New York", "make" => "Samsung"], ["temperature" => 18.5, "humidity" => 0.335]);
?>
This class:
:::tip For production use, consider adding error handling to check the HTTP response status and implement retry logic for failed requests. :::
Another approach is to use the official InfluxDB PHP client library, which supports the InfluxDB v2 write API. QuestDB is compatible with this API, making the client library a convenient option.
Install the required packages via Composer:
composer require influxdata/influxdb-client-php guzzlehttp/guzzle
Required dependencies:
influxdata/influxdb-client-php - The InfluxDB v2 PHP client libraryguzzlehttp/guzzle - A PSR-18 compatible HTTP client (required by the InfluxDB client):::info Alternative HTTP Clients
The InfluxDB client requires a PSR-18 compatible HTTP client. While we recommend Guzzle, you can use alternatives like php-http/guzzle7-adapter or symfony/http-client if preferred.
:::
When using the InfluxDB client with QuestDB:
http://localhost:9000):::warning Write API Only QuestDB only supports the InfluxDB v2 write API when using this client. Query operations are not supported through the InfluxDB client - use QuestDB's PostgreSQL wire protocol or REST API for queries instead. :::
<?php
require __DIR__ . '/vendor/autoload.php';
use InfluxDB2\Client;
use InfluxDB2\Model\WritePrecision;
use InfluxDB2\Point;
// Create client - token, bucket, and org are not used by QuestDB
$client = new Client([
"url" => "http://localhost:9000",
"token" => "", // Not required for QuestDB
"bucket" => "default", // Not used by QuestDB
"org" => "default", // Not used by QuestDB
"precision" => WritePrecision::NS
]);
$writeApi = $client->createWriteApi();
// Write points using the Point builder
// Note: Omit ->time() to let QuestDB assign server timestamps
$point = Point::measurement("readings")
->addTag("city", "London")
->addTag("make", "Omron")
->addField("temperature", 23.5)
->addField("humidity", 0.343);
$writeApi->write($point);
// Write multiple points
$points = [
Point::measurement("readings")
->addTag("city", "Madrid")
->addTag("make", "Sony")
->addField("temperature", 25.5)
->addField("humidity", 0.360),
Point::measurement("readings")
->addTag("city", "New York")
->addTag("make", "Philips")
->addField("temperature", 20.5)
->addField("humidity", 0.330)
];
$writeApi->write($points);
// Always close the client
$client->close();
?>
The Point builder provides several advantages:
:::warning Timestamp Limitation
The InfluxDB PHP client cannot be used with custom timestamps when writing to QuestDB. When you call ->time() with a nanosecond timestamp, the client serializes it in scientific notation (e.g., 1.76607297E+18), which QuestDB's ILP parser rejects.
Solution: Always omit the ->time() call and let QuestDB assign server-side timestamps automatically. This is the only reliable way to use the InfluxDB PHP client with QuestDB.
If you need client-side timestamps: Use the raw HTTP cURL approach (documented above) where you manually format the ILP string with full control over timestamp formatting. :::
TCP over socket provides higher throughput but is less reliable than HTTP. The message format is identical - only the transport changes.
Use TCP when:
Here's a basic example using PHP's socket functions:
<?php
error_reporting(E_ALL);
/* Allow the script to hang around waiting for connections. */
set_time_limit(0);
/* Turn on implicit output flushing so we see what we're getting
* as it comes in. */
ob_implicit_flush();
$address = 'localhost';
$port = 9009;
/* Create a TCP/IP socket. */
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === false) {
echo "socket_create() failed: reason: " . socket_strerror(socket_last_error()) . "\n";
} else {
echo "OK.\n";
}
echo "Attempting to connect to '$address' on port '$port'...";
$result = socket_connect($socket, $address, $port);
if ($result === false) {
echo "socket_connect() failed.\nReason: ($result) " . socket_strerror(socket_last_error($socket)) . "\n";
} else {
echo "OK.\n";
}
$row=utf8_encode("test_readings,city=London,make=Omron temperature=23.5,humidity=0.343 1465839830100400000\n");
echo "$row";
socket_write($socket, $row);
echo "\n";
socket_close($socket);
?>
This basic example:
For production use with TCP, you should:
:::warning TCP Considerations TCP ILP does not provide acknowledgments for successful writes. If the connection drops, you may lose data without notification. For critical data, use HTTP ILP instead. :::
| Feature | HTTP (cURL) | HTTP (InfluxDB Client) | TCP Socket |
|---|---|---|---|
| Reliability | High - responses indicate success/failure | High - responses indicate success/failure | Low - no acknowledgment |
| Throughput | Good | Good | Excellent |
| Error handling | Manual via cURL | Built-in via client library | Manual implementation required |
| Ease of use | Medium - manual ILP formatting | High - Point builder API | Low - manual everything |
| Custom timestamps | ✅ Full control | ❌ Must use server timestamps | ✅ Full control |
| Dependencies | None (cURL built-in) | influxdb-client-php | |
guzzlehttp/guzzle | None (sockets built-in) | ||
| Authentication | Standard HTTP auth | Standard HTTP auth | Limited options |
| Recommended for | Custom timestamps required | Ease of development, server timestamps acceptable | High-volume, loss-tolerant scenarios |
:::info Related Documentation