Romania XPlayZM.CSBlackDevil.COM # Zombie Plague UltimateX [ Respawn + FDL + VIP 22-12PM + POINTS] ® 13/32
Unknown ❤ RESPAWN.ZAFEST.RO | VIP FREE | FREE FOR ALL ❤ 0/32
France EMIRATES.KiNGS 亗 GAMEAREA ||͇̿P͇̿U͇̿B͇̿L͇̿I͇̿C͇̿| ✪ 32/32
United Kingdom BALKAN.KiNGS.EU ||͇̿P͇̿U͇̿B͇̿L͇̿I͇̿C͇̿| ✪ 10/32
Promover servidor
101 lines 6.1 KiB Markdown Source Rendered Raw Permalink History
---
title: "SQLx Threaded Queries in AMXX (Without Freezing the Server)"
excerpt: "Use SQL_ThreadQuery to run MySQL queries off the main thread so a slow database never stalls the game, with correct handle lifetime and error handling."
category: "plugin-dev"
tags: "sqlx, mysql"
image: ""
read_time: "8 min read"
---
<p>The single most common way plugin authors wreck server performance is a blocking database query. AMXX runs your Pawn on the main game thread &mdash; the same thread that simulates the world &mdash; so a synchronous <code>SQL_Query</code> that waits 200&nbsp;ms for MySQL freezes every player for 200&nbsp;ms. The fix is <code>SQL_ThreadQuery</code>: it hands the query to a background worker and calls you back when the result is ready, with the main thread never blocking. This guide shows the correct pattern, including the handle lifetime rules that trip people up.</p>

<h2>1. Load the module and make a connection tuple</h2>
<p>You need the <code>sqlx</code> module (see <a href="/tutorials/install-mysql-module-amxx">install the MySQL module</a>). A <em>tuple</em> holds the connection details; create it once and keep it for the plugin's lifetime &mdash; do not rebuild it per query:</p>
<pre>#include &lt;amxmodx&gt;
#include &lt;sqlx&gt;

new Handle:g_tuple

public plugin_init()
{
    register_plugin("SQLx Demo", "1.0", "CSB")
    g_tuple = SQL_MakeDbTuple("127.0.0.1", "csuser", "secret", "cs_stats")
}

public plugin_end()
{
    SQL_FreeHandle(g_tuple)
}</pre>
<p><code>SQL_MakeDbTuple</code> does not connect; it just stores credentials. The connection is opened by the worker when a threaded query runs.</p>

<h2>2. Fire a threaded query</h2>
<p><code>SQL_ThreadQuery(tuple, callback, query)</code> queues the query and returns immediately. Build the SQL with <code>formatex</code>, and always escape untrusted values with <code>SQL_QuoteString</code> first:</p>
<pre>public save_kills(id, kills)
{
    new name[32], safe[64]
    get_user_name(id, name, charsmax(name))
    SQL_QuoteString(g_tuple, safe, charsmax(safe), name)

    new query[256]
    formatex(query, charsmax(query),
        "INSERT INTO stats (name, kills) VALUES ('%s', %d)          ON DUPLICATE KEY UPDATE kills = kills + %d",
        safe, kills, kills)

    SQL_ThreadQuery(g_tuple, "qh_save", query)
}</pre>
<p>Never build a query by pasting a player name straight into it &mdash; a name containing a quote breaks the SQL and is a classic injection vector. <code>SQL_QuoteString</code> is not optional.</p>

<h2>3. The callback</h2>
<p>The worker calls your handler with a fixed signature. Check the failstate <em>first</em>, always &mdash; a dropped connection is reported here, not thrown:</p>
<pre>public qh_save(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime)
{
    if (failstate == TQUERY_CONNECT_FAILED)
    {
        log_amx("SQL connect failed: %s (%d)", error, errnum)
        return
    }
    if (failstate == TQUERY_QUERY_FAILED)
    {
        log_amx("SQL query failed: %s (%d)", error, errnum)
        return
    }

    // success: query is done
}</pre>
<p><strong>Do not call <code>SQL_FreeHandle</code> on the <code>query</code> handle in a threaded callback.</strong> The module owns it and frees it automatically when your handler returns. Freeing it yourself is a double-free. This is the opposite of the non-threaded API, and the rule people get wrong most often.</p>

<h2>4. Reading a result set</h2>
<p>For a <code>SELECT</code>, walk the rows with <code>SQL_MoreResults</code> and <code>SQL_NextRow</code>, reading columns by index or name:</p>
<pre>public qh_load(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime)
{
    if (failstate)
    {
        log_amx("load failed: %s", error)
        return
    }

    while (SQL_MoreResults(query))
    {
        new kills = SQL_ReadResult(query, SQL_FieldNameToNum(query, "kills"))
        server_print("loaded %d kills", kills)
        SQL_NextRow(query)
    }
}</pre>

<h2>5. Passing data to the callback</h2>
<p>The worker runs later, so a local like a player index is gone by the time the callback fires &mdash; and the player may have disconnected. Pass what you need through the <code>data</code> array, and guard against the player leaving:</p>
<pre>new data[1]
data[0] = get_user_userid(id)     // survives across the async gap
SQL_ThreadQuery(g_tuple, "qh_load", query, data, sizeof data)</pre>
<p>Store the <em>userid</em>, not the raw client index &mdash; indices get reused when someone reconnects, and by the time the result arrives, index 5 may be a different person. In the callback, resolve it with <code>find_player</code> using the <code>"k"</code> flag and confirm they are still connected before writing to their HUD.</p>

<h2>Common errors</h2>
<ul>
<li><strong>Server hitches on every save</strong> &mdash; you used <code>SQL_Query</code> (blocking) instead of <code>SQL_ThreadQuery</code>. Move it to the threaded API.</li>
<li><strong>Crash after a query</strong> &mdash; you freed the query handle in the threaded callback. Remove the <code>SQL_FreeHandle(query)</code> call; only free the <em>tuple</em>, and only in <code>plugin_end</code>.</li>
<li><strong><code>TQUERY_CONNECT_FAILED</code> every time</strong> &mdash; wrong host, credentials, or the MySQL user is not allowed from the server's IP. The <code>error</code> string carries the MySQL message; log it.</li>
<li><strong>Writing to the wrong player</strong> &mdash; you passed a client index through <code>data</code> and it was reused. Pass the userid and re-resolve.</li>
<li><strong>Query silently truncated</strong> &mdash; your <code>query</code> buffer was too small. Size it for the longest possible statement and check the <code>formatex</code> return.</li>
</ul>

<h2>Verification</h2>
<p>Watch the server console FPS while a query runs &mdash; a threaded query should cause no dip, a blocking one an obvious stutter. Confirm the row actually landed by checking the table directly in MySQL. Force a failure (stop MySQL, or use bad credentials) and confirm your failstate branch logs a clean message instead of crashing. For a full stats implementation built on this pattern, see <a href="/plugins/csb-rank-mysql">CSB Rank MySQL</a>.</p>